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.
- checksums.yaml +7 -0
- data/.claude/settings.local.json +38 -0
- data/.idea/copilot.data.migration.ask2agent.xml +6 -0
- data/.idea/inspectionProfiles/Project_Default.xml +5 -0
- data/.idea/jsLibraryMappings.xml +6 -0
- data/.idea/misc.xml +17 -0
- data/.idea/modules/bigdecimal-4.0.iml +18 -0
- data/.idea/modules/builder-3.3.iml +18 -0
- data/.idea/modules/concurrent-ruby-1.3.iml +21 -0
- data/.idea/modules/connection_pool-3.0.iml +18 -0
- data/.idea/modules/crass-1.0.iml +19 -0
- data/.idea/modules/docile-1.4.iml +20 -0
- data/.idea/modules/drb-2.2.iml +18 -0
- data/.idea/modules/erb-6.0.iml +23 -0
- data/.idea/modules/et-orbi-1.4.iml +20 -0
- data/.idea/modules/fugit-1.12.iml +18 -0
- data/.idea/modules/irb-1.17.iml +26 -0
- data/.idea/modules/json-2.18.iml +18 -0
- data/.idea/modules/lint_roller-1.1.iml +18 -0
- data/.idea/modules/mongo-2.23.iml +19 -0
- data/.idea/modules/nokogiri-1.19.iml +19 -0
- data/.idea/modules/parser-3.3.10.iml +19 -0
- data/.idea/modules/pp-0.6.iml +18 -0
- data/.idea/modules/prettyprint-0.2.iml +22 -0
- data/.idea/modules/prism-1.9.iml +20 -0
- data/.idea/modules/raabro-1.4.iml +18 -0
- data/.idea/modules/rake-13.3.iml +22 -0
- data/.idea/modules/rdoc-7.2.iml +22 -0
- data/.idea/modules/regexp_parser-2.11.iml +20 -0
- data/.idea/modules/specifications.iml +18 -0
- data/.idea/modules/thor-1.5.iml +20 -0
- data/.idea/modules/timeout-0.6.iml +22 -0
- data/.idea/modules/tsort-0.2.iml +22 -0
- data/.idea/modules/unicode-emoji-4.2.iml +19 -0
- data/.idea/modules.xml +36 -0
- data/.idea/solid_queue_mongoid.iml +3297 -0
- data/.idea/vcs.xml +6 -0
- data/.idea/workspace.xml +353 -0
- data/.rspec +3 -0
- data/.rubocop.yml +47 -0
- data/ARCHITECTURE.md +91 -0
- data/CHANGELOG.md +27 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +249 -0
- data/Rakefile +12 -0
- data/lib/solid_queue_mongoid/models/blocked_execution.rb +125 -0
- data/lib/solid_queue_mongoid/models/claimed_execution.rb +134 -0
- data/lib/solid_queue_mongoid/models/classes.rb +32 -0
- data/lib/solid_queue_mongoid/models/execution/dispatching.rb +23 -0
- data/lib/solid_queue_mongoid/models/execution/job_attributes.rb +54 -0
- data/lib/solid_queue_mongoid/models/execution.rb +65 -0
- data/lib/solid_queue_mongoid/models/failed_execution.rb +74 -0
- data/lib/solid_queue_mongoid/models/job/clearable.rb +28 -0
- data/lib/solid_queue_mongoid/models/job/concurrency_controls.rb +93 -0
- data/lib/solid_queue_mongoid/models/job/executable.rb +142 -0
- data/lib/solid_queue_mongoid/models/job/recurrable.rb +14 -0
- data/lib/solid_queue_mongoid/models/job/retryable.rb +51 -0
- data/lib/solid_queue_mongoid/models/job/schedulable.rb +55 -0
- data/lib/solid_queue_mongoid/models/job.rb +103 -0
- data/lib/solid_queue_mongoid/models/pause.rb +25 -0
- data/lib/solid_queue_mongoid/models/process/executor.rb +30 -0
- data/lib/solid_queue_mongoid/models/process/prunable.rb +49 -0
- data/lib/solid_queue_mongoid/models/process.rb +73 -0
- data/lib/solid_queue_mongoid/models/queue.rb +65 -0
- data/lib/solid_queue_mongoid/models/queue_selector.rb +101 -0
- data/lib/solid_queue_mongoid/models/ready_execution.rb +70 -0
- data/lib/solid_queue_mongoid/models/record.rb +147 -0
- data/lib/solid_queue_mongoid/models/recurring_execution.rb +62 -0
- data/lib/solid_queue_mongoid/models/recurring_task/arguments.rb +29 -0
- data/lib/solid_queue_mongoid/models/recurring_task.rb +194 -0
- data/lib/solid_queue_mongoid/models/scheduled_execution.rb +43 -0
- data/lib/solid_queue_mongoid/models/semaphore.rb +179 -0
- data/lib/solid_queue_mongoid/railtie.rb +29 -0
- data/lib/solid_queue_mongoid/version.rb +5 -0
- data/lib/solid_queue_mongoid.rb +136 -0
- data/lib/tasks/solid_queue_mongoid.rake +51 -0
- data/release.sh +13 -0
- data/sig/solid_queue_mongoid.rbs +4 -0
- 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
|
+
[](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,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
|