marj 5.0.0 → 6.1.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 +4 -4
- data/README.md +173 -92
- data/lib/marj/mission_control.rb +119 -0
- data/lib/marj/record.rb +2 -2
- data/lib/marj.rb +53 -16
- data/lib/marj_adapter.rb +70 -34
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1fd27db3bb7a04aee0b4e6cf043e35bde5329143194584bcb03b75ef00896f5d
|
4
|
+
data.tar.gz: d89fd27053c6beeb6aeddcaac59fd1a0ddd4b2a481a4dd0f9b96b3cbbbc29bde
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5b1304cba5e2780fa9fd24432624e37a0a1746efefa9ffaf11573875a2333b1ae9fda8f419b94f30f953c59be0896dae6b90e808b089bbac2324cbe787d2ed8d
|
7
|
+
data.tar.gz: c9b6926e5ef0cad9fc33d510eb9102d570e4ace04322cfc62c44367651a42034c7be5f2dbbf88fc851c13fc6acfc005056d342676bcd505f356696b8fa0305c1
|
data/README.md
CHANGED
@@ -2,69 +2,88 @@
|
|
2
2
|
|
3
3
|
A minimal database-backed ActiveJob queueing backend.
|
4
4
|
|
5
|
-
## Purpose
|
6
|
-
|
7
|
-
To provide a database-backed ActiveJob queueing backend with as few features
|
8
|
-
as possible and the minimum backend-specific API required.
|
9
|
-
|
10
5
|
## Quick Links
|
11
6
|
|
12
|
-
API docs: https://gemdocs.org/gems/marj/
|
7
|
+
API docs: https://gemdocs.org/gems/marj/6.1.0/ <br>
|
13
8
|
RubyGems: https://rubygems.org/gems/marj <br>
|
14
9
|
Changelog: https://github.com/nicholasdower/marj/releases <br>
|
15
10
|
Issues: https://github.com/nicholasdower/marj/issues <br>
|
16
11
|
Development: https://github.com/nicholasdower/marj/blob/master/CONTRIBUTING.md
|
17
12
|
|
18
|
-
##
|
13
|
+
## Motivation
|
19
14
|
|
20
|
-
|
15
|
+
There are already several great database-backed ActiveJob queueing backends:
|
16
|
+
- [Delayed::Job](https://github.com/collectiveidea/delayed_job)
|
17
|
+
- [Que](https://github.com/que-rb/que)
|
18
|
+
- [GoodJob](https://github.com/bensheldon/good_job)
|
19
|
+
- [Solid Queue](https://github.com/basecamp/solid_queue)
|
21
20
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
- An method is provided to query enqueued jobs.
|
27
|
-
- An method is provided to discard enqueued jobs.
|
28
|
-
- An `ActiveRecord` class is provided to query the database directly.
|
21
|
+
Any of these which support your RDBMS is likely to work well for you. But you
|
22
|
+
may find them to be more featureful and complex than what you require.
|
23
|
+
|
24
|
+
Marj aims to be a minimal alternative.
|
29
25
|
|
30
|
-
|
26
|
+
## Goal
|
31
27
|
|
32
|
-
-
|
33
|
-
-
|
34
|
-
-
|
35
|
-
-
|
36
|
-
-
|
28
|
+
To be the database-backend ActiveJob queueing backend with:
|
29
|
+
- The simplest setup
|
30
|
+
- The fewest configuration options
|
31
|
+
- The fewest features
|
32
|
+
- The fewest backend-specific APIs
|
33
|
+
- The fewest lines of code
|
37
34
|
|
38
|
-
|
39
|
-
|
35
|
+
## Features
|
36
|
+
|
37
|
+
Marj supports and has been tested with MySQL, PostgreSQL and SQLite.
|
38
|
+
|
39
|
+
It provides the following features:
|
40
|
+
- Enqueued jobs are written to the database.
|
41
|
+
- Enqueued jobs can be queried, executed and discarded.
|
42
|
+
- Executed jobs are re-enqueued or discarded, depending on the result.
|
43
|
+
- Compatibility with [Mission Control Jobs](https://github.com/basecamp/mission_control-jobs).
|
44
|
+
|
45
|
+
## Extensions
|
46
|
+
|
47
|
+
Marj does not provide the following features by default, but each can easily
|
48
|
+
be added to your application with a few lines of code. See [Example Usage](#example-usage)
|
49
|
+
and [Extension Examples](#extension-examples) for suggestions.
|
50
|
+
- [Automatic job execution](#example-usage)
|
51
|
+
- [Concurrency protection](#concurrency-protection)
|
52
|
+
- [Job timeouts](#job-timeouts)
|
53
|
+
- [Error storage](#error-storage)
|
54
|
+
- [Discarded job retention](#discarded-job-retention)
|
55
|
+
- [Observability](#observability)
|
56
|
+
- [Multi-DB Support](#multi-db-support)
|
40
57
|
|
41
58
|
## API
|
42
59
|
|
43
|
-
|
60
|
+
Marj relies on existing ActiveJob APIs, for example:
|
44
61
|
|
45
62
|
```ruby
|
46
|
-
|
47
|
-
job.
|
48
|
-
job.perform_now # Perform
|
63
|
+
job.enqueue # Enqueues a job
|
64
|
+
job.perform_now # Executes a job
|
49
65
|
```
|
50
66
|
|
51
|
-
|
52
|
-
|
67
|
+
Additionally, it extends the ActiveJob API with methods required for a
|
68
|
+
minimal queueing backend implementaion:
|
53
69
|
|
54
70
|
```ruby
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
job.discard # Discard
|
71
|
+
SomeJob.query(args) # Queries for enqeueued jobs
|
72
|
+
job.discard # Runs discard callbacks and, by default, deletes the job
|
73
|
+
job.delete # Deletes the job
|
59
74
|
```
|
60
75
|
|
76
|
+
## Requirements
|
77
|
+
|
78
|
+
- `activejob >= 7.1`
|
79
|
+
- `activerecord >= 7.1`
|
80
|
+
|
61
81
|
## Setup
|
62
82
|
|
63
83
|
### 1. Install
|
64
84
|
|
65
85
|
```shell
|
66
|
-
bundle add
|
67
|
-
gem install activejob activerecord marj # or globally
|
86
|
+
bundle add marj
|
68
87
|
```
|
69
88
|
|
70
89
|
### 2. Create the database table
|
@@ -85,6 +104,8 @@ class CreateJobs < ActiveRecord::Migration[7.1]
|
|
85
104
|
table.string :timezone, null: false
|
86
105
|
end
|
87
106
|
|
107
|
+
add_index :jobs, %i[job_class]
|
108
|
+
add_index :jobs, %i[queue_name]
|
88
109
|
add_index :jobs, %i[enqueued_at]
|
89
110
|
add_index :jobs, %i[scheduled_at]
|
90
111
|
add_index :jobs, %i[priority scheduled_at enqueued_at]
|
@@ -101,29 +122,26 @@ end
|
|
101
122
|
```ruby
|
102
123
|
require 'marj'
|
103
124
|
|
125
|
+
# Choose one of the following:
|
104
126
|
Rails.configuration.active_job.queue_adapter = :marj # Globally, with Rails
|
105
127
|
ActiveJob::Base.queue_adapter = :marj # Globally, without Rails
|
106
128
|
SomeJob.queue_adapter = :marj # Single job
|
107
129
|
```
|
108
130
|
|
109
|
-
### 4.
|
131
|
+
### 4. Optionally, include the Marj module
|
110
132
|
|
111
|
-
|
112
|
-
|
133
|
+
Without any additional setup, jobs can be queried and discarded via the `Marj`
|
134
|
+
module:
|
113
135
|
|
114
136
|
```ruby
|
115
137
|
Marj.query(:all)
|
116
|
-
ActiveJob::Base.queue_adapter.query(:all)
|
117
138
|
Marj.discard(job)
|
118
|
-
ActiveJob::Base.queue_adapter.discard(job)
|
119
139
|
```
|
120
140
|
|
121
141
|
But it is also convenient to query or discard via job classes:
|
122
142
|
|
123
143
|
```ruby
|
124
|
-
ApplicationJob.query(:all)
|
125
144
|
SomeJob.query(:all)
|
126
|
-
ApplicationJob.discard(job)
|
127
145
|
SomeJob.discard(job)
|
128
146
|
job.discard
|
129
147
|
```
|
@@ -131,11 +149,9 @@ job.discard
|
|
131
149
|
In order to enable this functionality, you must include the `Marj` module:
|
132
150
|
|
133
151
|
```ruby
|
134
|
-
class
|
152
|
+
class SomeJob < ActiveJob::Base
|
135
153
|
include Marj
|
136
|
-
end
|
137
154
|
|
138
|
-
class SomeJob < ApplicationJob
|
139
155
|
def perform; end
|
140
156
|
end
|
141
157
|
```
|
@@ -143,25 +159,27 @@ end
|
|
143
159
|
## Example Usage
|
144
160
|
|
145
161
|
```ruby
|
146
|
-
# Enqueue
|
162
|
+
# Enqueue a job
|
147
163
|
job = SomeJob.perform_later('foo')
|
148
|
-
job.perform_now
|
149
164
|
|
150
|
-
#
|
151
|
-
Marj.query(:
|
165
|
+
# Query jobs
|
166
|
+
Marj.query(:all)
|
167
|
+
Marj.query(:due)
|
168
|
+
Marj.query(:due, queue: :foo)
|
169
|
+
Marj.query('8720417d-8fff-4fcf-bc16-22aaef8543d2')
|
170
|
+
|
171
|
+
# Execute a job
|
172
|
+
job.perform_now
|
152
173
|
|
153
|
-
#
|
174
|
+
# Execute jobs which are due (single query)
|
154
175
|
Marj.query(:due).map(&:perform_now)
|
155
176
|
|
156
|
-
#
|
177
|
+
# Execute jobs which are due (multiple queries)
|
157
178
|
loop do
|
158
|
-
|
179
|
+
Marj.query(:due, :first)&.tap(&:perform_now) || break
|
159
180
|
end
|
160
181
|
|
161
|
-
#
|
162
|
-
Marj.query(:due, queue: :foo).map(&:perform_now)
|
163
|
-
|
164
|
-
# Run jobs as they become due
|
182
|
+
# Execute jobs as they become due
|
165
183
|
loop do
|
166
184
|
Marj.query(:due).each(&:perform_now) rescue logger.error($!)
|
167
185
|
ensure
|
@@ -175,17 +193,54 @@ By default, jobs enqeued during tests will be written to the database. Enqueued
|
|
175
193
|
jobs can be executed via:
|
176
194
|
|
177
195
|
```ruby
|
178
|
-
|
196
|
+
SomeJob.query(:due).map(&:perform_now)
|
179
197
|
```
|
180
198
|
|
181
|
-
Alternatively, to use [
|
199
|
+
Alternatively, to use ActiveJob's [TestAdapter](https://api.rubyonrails.org/classes/ActiveJob/QueueAdapters/TestAdapter.html):
|
182
200
|
```ruby
|
183
201
|
ActiveJob::Base.queue_adapter = :test
|
184
202
|
```
|
185
203
|
|
186
204
|
## Extension Examples
|
187
205
|
|
188
|
-
|
206
|
+
Most missing features can easily be added to your application with a few lines
|
207
|
+
of code.
|
208
|
+
|
209
|
+
### Concurrency Protection
|
210
|
+
|
211
|
+
To prevent two threads from executing the same job simultaneously, consider
|
212
|
+
moving jobs to a different queue before executing them.
|
213
|
+
|
214
|
+
```ruby
|
215
|
+
class ApplicationJob < ActiveJob::Base
|
216
|
+
around_perform do |job, block|
|
217
|
+
if job.queue_name.start_with?('claimed')
|
218
|
+
raise "Job #{job.job_id} already claimed"
|
219
|
+
end
|
220
|
+
|
221
|
+
updated = Marj::Record
|
222
|
+
.where(job_id: job.job_id, queue_name: job.queue_name)
|
223
|
+
.update_all(
|
224
|
+
queue_name: "claimed-#{job.queue_name}",
|
225
|
+
scheduled_at: Time.now.utc
|
226
|
+
)
|
227
|
+
unless updated == 1
|
228
|
+
raise "Failed to claim job #{job.job_id}. #{updated} records updated"
|
229
|
+
end
|
230
|
+
|
231
|
+
begin
|
232
|
+
block.call
|
233
|
+
rescue StandardError
|
234
|
+
Marj::Record
|
235
|
+
.where(job_id: job.job_id, queue_name: "claimed-#{job.queue_name}")
|
236
|
+
.update_all(queue_name: job.queue_name, scheduled_at: job.scheduled_at)
|
237
|
+
raise
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
```
|
242
|
+
|
243
|
+
### Job Timeouts
|
189
244
|
|
190
245
|
```ruby
|
191
246
|
class ApplicationJob < ActiveJob::Base
|
@@ -205,7 +260,7 @@ class ApplicationJob < ActiveJob::Base
|
|
205
260
|
end
|
206
261
|
```
|
207
262
|
|
208
|
-
###
|
263
|
+
### Error Storage
|
209
264
|
|
210
265
|
```ruby
|
211
266
|
class AddLastErrorToJobs < ActiveRecord::Migration[7.1]
|
@@ -246,51 +301,77 @@ class ApplicationJob < ActiveJob::Base
|
|
246
301
|
end
|
247
302
|
```
|
248
303
|
|
249
|
-
###
|
304
|
+
### Discarded Job Retention
|
250
305
|
|
251
|
-
|
252
|
-
|
306
|
+
By default, discarded jobs are deleted from the database. If desired, this
|
307
|
+
behavior can be overridden, for instance to move jobs to a differnt queue:
|
253
308
|
|
254
309
|
```ruby
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
table.string :locale, null: false
|
267
|
-
table.string :timezone, null: false
|
268
|
-
end
|
310
|
+
ActiveJob::Base.queue_adapter = MarjAdapter.new(
|
311
|
+
discard: proc { _1.enqueue(queue: 'discarded') }
|
312
|
+
)
|
313
|
+
|
314
|
+
# To retrieve a discarded job, query the discarded queue:
|
315
|
+
job = Marj.query(:first, queue_name: 'discarded')
|
316
|
+
|
317
|
+
# To delete, use one of the following:
|
318
|
+
Marj.delete(job)
|
319
|
+
job.delete
|
320
|
+
```
|
269
321
|
|
270
|
-
|
271
|
-
|
272
|
-
|
322
|
+
### Observability
|
323
|
+
|
324
|
+
For instance with Prometheus metrics:
|
325
|
+
|
326
|
+
```ruby
|
327
|
+
class ApplicationJob < ActiveJob::Base
|
328
|
+
counter = Prometheus::Client::Counter.new(
|
329
|
+
:job_events_total, docstring: '...', labels: [:job, :event]
|
330
|
+
)
|
331
|
+
|
332
|
+
around_enqueue do |job, block|
|
333
|
+
counter.increment(labels: { job: job.class.name, event: 'before_enqueue' })
|
334
|
+
block.call
|
335
|
+
counter.increment(labels: { job: job.class.name, event: 'after_enqueue' })
|
336
|
+
rescue Exception
|
337
|
+
counter.increment(labels: { job: job.class.name, event: 'enqueue_error' })
|
338
|
+
raise
|
273
339
|
end
|
274
340
|
|
275
|
-
|
276
|
-
|
341
|
+
around_perform do |job, block|
|
342
|
+
counter.increment(labels: { job: job.class.name, event: 'before_perform' })
|
343
|
+
block.call
|
344
|
+
counter.increment(labels: { job: job.class.name, event: 'after_perform' })
|
345
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
346
|
+
counter.increment(labels: { job: job.class.name, event: 'perform_error' })
|
347
|
+
raise
|
348
|
+
end
|
349
|
+
|
350
|
+
after_discard do |job|
|
351
|
+
counter.increment(labels: { job: job.class.name, event: 'after_discard' })
|
277
352
|
end
|
278
353
|
end
|
354
|
+
```
|
355
|
+
|
356
|
+
### Multi-DB Support
|
357
|
+
|
358
|
+
It is possible to create a custom record class in order to, for instance,
|
359
|
+
use a different class or table name, or write jobs to multiple
|
360
|
+
databases/tables within a single application.
|
361
|
+
|
362
|
+
Assuming you have a jobs tabled named `my_jobs`:
|
279
363
|
|
364
|
+
```ruby
|
280
365
|
class MyRecord < Marj::Record
|
281
366
|
self.table_name = 'my_jobs'
|
282
367
|
end
|
283
368
|
|
284
|
-
CreateMyJobs.migrate(:up)
|
285
|
-
|
286
369
|
class MyJob < ActiveJob::Base
|
287
|
-
self.queue_adapter = MarjAdapter.new('MyRecord')
|
288
|
-
|
289
370
|
include Marj
|
290
371
|
|
291
|
-
|
292
|
-
|
293
|
-
end
|
372
|
+
self.queue_adapter = MarjAdapter.new('MyRecord')
|
373
|
+
|
374
|
+
def perform; end
|
294
375
|
end
|
295
376
|
|
296
377
|
MyJob.perform_later('oh, hi')
|
@@ -378,7 +459,7 @@ job = SomeJob.new.deserialize(other_job.serialize)
|
|
378
459
|
job = SomeJob.perform_later
|
379
460
|
job = SomeJob.perform_later(args)
|
380
461
|
|
381
|
-
# Create without enqueueing and run (only enqueued on failure
|
462
|
+
# Create without enqueueing and run (only enqueued on retryable failure)
|
382
463
|
SomeJob.perform_now
|
383
464
|
SomeJob.perform_now(args)
|
384
465
|
```
|
@@ -399,7 +480,7 @@ SomeJob.perform_later(SomeJob.new(args))
|
|
399
480
|
SomeJob.perform_later(args)
|
400
481
|
SomeJob.set(options).perform_later(args)
|
401
482
|
|
402
|
-
# After a
|
483
|
+
# After a retryable execution failure
|
403
484
|
SomeJob.perform_now(args)
|
404
485
|
ActiveJob::Base.execute(SomeJob.new(args).serialize)
|
405
486
|
|
@@ -413,7 +494,7 @@ SomeJob.set(options).perform_all_later(SomeJob.new, SomeJob.new, options:)
|
|
413
494
|
### Executing Jobs
|
414
495
|
|
415
496
|
```ruby
|
416
|
-
# Executed without enqueueing, enqueued on failure
|
497
|
+
# Executed without enqueueing, enqueued on retryable failure
|
417
498
|
SomeJob.new(args).perform_now
|
418
499
|
SomeJob.perform_now(args)
|
419
500
|
ActiveJob::Base.execute(SomeJob.new(args).serialize)
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../marj_adapter'
|
4
|
+
require 'mission_control/jobs'
|
5
|
+
require 'mission_control/jobs/adapter'
|
6
|
+
|
7
|
+
# :nocov:
|
8
|
+
module Marj
|
9
|
+
module MissionControl
|
10
|
+
include ::MissionControl::Jobs::Adapter
|
11
|
+
|
12
|
+
def queues
|
13
|
+
record_class.group(:queue_name).count(:queue_name).map { |k, v| { name: k, size: v, active: true } }
|
14
|
+
end
|
15
|
+
|
16
|
+
def queue_size(queue_name)
|
17
|
+
record_class.where(queue_name: queue_name).count
|
18
|
+
end
|
19
|
+
|
20
|
+
def clear_queue(queue_name)
|
21
|
+
Marj::Record.where(queue_name: queue_name).delete_all
|
22
|
+
end
|
23
|
+
|
24
|
+
def pause_queue(_queue_name)
|
25
|
+
raise 'not supported: pause queue'
|
26
|
+
end
|
27
|
+
|
28
|
+
def resume_queue(_queue_name)
|
29
|
+
raise 'not supported: resume queue'
|
30
|
+
end
|
31
|
+
|
32
|
+
def queue_paused?(_queue_name)
|
33
|
+
false
|
34
|
+
end
|
35
|
+
|
36
|
+
def supported_statuses
|
37
|
+
%i[pending failed scheduled]
|
38
|
+
end
|
39
|
+
|
40
|
+
def supported_filters(_jobs_relation)
|
41
|
+
%i[queue_name job_class_name]
|
42
|
+
end
|
43
|
+
|
44
|
+
def exposes_workers?
|
45
|
+
false
|
46
|
+
end
|
47
|
+
|
48
|
+
def workers
|
49
|
+
raise 'not supported: workers'
|
50
|
+
end
|
51
|
+
|
52
|
+
def find_worker(_worker_id)
|
53
|
+
raise 'not supported: find workers'
|
54
|
+
end
|
55
|
+
|
56
|
+
def jobs_count(jobs_relation)
|
57
|
+
ar_relation(jobs_relation).count
|
58
|
+
end
|
59
|
+
|
60
|
+
def fetch_jobs(jobs_relation)
|
61
|
+
ar_relation(jobs_relation).each_with_index.map { |record, index| to_job(record, jobs_relation, index) }
|
62
|
+
end
|
63
|
+
|
64
|
+
def retry_all_jobs(jobs_relation)
|
65
|
+
ar_relation(jobs_relation).map { |record| record.to_job.perform_now }
|
66
|
+
end
|
67
|
+
|
68
|
+
def retry_job(job, _jobs_relation)
|
69
|
+
Marj::Record.find(job.job_id).to_job.perform_now
|
70
|
+
end
|
71
|
+
|
72
|
+
def discard_all_jobs(jobs_relation)
|
73
|
+
ar_relation(jobs_relation).map { |record| discard(record.to_job) }
|
74
|
+
end
|
75
|
+
|
76
|
+
def discard_job(job, _jobs_relation)
|
77
|
+
discard(Marj::Record.find(job.job_id).to_job)
|
78
|
+
end
|
79
|
+
|
80
|
+
def find_job(job_id, jobs_relation)
|
81
|
+
to_job(record_class.find_by(job_id: job_id), jobs_relation)
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def ar_relation(jobs_relation)
|
87
|
+
relation = Marj::Record.all.offset(jobs_relation.offset_value).limit(jobs_relation.limit_value)
|
88
|
+
relation = relation.where.not(executions: 0) if jobs_relation.status == :failed
|
89
|
+
relation = relation.where.not(scheduled_at: nil) if jobs_relation.status == :scheduled
|
90
|
+
relation = relation.where(job_class: jobs_relation.job_class_name) if jobs_relation.job_class_name
|
91
|
+
relation = relation.where(queue_name: jobs_relation.queue_name) if jobs_relation.queue_name
|
92
|
+
relation
|
93
|
+
end
|
94
|
+
|
95
|
+
def to_job(record, jobs_relation, index = 0)
|
96
|
+
return nil unless record
|
97
|
+
|
98
|
+
job = record.to_job
|
99
|
+
job_data = job.serialize
|
100
|
+
ActiveJob::JobProxy.new(job_data).tap do |proxy|
|
101
|
+
if job.executions.positive?
|
102
|
+
proxy.last_execution_error = ActiveJob::ExecutionError.new(
|
103
|
+
error_class: Exception, message: 'unknown', backtrace: []
|
104
|
+
)
|
105
|
+
proxy.failed_at = job.enqueued_at
|
106
|
+
proxy.status = :failed
|
107
|
+
elsif job.scheduled_at
|
108
|
+
proxy.status = :scheduled
|
109
|
+
else
|
110
|
+
proxy.status = :pending
|
111
|
+
end
|
112
|
+
proxy.raw_data = job_data
|
113
|
+
proxy.position = jobs_relation.offset_value + index
|
114
|
+
proxy.arguments = job.arguments # For some reason MissionControl sets the arguments to the entire job data
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
# :nocov:
|
data/lib/marj/record.rb
CHANGED
@@ -84,8 +84,8 @@ module Marj
|
|
84
84
|
# - If a job succeeds, after_perform will be called.
|
85
85
|
# - If a job fails and should be retried, enqueue will be called. This is handled by the queue adapter.
|
86
86
|
# - If a job exceeds its max attempts, after_discard will be called.
|
87
|
-
job.singleton_class.after_perform { |_j|
|
88
|
-
job.singleton_class.after_discard { |_j, _exception|
|
87
|
+
job.singleton_class.after_perform { |_j| job.queue_adapter.delete(job) }
|
88
|
+
job.singleton_class.after_discard { |_j, _exception| job.queue_adapter.discard(job, run_callbacks: false) }
|
89
89
|
job.singleton_class.instance_variable_set(:@record, record)
|
90
90
|
|
91
91
|
job
|
data/lib/marj.rb
CHANGED
@@ -6,13 +6,15 @@ require_relative 'marj_adapter'
|
|
6
6
|
#
|
7
7
|
# The {Marj} module provides the following methods:
|
8
8
|
# - +query+ - Queries enqueued jobs
|
9
|
-
# - +discard+ - Discards a job
|
9
|
+
# - +discard+ - Discards a job, by default by executing after_discard callbacks and delegating to delete
|
10
|
+
# - +delete+ - Deletes a job
|
10
11
|
#
|
11
12
|
# It is possible to call the above methods on the {Marj} module itself or on any class which includes it.
|
12
13
|
#
|
13
14
|
# Example usage:
|
14
15
|
# Marj.query(:first) # Returns the first job
|
15
16
|
# Marj.discard(job) # Discards the specified job
|
17
|
+
# Marj.delete(job) # Deletes the specified job
|
16
18
|
#
|
17
19
|
# class ApplicationJob < ActiveJob::Base
|
18
20
|
# include Marj
|
@@ -25,33 +27,37 @@ require_relative 'marj_adapter'
|
|
25
27
|
# job = ApplicationJob.query(:first) # Returns the first enqueued job
|
26
28
|
# job = SomeJob.query(:first) # Returns the first enqueued job with job_class SomeJob
|
27
29
|
# ApplicationJob.discard(job) # Discards the specified job
|
30
|
+
# ApplicationJob.delete(job) # Deletes the specified job
|
28
31
|
# job.discard # Discards the job
|
32
|
+
# job.delete # Deletes the job
|
29
33
|
#
|
30
34
|
# See https://github.com/nicholasdower/marj
|
31
35
|
module Marj
|
32
36
|
# The Marj version.
|
33
|
-
VERSION = '
|
37
|
+
VERSION = '6.1.0'
|
34
38
|
|
35
39
|
Kernel.autoload(:Record, File.expand_path(File.join('marj', 'record.rb'), __dir__))
|
36
40
|
|
37
41
|
# Provides the {query} and {discard} class methods.
|
38
42
|
module ClassMethods
|
39
|
-
# Queries enqueued jobs.
|
43
|
+
# Queries enqueued jobs. Similar to +ActiveRecord.where+ with a few additional features:
|
44
|
+
# - Symbol arguments are treated as +ActiveRecord+ scopes.
|
45
|
+
# - If only a job ID is specified, the corresponding job is returned.
|
46
|
+
# - If +:limit+ is specified, the maximum number of jobs is limited.
|
47
|
+
# - If +:order+ is specified, the jobs are ordered by the given attribute.
|
40
48
|
#
|
41
|
-
#
|
49
|
+
# By default jobs are ordered by when they should be executed.
|
42
50
|
#
|
43
51
|
# Example usage:
|
44
|
-
# query
|
45
|
-
# query(:
|
46
|
-
# query(:
|
47
|
-
# query(
|
48
|
-
#
|
49
|
-
# query(
|
50
|
-
# query(
|
51
|
-
# query(job_id: '123')
|
52
|
-
#
|
53
|
-
# query(queue: 'foo') # Returns all jobs in the 'foo' queue
|
54
|
-
# query(job_queue: 'foo') # Same as above
|
52
|
+
# query # Returns all jobs
|
53
|
+
# query(:all) # Returns all jobs
|
54
|
+
# query(:due) # Returns jobs which are due to be executed
|
55
|
+
# query(:due, limit: 10) # Returns at most 10 jobs which are due to be executed
|
56
|
+
# query(job_class: Foo) # Returns all jobs with job_class Foo
|
57
|
+
# query(:due, job_class: Foo) # Returns jobs which are due to be executed with job_class Foo
|
58
|
+
# query(queue_name: 'foo') # Returns all jobs in the 'foo' queue
|
59
|
+
# query(job_id: '123') # Returns the job with job_id '123' or nil if no such job exists
|
60
|
+
# query('123') # Returns the job with job_id '123' or nil if no such job exists
|
55
61
|
def query(*args, **kwargs)
|
56
62
|
kwargs[:job_class] ||= self if self < ActiveJob::Base && name != 'ApplicationJob'
|
57
63
|
queue_adapter.query(*args, **kwargs)
|
@@ -63,6 +69,13 @@ module Marj
|
|
63
69
|
def discard(job)
|
64
70
|
queue_adapter.discard(job)
|
65
71
|
end
|
72
|
+
|
73
|
+
# Deletes the record associated with the specified job.
|
74
|
+
#
|
75
|
+
# @return [ActiveJob::Base] the deleted job
|
76
|
+
def delete(job)
|
77
|
+
queue_adapter.delete(job)
|
78
|
+
end
|
66
79
|
end
|
67
80
|
|
68
81
|
# (see ClassMethods#query)
|
@@ -75,13 +88,25 @@ module Marj
|
|
75
88
|
queue_adapter.discard(job)
|
76
89
|
end
|
77
90
|
|
78
|
-
#
|
91
|
+
# (see ClassMethods#delete)
|
92
|
+
def self.delete(job)
|
93
|
+
queue_adapter.delete(job)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Deletes this job.
|
79
97
|
#
|
80
98
|
# @return [ActiveJob::Base] this job
|
81
99
|
def discard
|
82
100
|
self.class.queue_adapter.discard(self)
|
83
101
|
end
|
84
102
|
|
103
|
+
# Deletes the record associated with this job.
|
104
|
+
#
|
105
|
+
# @return [ActiveJob::Base] this job
|
106
|
+
def delete
|
107
|
+
self.class.queue_adapter.delete(self)
|
108
|
+
end
|
109
|
+
|
85
110
|
def self.included(clazz)
|
86
111
|
clazz.extend(ClassMethods)
|
87
112
|
end
|
@@ -92,3 +117,15 @@ module Marj
|
|
92
117
|
end
|
93
118
|
private_class_method :queue_adapter
|
94
119
|
end
|
120
|
+
|
121
|
+
# :nocov:
|
122
|
+
if defined?(Rails)
|
123
|
+
begin
|
124
|
+
require 'mission_control/jobs'
|
125
|
+
require_relative 'marj/mission_control'
|
126
|
+
MarjAdapter.include(Marj::MissionControl)
|
127
|
+
rescue LoadError
|
128
|
+
# ignore
|
129
|
+
end
|
130
|
+
end
|
131
|
+
# :nocov:
|
data/lib/marj_adapter.rb
CHANGED
@@ -2,11 +2,13 @@
|
|
2
2
|
|
3
3
|
# ActiveJob queue adapter for Marj.
|
4
4
|
#
|
5
|
-
# In addition to the standard +ActiveJob+ queue adapter API, this adapter provides
|
6
|
-
#
|
5
|
+
# In addition to the standard +ActiveJob+ queue adapter API, this adapter provides:
|
6
|
+
# - A +query+ method which can be used to query enqueued jobs
|
7
|
+
# - A +discard+ method which can be used to discard enqueued jobs.
|
8
|
+
# - A +delete+ method which can be used to delete enqueued jobs.
|
7
9
|
#
|
8
|
-
# Although it is possible to access the adapter directly in order to query or
|
9
|
-
# {Marj} module.
|
10
|
+
# Although it is possible to access the adapter directly in order to query, discard or delete, it is recommended to use
|
11
|
+
# the {Marj} module.
|
10
12
|
#
|
11
13
|
# See https://github.com/nicholasdower/marj
|
12
14
|
class MarjAdapter
|
@@ -15,9 +17,17 @@ class MarjAdapter
|
|
15
17
|
|
16
18
|
# Creates a new adapter which will enqueue jobs using the given +ActiveRecord+ class.
|
17
19
|
#
|
18
|
-
# @param record_class [Class, String] the +ActiveRecord+ class (or its name) to use to
|
19
|
-
|
20
|
+
# @param record_class [Class, String] the +ActiveRecord+ class (or its name) to use, defaults to +Marj::Record+
|
21
|
+
# @param discard [Proc] the proc to use to discard jobs, defaults to delegating to {delete}
|
22
|
+
def initialize(record_class: 'Marj::Record', discard: proc { |job| delete(job) })
|
20
23
|
@record_class = record_class
|
24
|
+
@discard_proc = discard
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns whether jobs enqueued during a transaction should actually be enqueued only after the
|
28
|
+
# transaction is committed.
|
29
|
+
def enqueue_after_transaction_commit?
|
30
|
+
false
|
21
31
|
end
|
22
32
|
|
23
33
|
# Enqueue a job for immediate execution.
|
@@ -77,25 +87,25 @@ class MarjAdapter
|
|
77
87
|
end
|
78
88
|
|
79
89
|
# Queries enqueued jobs. Similar to +ActiveRecord.where+ with a few additional features:
|
80
|
-
# -
|
90
|
+
# - Symbol arguments are treated as +ActiveRecord+ scopes.
|
81
91
|
# - If only a job ID is specified, the corresponding job is returned.
|
82
92
|
# - If +:limit+ is specified, the maximum number of jobs is limited.
|
93
|
+
# - If +:order+ is specified, the jobs are ordered by the given attribute.
|
83
94
|
#
|
84
|
-
#
|
85
|
-
# query(:all) # Delegates to Marj::Record.all
|
86
|
-
# query(:due) # Delegates to Marj::Record.due
|
87
|
-
# query(:all, limit: 10) # Returns a maximum of 10 jobs
|
88
|
-
# query(job_class: Foo) # Returns all jobs with job_class Foo
|
89
|
-
#
|
90
|
-
# query('123') # Returns the job with id '123' or nil if no such job exists
|
91
|
-
# query(id: '123') # Same as above
|
92
|
-
# query(job_id: '123') # Same as above
|
95
|
+
# By default jobs are ordered by when they should be executed.
|
93
96
|
#
|
94
|
-
#
|
95
|
-
# query
|
97
|
+
# Example usage:
|
98
|
+
# query # Returns all jobs
|
99
|
+
# query(:all) # Returns all jobs
|
100
|
+
# query(:due) # Returns jobs which are due to be executed
|
101
|
+
# query(:due, limit: 10) # Returns at most 10 jobs which are due to be executed
|
102
|
+
# query(job_class: Foo) # Returns all jobs with job_class Foo
|
103
|
+
# query(:due, job_class: Foo) # Returns jobs which are due to be executed with job_class Foo
|
104
|
+
# query(queue_name: 'foo') # Returns all jobs in the 'foo' queue
|
105
|
+
# query(job_id: '123') # Returns the job with job_id '123' or nil if no such job exists
|
106
|
+
# query('123') # Returns the job with job_id '123' or nil if no such job exists
|
96
107
|
def query(*args, **kwargs)
|
97
108
|
args, kwargs = args.dup, kwargs.dup.symbolize_keys
|
98
|
-
kwargs = kwargs.merge(queue_name: kwargs.delete(:queue)) if kwargs.key?(:queue)
|
99
109
|
kwargs = kwargs.merge(job_id: kwargs.delete(:id)) if kwargs.key?(:id)
|
100
110
|
kwargs[:job_id] = args.shift if args.size == 1 && args.first.is_a?(String) && args.first.match(JOB_ID_REGEX)
|
101
111
|
|
@@ -103,18 +113,16 @@ class MarjAdapter
|
|
103
113
|
return record_class.find_by(job_id: kwargs[:job_id])&.to_job
|
104
114
|
end
|
105
115
|
|
106
|
-
symbol_args =
|
107
|
-
symbol_args
|
108
|
-
order_by = kwargs.delete(:order)
|
109
|
-
order_by = :queue_name if [:queue, 'queue'].include?(order_by)
|
116
|
+
symbol_args, args = args.partition { _1.is_a?(Symbol) }
|
117
|
+
symbol_args.delete(:all)
|
110
118
|
limit = kwargs.delete(:limit)
|
111
|
-
symbol_args.shift if symbol_args.first == :all
|
112
119
|
relation = record_class.all
|
113
|
-
relation = relation.order(
|
114
|
-
relation = relation.by_due_date unless relation.order_values.any?
|
120
|
+
relation = relation.order(kwargs.delete(:order)) if kwargs.key?(:order)
|
115
121
|
relation = relation.where(*args, **kwargs) if args.any? || kwargs.any?
|
116
122
|
relation = relation.limit(limit) if limit
|
117
123
|
relation = relation.send(symbol_args.shift) while symbol_args.any?
|
124
|
+
relation = relation.by_due_date if relation.is_a?(ActiveRecord::Relation) && relation.order_values.empty?
|
125
|
+
|
118
126
|
if relation.is_a?(Enumerable)
|
119
127
|
relation.map(&:to_job)
|
120
128
|
elsif relation.is_a?(record_class)
|
@@ -126,12 +134,46 @@ class MarjAdapter
|
|
126
134
|
|
127
135
|
# Discards the specified job.
|
128
136
|
#
|
137
|
+
# @param job [ActiveJob::Base] the job being discarded
|
138
|
+
# @param run_callbacks [Boolean] whether to run the +after_discard+ callbacks
|
129
139
|
# @return [ActiveJob::Base] the discarded job
|
130
|
-
def discard(job)
|
140
|
+
def discard(job, run_callbacks: true)
|
141
|
+
job.tap do
|
142
|
+
@discard_proc.call(job)
|
143
|
+
run_after_discard_callbacks(job) if run_callbacks
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Deletes the record associated with the specified job.
|
148
|
+
#
|
149
|
+
# @return [ActiveJob::Base] the deleted job
|
150
|
+
def delete(job)
|
151
|
+
job.tap { destroy_record(job) }
|
152
|
+
end
|
153
|
+
|
154
|
+
private
|
155
|
+
|
156
|
+
# Returns the +ActiveRecord+ class to use to store jobs.
|
157
|
+
#
|
158
|
+
# @return [Class] the +ActiveRecord+ class
|
159
|
+
def record_class
|
160
|
+
@record_class = @record_class.is_a?(String) ? @record_class.constantize : @record_class
|
161
|
+
end
|
162
|
+
|
163
|
+
# Destroys the record associated with the given job if it exists.
|
164
|
+
#
|
165
|
+
# @return [ActiveRecord::Base, NilClass] the destroyed record or +nil+ if no such record exists
|
166
|
+
def destroy_record(job)
|
131
167
|
record = job.singleton_class.instance_variable_get(:@record)
|
132
168
|
record ||= Marj::Record.find_by(job_id: job.job_id)&.tap { _1.send(:register_callbacks, job) }
|
133
169
|
record&.destroy
|
170
|
+
end
|
134
171
|
|
172
|
+
# Invokes the specified job's +after_discard+ callbacks.
|
173
|
+
#
|
174
|
+
# @param job [ActiveJob::Base] the job being discarded
|
175
|
+
# @return [NilClass] the given job
|
176
|
+
def run_after_discard_callbacks(job)
|
135
177
|
# Copied from ActiveJob::Exceptions#run_after_discard_procs
|
136
178
|
exceptions = []
|
137
179
|
job.after_discard_procs.each do |blk|
|
@@ -141,12 +183,6 @@ class MarjAdapter
|
|
141
183
|
end
|
142
184
|
raise exceptions.last if exceptions.any?
|
143
185
|
|
144
|
-
|
145
|
-
end
|
146
|
-
|
147
|
-
private
|
148
|
-
|
149
|
-
def record_class
|
150
|
-
@record_class = @record_class.is_a?(String) ? @record_class.constantize : @record_class
|
186
|
+
nil
|
151
187
|
end
|
152
188
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: marj
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 6.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nick Dower
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-08-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activejob
|
@@ -48,6 +48,7 @@ files:
|
|
48
48
|
- LICENSE.txt
|
49
49
|
- README.md
|
50
50
|
- lib/marj.rb
|
51
|
+
- lib/marj/mission_control.rb
|
51
52
|
- lib/marj/record.rb
|
52
53
|
- lib/marj_adapter.rb
|
53
54
|
homepage: https://github.com/nicholasdower/marj
|
@@ -55,8 +56,8 @@ licenses:
|
|
55
56
|
- MIT
|
56
57
|
metadata:
|
57
58
|
bug_tracker_uri: https://github.com/nicholasdower/marj/issues
|
58
|
-
changelog_uri: https://github.com/nicholasdower/marj/releases/tag/
|
59
|
-
documentation_uri: https://www.rubydoc.info/github/nicholasdower/marj/
|
59
|
+
changelog_uri: https://github.com/nicholasdower/marj/releases/tag/v6.1.0
|
60
|
+
documentation_uri: https://www.rubydoc.info/github/nicholasdower/marj/v6.1.0
|
60
61
|
homepage_uri: https://github.com/nicholasdower/marj
|
61
62
|
rubygems_mfa_required: 'true'
|
62
63
|
source_code_uri: https://github.com/nicholasdower/marj
|