solid_queue_mongoid 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. checksums.yaml +7 -0
  2. data/.claude/settings.local.json +38 -0
  3. data/.idea/copilot.data.migration.ask2agent.xml +6 -0
  4. data/.idea/inspectionProfiles/Project_Default.xml +5 -0
  5. data/.idea/jsLibraryMappings.xml +6 -0
  6. data/.idea/misc.xml +17 -0
  7. data/.idea/modules/bigdecimal-4.0.iml +18 -0
  8. data/.idea/modules/builder-3.3.iml +18 -0
  9. data/.idea/modules/concurrent-ruby-1.3.iml +21 -0
  10. data/.idea/modules/connection_pool-3.0.iml +18 -0
  11. data/.idea/modules/crass-1.0.iml +19 -0
  12. data/.idea/modules/docile-1.4.iml +20 -0
  13. data/.idea/modules/drb-2.2.iml +18 -0
  14. data/.idea/modules/erb-6.0.iml +23 -0
  15. data/.idea/modules/et-orbi-1.4.iml +20 -0
  16. data/.idea/modules/fugit-1.12.iml +18 -0
  17. data/.idea/modules/irb-1.17.iml +26 -0
  18. data/.idea/modules/json-2.18.iml +18 -0
  19. data/.idea/modules/lint_roller-1.1.iml +18 -0
  20. data/.idea/modules/mongo-2.23.iml +19 -0
  21. data/.idea/modules/nokogiri-1.19.iml +19 -0
  22. data/.idea/modules/parser-3.3.10.iml +19 -0
  23. data/.idea/modules/pp-0.6.iml +18 -0
  24. data/.idea/modules/prettyprint-0.2.iml +22 -0
  25. data/.idea/modules/prism-1.9.iml +20 -0
  26. data/.idea/modules/raabro-1.4.iml +18 -0
  27. data/.idea/modules/rake-13.3.iml +22 -0
  28. data/.idea/modules/rdoc-7.2.iml +22 -0
  29. data/.idea/modules/regexp_parser-2.11.iml +20 -0
  30. data/.idea/modules/specifications.iml +18 -0
  31. data/.idea/modules/thor-1.5.iml +20 -0
  32. data/.idea/modules/timeout-0.6.iml +22 -0
  33. data/.idea/modules/tsort-0.2.iml +22 -0
  34. data/.idea/modules/unicode-emoji-4.2.iml +19 -0
  35. data/.idea/modules.xml +36 -0
  36. data/.idea/solid_queue_mongoid.iml +3297 -0
  37. data/.idea/vcs.xml +6 -0
  38. data/.idea/workspace.xml +353 -0
  39. data/.rspec +3 -0
  40. data/.rubocop.yml +47 -0
  41. data/ARCHITECTURE.md +91 -0
  42. data/CHANGELOG.md +27 -0
  43. data/CODE_OF_CONDUCT.md +132 -0
  44. data/LICENSE.txt +21 -0
  45. data/README.md +249 -0
  46. data/Rakefile +12 -0
  47. data/lib/solid_queue_mongoid/models/blocked_execution.rb +125 -0
  48. data/lib/solid_queue_mongoid/models/claimed_execution.rb +134 -0
  49. data/lib/solid_queue_mongoid/models/classes.rb +32 -0
  50. data/lib/solid_queue_mongoid/models/execution/dispatching.rb +23 -0
  51. data/lib/solid_queue_mongoid/models/execution/job_attributes.rb +54 -0
  52. data/lib/solid_queue_mongoid/models/execution.rb +65 -0
  53. data/lib/solid_queue_mongoid/models/failed_execution.rb +74 -0
  54. data/lib/solid_queue_mongoid/models/job/clearable.rb +28 -0
  55. data/lib/solid_queue_mongoid/models/job/concurrency_controls.rb +93 -0
  56. data/lib/solid_queue_mongoid/models/job/executable.rb +142 -0
  57. data/lib/solid_queue_mongoid/models/job/recurrable.rb +14 -0
  58. data/lib/solid_queue_mongoid/models/job/retryable.rb +51 -0
  59. data/lib/solid_queue_mongoid/models/job/schedulable.rb +55 -0
  60. data/lib/solid_queue_mongoid/models/job.rb +103 -0
  61. data/lib/solid_queue_mongoid/models/pause.rb +25 -0
  62. data/lib/solid_queue_mongoid/models/process/executor.rb +30 -0
  63. data/lib/solid_queue_mongoid/models/process/prunable.rb +49 -0
  64. data/lib/solid_queue_mongoid/models/process.rb +73 -0
  65. data/lib/solid_queue_mongoid/models/queue.rb +65 -0
  66. data/lib/solid_queue_mongoid/models/queue_selector.rb +101 -0
  67. data/lib/solid_queue_mongoid/models/ready_execution.rb +70 -0
  68. data/lib/solid_queue_mongoid/models/record.rb +147 -0
  69. data/lib/solid_queue_mongoid/models/recurring_execution.rb +62 -0
  70. data/lib/solid_queue_mongoid/models/recurring_task/arguments.rb +29 -0
  71. data/lib/solid_queue_mongoid/models/recurring_task.rb +194 -0
  72. data/lib/solid_queue_mongoid/models/scheduled_execution.rb +43 -0
  73. data/lib/solid_queue_mongoid/models/semaphore.rb +179 -0
  74. data/lib/solid_queue_mongoid/railtie.rb +29 -0
  75. data/lib/solid_queue_mongoid/version.rb +5 -0
  76. data/lib/solid_queue_mongoid.rb +136 -0
  77. data/lib/tasks/solid_queue_mongoid.rake +51 -0
  78. data/release.sh +13 -0
  79. data/sig/solid_queue_mongoid.rbs +4 -0
  80. metadata +173 -0
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Sal Scotto
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,249 @@
1
+ # SolidQueueMongoid
2
+
3
+ [![CI](https://github.com/washu/solid_queue_mongoid/actions/workflows/ci.yml/badge.svg)](https://github.com/washu/solid_queue_mongoid/actions/workflows/ci.yml)
4
+
5
+ A MongoDB/Mongoid adapter for [SolidQueue](https://github.com/basecamp/solid_queue) that allows you to use MongoDB as the backend instead of ActiveRecord/PostgreSQL/MySQL.
6
+
7
+ This gem provides a drop-in replacement for SolidQueue's ActiveRecord models, using Mongoid documents instead. All SolidQueue features are supported including job scheduling, concurrency controls, recurring tasks, and more.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem 'solid_queue_mongoid'
15
+ ```
16
+
17
+ And then execute:
18
+
19
+ ```bash
20
+ bundle install
21
+ ```
22
+
23
+ ## How It Works
24
+
25
+ `solid_queue_mongoid` defines its Mongoid models in the `SolidQueue::` namespace and then requires `solid_queue` for you. This means you only need one gem in your Gemfile — `solid_queue` is pulled in automatically as a dependency.
26
+
27
+ In Rails, the gem's Railtie runs before Rails freezes `eager_load_paths` and tells Zeitwerk to ignore SolidQueue's `app/models` directory, so the ActiveRecord model files are never autoloaded.
28
+
29
+ No special require ordering is needed in your application.
30
+
31
+ ## Configuration
32
+
33
+ ### 1. Configure Mongoid
34
+
35
+ First, ensure you have Mongoid configured in your application. Create or update `config/mongoid.yml`:
36
+
37
+ ```yaml
38
+ development:
39
+ clients:
40
+ default:
41
+ database: my_app_development
42
+ hosts:
43
+ - localhost:27017
44
+ ```
45
+
46
+ ### 2. Configure SolidQueueMongoid
47
+
48
+ Create an initializer at `config/initializers/solid_queue_mongoid.rb`:
49
+
50
+ ```ruby
51
+ # frozen_string_literal: true
52
+
53
+ SolidQueueMongoid.configure do |config|
54
+ # Optional: Specify which Mongoid client to use for SolidQueue collections
55
+ # Default is :default
56
+ config.client = :default # or :secondary, :solid_queue, etc.
57
+
58
+ # Optional: Set a collection prefix to avoid conflicts with existing collections
59
+ # Default is "solid_queue_"
60
+ config.collection_prefix = "solid_queue_"
61
+ end
62
+ ```
63
+
64
+ #### Using a Separate MongoDB Client
65
+
66
+ If you want to store SolidQueue data in a separate MongoDB database, configure a separate client in `config/mongoid.yml`:
67
+
68
+ ```yaml
69
+ development:
70
+ clients:
71
+ default:
72
+ database: my_app_development
73
+ hosts:
74
+ - localhost:27017
75
+ solid_queue:
76
+ database: my_app_jobs
77
+ hosts:
78
+ - localhost:27017
79
+ ```
80
+
81
+ Then configure SolidQueueMongoid to use it:
82
+
83
+ ```ruby
84
+ SolidQueueMongoid.configure do |config|
85
+ config.client = :solid_queue
86
+ config.collection_prefix = "solid_queue_"
87
+ end
88
+ ```
89
+
90
+ ### 3. Create Indexes
91
+
92
+ After configuration, create the necessary MongoDB indexes:
93
+
94
+ ```bash
95
+ # Using rake task (recommended)
96
+ bundle exec rake solid_queue_mongoid:create_indexes
97
+
98
+ # Or in Ruby/Rails console
99
+ SolidQueueMongoid.create_indexes
100
+ ```
101
+
102
+ ### How Client Configuration Works
103
+
104
+ All SolidQueue models automatically use the configured Mongoid client for all queries and operations. The gem overrides Mongoid's query methods to ensure:
105
+
106
+ - All queries (`where`, `find`, `create`, etc.) use the configured client
107
+ - Cross-model associations work correctly within the same client
108
+ - Index creation happens on the correct database
109
+ - No manual `with(client:)` calls are needed in your code
110
+
111
+ This means you can safely use multiple MongoDB databases without any special handling - just configure the client and everything works automatically.
112
+
113
+ ### Collection Naming
114
+
115
+ With the default `collection_prefix` of `"solid_queue_"`, your collections will be named:
116
+
117
+ - `solid_queue_jobs`
118
+ - `solid_queue_ready_executions`
119
+ - `solid_queue_claimed_executions`
120
+ - `solid_queue_blocked_executions`
121
+ - `solid_queue_scheduled_executions`
122
+ - `solid_queue_failed_executions`
123
+ - `solid_queue_recurring_executions`
124
+ - `solid_queue_processes`
125
+ - `solid_queue_pauses`
126
+ - `solid_queue_semaphores`
127
+ - `solid_queue_recurring_tasks`
128
+
129
+ This prefix ensures that SolidQueue collections won't conflict with any existing collections in your database.
130
+
131
+ To see all collection names:
132
+
133
+ ```bash
134
+ bundle exec rake solid_queue_mongoid:show_collections
135
+ ```
136
+
137
+ ## Usage
138
+
139
+ ### 4. Configure the ActiveJob Adapter
140
+
141
+ In `config/application.rb` (or the appropriate environment file):
142
+
143
+ ```ruby
144
+ config.active_job.queue_adapter = :solid_queue
145
+ ```
146
+
147
+ ### Enqueuing Jobs
148
+
149
+ Once configured, use SolidQueue exactly as you would with ActiveRecord:
150
+
151
+ ```ruby
152
+ # In your ActiveJob
153
+ class MyJob < ApplicationJob
154
+ queue_as :default
155
+
156
+ def perform(*args)
157
+ # Your job logic
158
+ end
159
+ end
160
+
161
+ # Enqueue jobs
162
+ MyJob.perform_later(arg1, arg2)
163
+
164
+ # Schedule jobs
165
+ MyJob.set(wait: 1.hour).perform_later(arg1, arg2)
166
+ MyJob.set(wait_until: Date.tomorrow.noon).perform_later(arg1, arg2)
167
+ ```
168
+
169
+ ### Configuration in Rails
170
+
171
+ Configure SolidQueue in `config/queue.yml` or through `config.solid_queue` in your Rails configuration:
172
+
173
+ ```yaml
174
+ # config/queue.yml
175
+ production:
176
+ dispatchers:
177
+ - polling_interval: 1
178
+ batch_size: 500
179
+ workers:
180
+ - queues: "*"
181
+ threads: 3
182
+ processes: 2
183
+ polling_interval: 0.1
184
+ ```
185
+
186
+ ## Rake Tasks
187
+
188
+ The gem provides several Rake tasks for managing indexes:
189
+
190
+ ```bash
191
+ # Create all indexes
192
+ bundle exec rake solid_queue_mongoid:create_indexes
193
+
194
+ # Remove all indexes
195
+ bundle exec rake solid_queue_mongoid:remove_indexes
196
+
197
+ # Show collection names and configuration
198
+ bundle exec rake solid_queue_mongoid:show_collections
199
+ ```
200
+
201
+ ## Index Management
202
+
203
+ Unlike ActiveRecord migrations, MongoDB uses indexes that can be created on-demand. The gem provides a convenient way to manage these indexes similar to `db:migrate`:
204
+
205
+ ### Creating Indexes
206
+
207
+ Always run this after:
208
+ - Initial installation
209
+ - Upgrading the gem
210
+ - Changing configuration
211
+
212
+ ```bash
213
+ bundle exec rake solid_queue_mongoid:create_indexes
214
+ ```
215
+
216
+ ### In Production
217
+
218
+ Add index creation to your deployment process:
219
+
220
+ ```ruby
221
+ # In a Rails initializer or deployment script
222
+ if Rails.env.production?
223
+ SolidQueueMongoid.create_indexes
224
+ end
225
+ ```
226
+
227
+ Or use the Rake task in your deployment pipeline:
228
+
229
+ ```bash
230
+ bundle exec rake solid_queue_mongoid:create_indexes
231
+ ```
232
+
233
+ ## Development
234
+
235
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
236
+
237
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
238
+
239
+ ## Contributing
240
+
241
+ Bug reports and pull requests are welcome on GitHub at https://github.com/washu/solid_queue_mongoid. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/washu/solid_queue_mongoid/blob/main/CODE_OF_CONDUCT.md).
242
+
243
+ ## License
244
+
245
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
246
+
247
+ ## Code of Conduct
248
+
249
+ Everyone interacting in the SolidQueueMongoid project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/washu/solid_queue_mongoid/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueue
4
+ class BlockedExecution < Execution
5
+ assumes_attributes_from_job :concurrency_key
6
+
7
+ field :concurrency_key, type: String
8
+ field :expires_at, type: Time
9
+
10
+ before_create :set_expires_at
11
+
12
+ index({ concurrency_key: 1, priority: 1, job_id: 1 })
13
+ index({ expires_at: 1, concurrency_key: 1 })
14
+
15
+ INDEX_HINTS = {
16
+ index_solid_queue_blocked_executions_for_release: { concurrency_key: 1, priority: 1, job_id: 1 },
17
+ index_solid_queue_blocked_executions_for_maintenance: { expires_at: 1, concurrency_key: 1 }
18
+ }.freeze
19
+
20
+ scope :expired, -> { where(:expires_at.lt => Time.current) }
21
+
22
+ class << self
23
+ # Make create! idempotent: if a BlockedExecution for this job already exists
24
+ # (e.g. created by auto-dispatch), return the existing record instead of raising.
25
+ def create!(**attrs, &block)
26
+ super
27
+ rescue Mongo::Error::OperationFailure => e
28
+ raise unless e.message.to_s.include?("E11000") || e.message.to_s.include?("duplicate key")
29
+
30
+ job_id = attrs[:job_id] || attrs[:job]&.id
31
+ where(job_id: job_id).first || raise(e)
32
+ rescue Mongoid::Errors::Validations => e
33
+ return where(job_id: attrs[:job_id] || attrs[:job]&.id).first || raise(e) if uniqueness_only_error?(e.document)
34
+
35
+ raise
36
+ end
37
+
38
+ def unblock(limit)
39
+ SolidQueue.instrument(:release_many_blocked, limit: limit) do |payload|
40
+ expired_keys = expired.order(:concurrency_key).distinct(:concurrency_key).first(limit)
41
+ payload[:size] = release_many(releasable(expired_keys))
42
+ end
43
+ end
44
+
45
+ # Convenience method: release blocked executions for a given concurrency key.
46
+ # +limit+ is the maximum number to unblock (default: all).
47
+ def unblock_all(concurrency_key, limit = nil)
48
+ scope = ordered.where(concurrency_key: concurrency_key)
49
+ scope = scope.limit(limit) if limit
50
+ count = 0
51
+ scope.each do |execution|
52
+ break if limit && count >= limit
53
+
54
+ count += 1 if execution.release
55
+ end
56
+ count
57
+ end
58
+
59
+ def release_many(concurrency_keys)
60
+ Array(concurrency_keys).count { |key| release_one(key) }
61
+ end
62
+
63
+ def release_one(concurrency_key)
64
+ # NOTE: no outer Mongoid.transaction here — #release already wraps its work
65
+ # in a transaction. MongoDB does not support nested sessions, so wrapping
66
+ # again would cause InvalidSessionNesting errors.
67
+ execution = ordered.where(concurrency_key: concurrency_key).limit(1).first
68
+ execution ? execution.release : false
69
+ end
70
+
71
+ private
72
+
73
+ def releasable(concurrency_keys)
74
+ semaphores = Semaphore.where(:key.in => concurrency_keys).pluck(:key, :value, :limit)
75
+ # Build hash of key => [value, limit]
76
+ sem_map = semaphores.each_with_object({}) do |(key, value, limit), h|
77
+ h[key] = { value: value, limit: limit }
78
+ end
79
+
80
+ # Keys without semaphore (never acquired) + keys where value < limit (slot available)
81
+ (concurrency_keys - sem_map.keys) +
82
+ sem_map.select { |_key, s| s[:value] < s[:limit] }.keys
83
+ end
84
+ end
85
+
86
+ def unblock
87
+ release
88
+ end
89
+
90
+ def release
91
+ SolidQueue.instrument(:release_blocked, job_id: job.id, concurrency_key: concurrency_key,
92
+ released: false) do |payload|
93
+ Mongoid.transaction do
94
+ if acquire_concurrency_lock
95
+ promote_to_ready
96
+ destroy!
97
+ payload[:released] = true
98
+ true
99
+ else
100
+ false
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+ private
107
+
108
+ def set_expires_at
109
+ self.expires_at = job.concurrency_duration.from_now
110
+ end
111
+
112
+ def acquire_concurrency_lock
113
+ Semaphore.wait(job)
114
+ end
115
+
116
+ def promote_to_ready
117
+ existing = ReadyExecution.where(job_id: job_id).first
118
+ return existing if existing
119
+
120
+ ReadyExecution.create!(job_id: job_id, queue_name: queue_name, priority: priority)
121
+ rescue Mongoid::Errors::Validations, Mongo::Error::OperationFailure
122
+ ReadyExecution.where(job_id: job_id).first
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueue
4
+ class ClaimedExecution < Execution
5
+ assumes_attributes_from_job # inherits queue_name and priority from job
6
+
7
+ field :process_id, type: BSON::ObjectId
8
+
9
+ belongs_to :process, class_name: "SolidQueue::Process", optional: true
10
+
11
+ # Executions whose process_id references a process that no longer exists.
12
+ scope :orphaned, lambda {
13
+ existing_process_ids = SolidQueue::Process.all.pluck(:id)
14
+ existing_process_ids.empty? ? all : where(:process_id.nin => existing_process_ids)
15
+ }
16
+
17
+ index({ process_id: 1 })
18
+
19
+ Result = Struct.new(:success, :error) do
20
+ def success?
21
+ success
22
+ end
23
+ end
24
+
25
+ class << self
26
+ # Atomically creates ClaimedExecution records for the given job_ids and
27
+ # yields the claimed set to the block (which deletes the ReadyExecutions).
28
+ def claiming(job_ids, process_id, &block)
29
+ job_data = Array(job_ids).map { |job_id| { job_id: job_id, process_id: process_id } }
30
+
31
+ SolidQueue.instrument(:claim, process_id: process_id, job_ids: job_ids) do |payload|
32
+ claimed = job_data.filter_map do |attrs|
33
+ create!(attrs)
34
+ rescue Mongoid::Errors::Validations, Mongo::Error::OperationFailure
35
+ nil
36
+ end
37
+
38
+ block.call(claimed)
39
+
40
+ payload[:size] = claimed.size
41
+ payload[:claimed_job_ids] = claimed.map(&:job_id)
42
+ end
43
+ end
44
+
45
+ def release_all
46
+ SolidQueue.instrument(:release_many_claimed) do |payload|
47
+ executions = all.to_a
48
+ executions.each do |execution|
49
+ execution.release
50
+ rescue Mongoid::Errors::Validations, Mongo::Error::OperationFailure
51
+ # If ReadyExecution already exists, that's fine
52
+ end
53
+ payload[:size] = executions.size
54
+ end
55
+ end
56
+
57
+ def fail_all_with(error)
58
+ executions = includes(:job).to_a
59
+ return if executions.empty?
60
+
61
+ SolidQueue.instrument(:fail_many_claimed) do |payload|
62
+ executions.each do |execution|
63
+ execution.failed_with(error)
64
+ execution.unblock_next_job
65
+ end
66
+ payload[:process_ids] = executions.map(&:process_id).uniq
67
+ payload[:job_ids] = executions.map(&:job_id).uniq
68
+ payload[:size] = executions.size
69
+ end
70
+ end
71
+
72
+ def discard_all_in_batches(*)
73
+ raise Execution::UndiscardableError, "Can't discard jobs in progress"
74
+ end
75
+
76
+ def discard_all_from_jobs(*)
77
+ raise Execution::UndiscardableError, "Can't discard jobs in progress"
78
+ end
79
+ end
80
+
81
+ # Called by Pool thread — executes the job and marks it finished or failed.
82
+ def perform
83
+ result = execute
84
+
85
+ if result.success?
86
+ finished
87
+ else
88
+ failed_with(result.error)
89
+ raise result.error
90
+ end
91
+ ensure
92
+ unblock_next_job
93
+ end
94
+
95
+ # Release this execution back to ready (called by process deregister / prune).
96
+ def release
97
+ SolidQueue.instrument(:release_claimed, job_id: job.id, process_id: process_id) do
98
+ job.dispatch_bypassing_concurrency_limits
99
+ destroy!
100
+ end
101
+ end
102
+
103
+ def discard
104
+ raise Execution::UndiscardableError, "Can't discard a job in progress"
105
+ end
106
+
107
+ def failed_with(error)
108
+ Mongoid.transaction do
109
+ job.failed_with(error)
110
+ destroy!
111
+ end
112
+ end
113
+
114
+ def unblock_next_job
115
+ job.unblock_next_blocked_job
116
+ end
117
+
118
+ private
119
+
120
+ def execute
121
+ ActiveJob::Base.execute(job.arguments.merge("provider_job_id" => job.id.to_s))
122
+ Result.new(true, nil)
123
+ rescue Exception => e # rubocop:disable Lint/RescueException
124
+ Result.new(false, e)
125
+ end
126
+
127
+ def finished
128
+ Mongoid.transaction do
129
+ job.finished!
130
+ destroy!
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file pre-declares all the main model classes without their concerns.
4
+ # Must be loaded BEFORE concern files to avoid superclass mismatch errors.
5
+
6
+ module SolidQueue
7
+ class Execution < Record; end
8
+
9
+ class Job < Record; end
10
+
11
+ class ReadyExecution < Execution; end
12
+ class ClaimedExecution < Execution; end
13
+ class BlockedExecution < Execution; end
14
+ class ScheduledExecution < Execution; end
15
+ class FailedExecution < Execution; end
16
+
17
+ class RecurringExecution < Record; end
18
+
19
+ class Process < Record
20
+ module Executor; end
21
+ module Prunable; end
22
+ end
23
+
24
+ class Pause < Record; end
25
+ # plain Ruby class — not Mongoid-backed
26
+ class Queue; end
27
+ class Semaphore < Record; end
28
+
29
+ class RecurringTask < Record
30
+ module Arguments; end
31
+ end
32
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueue
4
+ class Execution < Record
5
+ module Dispatching
6
+ extend ActiveSupport::Concern
7
+
8
+ class_methods do
9
+ # Called by ScheduledExecution.dispatch_next_batch and FailedExecution.retry_all.
10
+ # Dispatches jobs by id: promotes them to ready/blocked, then removes the
11
+ # source execution records.
12
+ def dispatch_jobs(job_ids)
13
+ jobs = Job.where(:id.in => job_ids)
14
+
15
+ Job.dispatch_all(jobs).map(&:id).then do |dispatched_job_ids|
16
+ where(:job_id.in => dispatched_job_ids).delete_all
17
+ dispatched_job_ids.size
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueue
4
+ class Execution < Record
5
+ module JobAttributes
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ class_attribute :assumable_attributes_from_job, instance_accessor: false,
10
+ default: %i[queue_name priority]
11
+
12
+ field :queue_name, type: String
13
+ field :priority, type: Integer, default: 0
14
+ field :job_id, type: BSON::ObjectId
15
+
16
+ index({ queue_name: 1, priority: 1, created_at: 1 })
17
+
18
+ belongs_to :job, class_name: "SolidQueue::Job", optional: false
19
+
20
+ # NOTE: uniqueness is enforced by the MongoDB unique index on job_id
21
+ # (added per-subclass by assumes_attributes_from_job), not by a Rails
22
+ # validator. Rails-level uniqueness checks are susceptible to stale
23
+ # QueryCache reads and are redundant given the DB constraint.
24
+ end
25
+
26
+ class_methods do
27
+ # Subclasses call this to declare which additional job attributes they mirror.
28
+ # It registers a before_create callback that copies those attributes from the
29
+ # associated job at creation time.
30
+ # Also ensures a unique index on job_id for the calling class's collection.
31
+ def assumes_attributes_from_job(*attribute_names)
32
+ self.assumable_attributes_from_job = (assumable_attributes_from_job + attribute_names).uniq
33
+ before_create :assume_attributes_from_job
34
+ # Add unique job_id index to THIS subclass's collection (not parent's)
35
+ return if index_specifications.any? { |s| s.spec.keys.map(&:to_s) == ["job_id"] && s.options[:unique] }
36
+
37
+ index({ job_id: 1 }, { unique: true })
38
+ end
39
+
40
+ def attributes_from_job(job)
41
+ job.attributes.symbolize_keys.slice(*assumable_attributes_from_job)
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def assume_attributes_from_job
48
+ self.class.assumable_attributes_from_job.each do |attr|
49
+ send("#{attr}=", job.send(attr)) if job.respond_to?(attr)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end