marj 3.0.0 → 4.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +93 -86
- data/lib/marj/jobs_interface.rb +42 -15
- data/lib/marj/record.rb +72 -7
- data/lib/marj/relation.rb +8 -48
- data/lib/marj.rb +57 -7
- data/lib/marj_adapter.rb +2 -2
- metadata +4 -6
- data/lib/marj/jobs.rb +0 -29
- data/lib/marj/record_interface.rb +0 -94
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4eb6129fa4948ed6e108469f44f380cda908620c6d08392cfa1e8c3bc84a9875
|
4
|
+
data.tar.gz: 28faed9a10fa3a8c885b521d6c3465196f814e97af07018d5eea88e580f322a9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5f984f366a9cefb3187cbac7129144c8241d46d00357d719ec442dfce62d5c833c95ec30363a5af867e111499cc91cd2c60768da55d04c9c7fc401cce8ac3f83
|
7
|
+
data.tar.gz: e86029649f11ca05f96efcae59c1c60212b29335d9b154c3288a15aa81cd432b46f960166680b5bb0a534346852ccc73970cbaf8a298eb52893c37e260854f61
|
data/README.md
CHANGED
@@ -4,7 +4,7 @@ A minimal database-backed ActiveJob queueing backend.
|
|
4
4
|
|
5
5
|
## Quick Links
|
6
6
|
|
7
|
-
API docs: https://
|
7
|
+
API docs: https://gemdocs.org/gems/marj/latest <br>
|
8
8
|
RubyGems: https://rubygems.org/gems/marj <br>
|
9
9
|
Changelog: https://github.com/nicholasdower/marj/releases <br>
|
10
10
|
Issues: https://github.com/nicholasdower/marj/issues <br>
|
@@ -17,7 +17,7 @@ Development: https://github.com/nicholasdower/marj/blob/master/CONTRIBUTING.md
|
|
17
17
|
- Failed jobs which should be retried are updated in the database.
|
18
18
|
- Failed jobs which should not be retried are deleted from the database.
|
19
19
|
- An interface is provided to retrieve, execute, discard and re-enqueue jobs.
|
20
|
-
- An `ActiveRecord`
|
20
|
+
- An `ActiveRecord` class is provided to query the database directly.
|
21
21
|
|
22
22
|
## Features Not Provided
|
23
23
|
|
@@ -39,7 +39,7 @@ bundle add activejob activerecord marj
|
|
39
39
|
gem install activejob activerecord marj
|
40
40
|
```
|
41
41
|
|
42
|
-
### 3. Create the
|
42
|
+
### 3. Create the database table
|
43
43
|
|
44
44
|
```ruby
|
45
45
|
class CreateJobs < ActiveRecord::Migration[7.1]
|
@@ -68,6 +68,9 @@ class CreateJobs < ActiveRecord::Migration[7.1]
|
|
68
68
|
end
|
69
69
|
```
|
70
70
|
|
71
|
+
Note that by default, Marj uses a table named `jobs`. To override the default
|
72
|
+
table name, set `Marj.table_name` before loading `ActiveRecord`.
|
73
|
+
|
71
74
|
### 4. Configure the queue adapter
|
72
75
|
|
73
76
|
```ruby
|
@@ -78,34 +81,64 @@ ActiveJob::Base.queue_adapter = :marj # Globally, without Rails
|
|
78
81
|
SomeJob.queue_adapter = :marj # Single job
|
79
82
|
```
|
80
83
|
|
81
|
-
##
|
84
|
+
## Example Usage
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
# Enqueue and manually run a job:
|
88
|
+
job = SomeJob.perform_later('foo')
|
89
|
+
job.perform_now
|
90
|
+
|
91
|
+
# Retrieve and execute a job
|
92
|
+
Marj.due.next.perform_now
|
93
|
+
|
94
|
+
# Run all due jobs (single DB query)
|
95
|
+
Marj.due.perform_all
|
96
|
+
|
97
|
+
# Run all due jobs (multiple DB queries)
|
98
|
+
Marj.due.perform_all(batch_size: 1)
|
99
|
+
|
100
|
+
# Run all due jobs in a specific queue:
|
101
|
+
Marj.queue('foo').due.perform_all
|
102
|
+
|
103
|
+
# Run jobs as they become due:
|
104
|
+
loop do
|
105
|
+
Marj.due.perform_all rescue logger.error($!)
|
106
|
+
ensure
|
107
|
+
sleep 5.seconds
|
108
|
+
end
|
109
|
+
```
|
110
|
+
|
111
|
+
# Jobs Interface
|
112
|
+
|
113
|
+
The `Marj` module provides methods for interacting with enqueued jobs. These
|
114
|
+
methods accept, return and yield +ActiveJob+ objects rather than +ActiveRecord+
|
115
|
+
objects. Returned jobs are orderd by due date. To query the database directly,
|
116
|
+
use `Marj::Record`.
|
82
117
|
|
83
|
-
|
84
|
-
used to retrieve, execute and discard enqueued jobs. It returns, yields and
|
85
|
-
accepts `ActiveJob` objects rather than `ActiveRecord` objects. Jobs are
|
86
|
-
orderd by due date. To query the database directly, use `Marj::Record`.
|
118
|
+
Example usage:
|
87
119
|
|
88
120
|
```ruby
|
89
|
-
Marj
|
90
|
-
Marj
|
91
|
-
Marj
|
92
|
-
Marj
|
93
|
-
Marj
|
94
|
-
Marj
|
95
|
-
Marj
|
96
|
-
Marj
|
97
|
-
Marj
|
121
|
+
Marj.all # Returns all enqueued jobs.
|
122
|
+
Marj.queue # Returns jobs in the specified queue(s).
|
123
|
+
Marj.due # Returns jobs which are due to be executed.
|
124
|
+
Marj.next # Returns the next job(s) to be executed.
|
125
|
+
Marj.count # Returns the number of enqueued jobs.
|
126
|
+
Marj.where # Returns jobs matching the specified criteria.
|
127
|
+
Marj.perform_all # Executes all jobs.
|
128
|
+
Marj.discard_all # Discards all jobs.
|
129
|
+
Marj.discard # Discards the specified job.
|
98
130
|
```
|
99
131
|
|
100
|
-
|
101
|
-
the same `Marj::JobsInterface`. This can be used to chain query methods like:
|
132
|
+
Query methods can also be chained:
|
102
133
|
|
103
134
|
```ruby
|
104
|
-
Marj
|
135
|
+
Marj.due.where(job_class: SomeJob).next # Returns the next SomeJob that is due
|
105
136
|
```
|
106
137
|
|
107
|
-
|
108
|
-
|
138
|
+
# Custom Jobs Interface
|
139
|
+
|
140
|
+
The `Marj::JobsInterface` can be added to any class or module. For example, to
|
141
|
+
add it to all jobs classes:
|
109
142
|
|
110
143
|
```ruby
|
111
144
|
class ApplicationJob < ActiveJob::Base
|
@@ -125,40 +158,13 @@ ApplicationJob.due # Returns all jobs which are due to be executed.
|
|
125
158
|
SomeJob.due # Returns SomeJobs which are due to be executed.
|
126
159
|
```
|
127
160
|
|
128
|
-
## Example Usage
|
129
|
-
|
130
|
-
```ruby
|
131
|
-
# Enqueue and manually run a job:
|
132
|
-
job = SomeJob.perform_later('foo')
|
133
|
-
job.perform_now
|
134
|
-
|
135
|
-
# Retrieve and execute a job
|
136
|
-
Marj::Jobs.due.next.perform_now
|
137
|
-
|
138
|
-
# Run all due jobs (single DB query)
|
139
|
-
Marj::Jobs.due.perform_all
|
140
|
-
|
141
|
-
# Run all due jobs (multiple DB queries)
|
142
|
-
Marj::Jobs.due.perform_all(batch_size: 1)
|
143
|
-
|
144
|
-
# Run all due jobs in a specific queue:
|
145
|
-
Marj::Jobs.queue('foo').due.perform_all
|
146
|
-
|
147
|
-
# Run all jobs indefinitely, as they become due:
|
148
|
-
loop do
|
149
|
-
Marj::Jobs.due.perform_all rescue logger.error($!)
|
150
|
-
ensure
|
151
|
-
sleep 5.seconds
|
152
|
-
end
|
153
|
-
```
|
154
|
-
|
155
161
|
## Customization
|
156
162
|
|
157
163
|
It is possible to create a custom record class and jobs interface. This enables,
|
158
164
|
for instance, writing jobs to multiple databases/tables within a single
|
159
165
|
application.
|
160
166
|
|
161
|
-
```
|
167
|
+
```ruby
|
162
168
|
class CreateMyJobs < ActiveRecord::Migration[7.1]
|
163
169
|
def self.up
|
164
170
|
create_table :my_jobs, id: :string, primary_key: :job_id do |table|
|
@@ -184,28 +190,21 @@ class CreateMyJobs < ActiveRecord::Migration[7.1]
|
|
184
190
|
end
|
185
191
|
end
|
186
192
|
|
187
|
-
class MyRecord <
|
188
|
-
include Marj::RecordInterface
|
189
|
-
|
193
|
+
class MyRecord < Marj::Record
|
190
194
|
self.table_name = 'my_jobs'
|
191
195
|
end
|
192
196
|
|
193
197
|
CreateMyJobs.migrate(:up)
|
194
198
|
|
195
|
-
class
|
199
|
+
class MyJob < ActiveJob::Base
|
196
200
|
self.queue_adapter = MarjAdapter.new('MyRecord')
|
197
201
|
|
198
202
|
extend Marj::JobsInterface
|
199
203
|
|
200
204
|
def self.all
|
201
|
-
Marj::Relation.new(
|
202
|
-
self == ApplicationJob ?
|
203
|
-
MyRecord.ordered : MyRecord.where(job_class: self)
|
204
|
-
)
|
205
|
+
Marj::Relation.new(MyRecord.all)
|
205
206
|
end
|
206
|
-
end
|
207
207
|
|
208
|
-
class MyJob < ApplicationJob
|
209
208
|
def perform(msg)
|
210
209
|
puts msg
|
211
210
|
end
|
@@ -221,7 +220,7 @@ By default, jobs enqeued during tests will be written to the database. Enqueued
|
|
221
220
|
jobs can be executed via:
|
222
221
|
|
223
222
|
```ruby
|
224
|
-
Marj
|
223
|
+
Marj.due.perform_all
|
225
224
|
```
|
226
225
|
|
227
226
|
Alternatively, to use [ActiveJob::QueueAdapters::TestAdapter](https://api.rubyonrails.org/classes/ActiveJob/QueueAdapters/TestAdapter.html):
|
@@ -318,40 +317,48 @@ SomeJob.queue_adapter = FooAdapter.new # Uses FooAdapter directly
|
|
318
317
|
|
319
318
|
### Configuration
|
320
319
|
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
320
|
+
```ruby
|
321
|
+
config.active_job.default_queue_name
|
322
|
+
config.active_job.queue_name_prefix
|
323
|
+
config.active_job.queue_name_delimiter
|
324
|
+
config.active_job.retry_jitter
|
325
|
+
SomeJob.queue_name
|
326
|
+
SomeJob.queue_as
|
327
|
+
SomeJob.queue_name_prefix
|
328
|
+
SomeJob.queue_name_delimiter
|
329
|
+
SomeJob.retry_jitter
|
330
|
+
```
|
330
331
|
|
331
332
|
### Options
|
332
333
|
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
334
|
+
```ruby
|
335
|
+
:wait # Enqueues the job with the specified delay
|
336
|
+
:wait_until # Enqueues the job at the time specified
|
337
|
+
:queue # Enqueues the job on the specified queue
|
338
|
+
:priority # Enqueues the job with the specified priority
|
339
|
+
```
|
337
340
|
|
338
341
|
### Callbacks
|
339
342
|
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
343
|
+
```ruby
|
344
|
+
SomeJob.before_enqueue
|
345
|
+
SomeJob.after_enqueue
|
346
|
+
SomeJob.around_enqueue
|
347
|
+
SomeJob.before_perform
|
348
|
+
SomeJob.after_perform
|
349
|
+
SomeJob.around_perform
|
350
|
+
ActiveJob::Callbacks.singleton_class.set_callback(:execute, :before, &block)
|
351
|
+
ActiveJob::Callbacks.singleton_class.set_callback(:execute, :after, &block)
|
352
|
+
ActiveJob::Callbacks.singleton_class.set_callback(:execute, :around, &block)
|
353
|
+
```
|
349
354
|
|
350
355
|
### Handling Exceptions
|
351
356
|
|
352
|
-
|
353
|
-
|
354
|
-
|
357
|
+
```ruby
|
358
|
+
SomeJob.retry_on
|
359
|
+
SomeJob.discard_on
|
360
|
+
SomeJob.after_discard
|
361
|
+
```
|
355
362
|
|
356
363
|
### Creating Jobs
|
357
364
|
|
data/lib/marj/jobs_interface.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Marj
|
4
|
-
# The interface provided by {Marj
|
4
|
+
# The interface provided by {Marj} and {Marj::Relation}.
|
5
5
|
#
|
6
|
-
# To create a jobs interface for all job classes:
|
6
|
+
# To create a custom jobs interface, for example for all job classes in your application:
|
7
7
|
# class ApplicationJob < ActiveJob::Base
|
8
8
|
# extend Marj::JobsInterface
|
9
9
|
#
|
@@ -12,10 +12,19 @@ module Marj
|
|
12
12
|
# end
|
13
13
|
# end
|
14
14
|
#
|
15
|
-
# ApplicationJob
|
16
|
-
#
|
15
|
+
# class SomeJob < ApplicationJob
|
16
|
+
# def perform(msg)
|
17
|
+
# puts msg
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# This will allow you to query jobs via the +ApplicationJob+ class:
|
22
|
+
# ApplicationJob.next # Returns the next job of any type
|
17
23
|
#
|
18
|
-
#
|
24
|
+
# Or to query jobs via a specific job class:
|
25
|
+
# SomeJob.next # Returns the next SomeJob
|
26
|
+
#
|
27
|
+
# Alternatively, to create a jobs interface for a single job class:
|
19
28
|
# class SomeJob < ActiveJob::Base
|
20
29
|
# extend Marj::JobsInterface
|
21
30
|
#
|
@@ -23,16 +32,26 @@ module Marj
|
|
23
32
|
# Marj::Relation.new(Marj::Record.where(job_class: self).ordered)
|
24
33
|
# end
|
25
34
|
# end
|
26
|
-
#
|
27
|
-
# SomeJob.next
|
28
35
|
module JobsInterface
|
36
|
+
def self.included(clazz)
|
37
|
+
return if clazz == Marj::Relation
|
38
|
+
|
39
|
+
clazz.delegate :queue, :next, :count, :where, :due, :perform_all, :discard_all, to: :all
|
40
|
+
end
|
41
|
+
private_class_method :included
|
42
|
+
|
43
|
+
def self.extended(clazz)
|
44
|
+
clazz.singleton_class.delegate :queue, :next, :count, :where, :due, :perform_all, :discard_all, to: :all
|
45
|
+
end
|
46
|
+
private_class_method :extended
|
47
|
+
|
29
48
|
# Returns a {Marj::Relation} for jobs in the specified queue(s).
|
30
49
|
#
|
31
50
|
# @param queue [String, Symbol] the queue to query
|
32
|
-
# @param queues [Array<String
|
51
|
+
# @param queues [Array<String>, Array<Symbol>] more queues to query
|
33
52
|
# @return [Marj::Relation]
|
34
53
|
def queue(queue, *queues)
|
35
|
-
all.
|
54
|
+
Marj::Relation.new(all.where(queue_name: queues.dup.unshift(queue)))
|
36
55
|
end
|
37
56
|
|
38
57
|
# Returns the next job or the next N jobs if +limit+ is specified. If no jobs exist, returns +nil+.
|
@@ -40,7 +59,7 @@ module Marj
|
|
40
59
|
# @param limit [Integer, NilClass]
|
41
60
|
# @return [ActiveJob::Base, NilClass]
|
42
61
|
def next(limit = nil)
|
43
|
-
all.
|
62
|
+
all.first(limit)&.then { _1.is_a?(Array) ? _1.map(&:as_job) : _1.as_job }
|
44
63
|
end
|
45
64
|
|
46
65
|
# Returns a count of jobs, optionally either matching the specified column name criteria or where the specified
|
@@ -50,7 +69,7 @@ module Marj
|
|
50
69
|
# @param block [Proc, NilClass]
|
51
70
|
# @return [Integer]
|
52
71
|
def count(column_name = nil, &block)
|
53
|
-
all.count(column_name
|
72
|
+
block_given? ? all.count(column_name) { |r| block.call(r.as_job) } : all.count(column_name)
|
54
73
|
end
|
55
74
|
|
56
75
|
# Returns a {Marj::Relation} for jobs matching the specified criteria.
|
@@ -58,14 +77,14 @@ module Marj
|
|
58
77
|
# @param args [Array]
|
59
78
|
# @return [Marj::Relation]
|
60
79
|
def where(*args)
|
61
|
-
all.where(*args)
|
80
|
+
Marj::Relation.new(all.where(*args))
|
62
81
|
end
|
63
82
|
|
64
83
|
# Returns a {Marj::Relation} for enqueued jobs with a +scheduled_at+ that is either +null+ or in the past.
|
65
84
|
#
|
66
85
|
# @return [Marj::Relation]
|
67
86
|
def due
|
68
|
-
all.due
|
87
|
+
Marj::Relation.new(all.due)
|
69
88
|
end
|
70
89
|
|
71
90
|
# Calls +perform_now+ on each job.
|
@@ -73,14 +92,22 @@ module Marj
|
|
73
92
|
# @param batch_size [Integer, NilClass] the number of jobs to fetch at a time, or +nil+ to fetch all jobs at once
|
74
93
|
# @return [Array] the results returned by each job
|
75
94
|
def perform_all(batch_size: nil)
|
76
|
-
|
95
|
+
if batch_size
|
96
|
+
[].tap do |results|
|
97
|
+
while (jobs = all.limit(batch_size).map(&:as_job)).any?
|
98
|
+
results.concat(jobs.map { |job| ActiveJob::Callbacks.run_callbacks(:execute) { job.perform_now } })
|
99
|
+
end
|
100
|
+
end
|
101
|
+
else
|
102
|
+
all.map(&:as_job).map { |job| ActiveJob::Callbacks.run_callbacks(:execute) { job.perform_now } }
|
103
|
+
end
|
77
104
|
end
|
78
105
|
|
79
106
|
# Discards all jobs.
|
80
107
|
#
|
81
108
|
# @return [Numeric] the number of discarded jobs
|
82
109
|
def discard_all
|
83
|
-
all.
|
110
|
+
all.delete_all
|
84
111
|
end
|
85
112
|
end
|
86
113
|
end
|
data/lib/marj/record.rb
CHANGED
@@ -2,16 +2,81 @@
|
|
2
2
|
|
3
3
|
require 'active_job'
|
4
4
|
require 'active_record'
|
5
|
-
require_relative 'record_interface'
|
6
5
|
|
7
6
|
module Marj
|
8
|
-
# The
|
9
|
-
#
|
10
|
-
# See https://github.com/nicholasdower/marj
|
7
|
+
# The default +ActiveRecord+ class.
|
11
8
|
class Record < ActiveRecord::Base
|
12
|
-
|
13
|
-
extend Marj::RecordInterface::ClassMethods # Added explicitly to generate docs
|
9
|
+
self.table_name = Marj.table_name
|
14
10
|
|
15
|
-
|
11
|
+
# Order by +enqueued_at+ rather than +job_id+ (the default).
|
12
|
+
self.implicit_order_column = 'enqueued_at'
|
13
|
+
|
14
|
+
# Using a custom serializer for exception_executions so that we can interact with it as a hash rather than a
|
15
|
+
# string.
|
16
|
+
serialize(:exception_executions, coder: JSON)
|
17
|
+
|
18
|
+
# Using a custom serializer for arguments so that we can interact with as an array rather than a string.
|
19
|
+
# This enables code like:
|
20
|
+
# Marj::Record.next.arguments.first
|
21
|
+
# Marj::Record.next.update!(arguments: ['foo', 1, Time.now])
|
22
|
+
serialize(:arguments, coder: Class.new do
|
23
|
+
def self.dump(arguments)
|
24
|
+
return ActiveJob::Arguments.serialize(arguments).to_json if arguments.is_a?(Array)
|
25
|
+
return arguments if arguments.is_a?(String) || arguments.nil?
|
26
|
+
|
27
|
+
raise "invalid arguments: #{arguments}"
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.load(arguments)
|
31
|
+
arguments ? ActiveJob::Arguments.deserialize(JSON.parse(arguments)) : nil
|
32
|
+
end
|
33
|
+
end)
|
34
|
+
|
35
|
+
# Using a custom serializer for job_class so that we can interact with it as a class rather than a string.
|
36
|
+
serialize(:job_class, coder: Class.new do
|
37
|
+
def self.dump(clazz)
|
38
|
+
return clazz.name if clazz.is_a?(Class)
|
39
|
+
return clazz if clazz.is_a?(String) || clazz.nil?
|
40
|
+
|
41
|
+
raise "invalid class: #{clazz}"
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.load(str)
|
45
|
+
str&.constantize
|
46
|
+
end
|
47
|
+
end)
|
48
|
+
|
49
|
+
# Returns a job object for this record which will update the database when successfully executed, enqueued or
|
50
|
+
# discarded.
|
51
|
+
#
|
52
|
+
# @return [ActiveJob::Base]
|
53
|
+
def as_job
|
54
|
+
Marj.send(:to_job, self)
|
55
|
+
end
|
56
|
+
|
57
|
+
class << self
|
58
|
+
# Returns an +ActiveRecord::Relation+ scope for enqueued jobs with a +scheduled_at+ that is either +null+ or in
|
59
|
+
# the past.
|
60
|
+
#
|
61
|
+
# @return [ActiveRecord::Relation]
|
62
|
+
def due
|
63
|
+
where('scheduled_at IS NULL OR scheduled_at <= ?', Time.now.utc)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns an +ActiveRecord::Relation+ scope for jobs ordered by +priority+ (+null+ last), then +scheduled_at+
|
67
|
+
# (+null+ last), then +enqueued_at+.
|
68
|
+
#
|
69
|
+
# @return [ActiveRecord::Relation]
|
70
|
+
def ordered
|
71
|
+
order(
|
72
|
+
Arel.sql(<<~SQL.squish, Time.now.utc)
|
73
|
+
CASE WHEN scheduled_at IS NULL OR scheduled_at <= ? THEN 0 ELSE 1 END,
|
74
|
+
CASE WHEN priority IS NULL THEN 1 ELSE 0 END, priority,
|
75
|
+
CASE WHEN scheduled_at IS NULL THEN 1 ELSE 0 END, scheduled_at,
|
76
|
+
enqueued_at
|
77
|
+
SQL
|
78
|
+
)
|
79
|
+
end
|
80
|
+
end
|
16
81
|
end
|
17
82
|
end
|
data/lib/marj/relation.rb
CHANGED
@@ -8,52 +8,12 @@ module Marj
|
|
8
8
|
include Enumerable
|
9
9
|
include Marj::JobsInterface
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
@ar_relation = ar_relation
|
14
|
-
end
|
15
|
-
|
16
|
-
# (see Marj::JobsInterface#queue)
|
17
|
-
def queue(queue, *queues)
|
18
|
-
Marj::Relation.new(@ar_relation.where(queue_name: queues.dup.unshift(queue)))
|
19
|
-
end
|
20
|
-
|
21
|
-
# (see Marj::JobsInterface#next)
|
22
|
-
def next(limit = nil)
|
23
|
-
@ar_relation.first(limit)&.then { _1.is_a?(Array) ? _1.map(&:as_job) : _1.as_job }
|
24
|
-
end
|
25
|
-
|
26
|
-
# (see Marj::JobsInterface#count)
|
27
|
-
def count(column_name = nil, &block)
|
28
|
-
block_given? ? @ar_relation.count(column_name) { |r| block.call(r.as_job) } : @ar_relation.count(column_name)
|
29
|
-
end
|
11
|
+
attr_reader :all
|
12
|
+
private :all
|
30
13
|
|
31
|
-
#
|
32
|
-
def
|
33
|
-
|
34
|
-
end
|
35
|
-
|
36
|
-
# (see Marj::JobsInterface#due)
|
37
|
-
def due
|
38
|
-
Marj::Relation.new(@ar_relation.due)
|
39
|
-
end
|
40
|
-
|
41
|
-
# (see Marj::JobsInterface#perform_all)
|
42
|
-
def perform_all(batch_size: nil)
|
43
|
-
if batch_size
|
44
|
-
[].tap do |results|
|
45
|
-
while (jobs = @ar_relation.limit(batch_size).map(&:as_job)).any?
|
46
|
-
results.concat(jobs.map { |job| ActiveJob::Callbacks.run_callbacks(:execute) { job.perform_now } })
|
47
|
-
end
|
48
|
-
end
|
49
|
-
else
|
50
|
-
@ar_relation.map(&:as_job).map { |job| ActiveJob::Callbacks.run_callbacks(:execute) { job.perform_now } }
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
# (see Marj::JobsInterface#discard_all)
|
55
|
-
def discard_all
|
56
|
-
@ar_relation.delete_all
|
14
|
+
# Returns a {Marj::Relation} which wraps the specified +ActiveRecord+ relation.
|
15
|
+
def initialize(ar_relation)
|
16
|
+
@all = ar_relation
|
57
17
|
end
|
58
18
|
|
59
19
|
# Yields each job in this relation.
|
@@ -61,14 +21,14 @@ module Marj
|
|
61
21
|
# @param block [Proc]
|
62
22
|
# @return [Array] the jobs in this relation
|
63
23
|
def each(&block)
|
64
|
-
|
24
|
+
all.map(&:as_job).each(&block)
|
65
25
|
end
|
66
26
|
|
67
27
|
# Provides +pretty_inspect+ output containing arrays of jobs rather than arrays of records, similar to the output
|
68
28
|
# produced when calling +pretty_inspect+ on +ActiveRecord::Relation+.
|
69
29
|
#
|
70
30
|
# Instead of the default +pretty_inspect+ output:
|
71
|
-
# > Marj
|
31
|
+
# > Marj.all
|
72
32
|
# =>
|
73
33
|
# #<Marj::Relation:0x000000012728bd88
|
74
34
|
# @ar_relation=
|
@@ -86,7 +46,7 @@ module Marj
|
|
86
46
|
# timezone: "UTC">]>
|
87
47
|
#
|
88
48
|
# Produces:
|
89
|
-
# > Marj
|
49
|
+
# > Marj.all
|
90
50
|
# =>
|
91
51
|
# [#<TestJob:0x000000010b63cef8
|
92
52
|
# @_scheduled_at_time=nil,
|
data/lib/marj.rb
CHANGED
@@ -1,21 +1,74 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative 'marj_adapter'
|
4
|
-
require_relative 'marj/jobs'
|
5
4
|
require_relative 'marj/jobs_interface'
|
6
|
-
require_relative 'marj/record_interface'
|
7
5
|
require_relative 'marj/relation'
|
8
6
|
|
9
7
|
# A minimal database-backed ActiveJob queueing backend.
|
10
8
|
#
|
9
|
+
# The {Marj} module provides methods for interacting with enqueued jobs. These methods accept, return and yield
|
10
|
+
# +ActiveJob+ objects rather than +ActiveRecord+ objects. Returned jobs are ordered by due date. To query the database
|
11
|
+
# directly, use {Record}.
|
12
|
+
#
|
13
|
+
# Example usage:
|
14
|
+
# Marj.all # Returns all enqueued jobs.
|
15
|
+
# Marj.queue # Returns jobs in the specified queue(s).
|
16
|
+
# Marj.due # Returns jobs which are due to be executed.
|
17
|
+
# Marj.next # Returns the next job(s) to be executed.
|
18
|
+
# Marj.count # Returns the number of enqueued jobs.
|
19
|
+
# Marj.where # Returns jobs matching the specified criteria.
|
20
|
+
# Marj.perform_all # Executes all jobs.
|
21
|
+
# Marj.discard_all # Discards all jobs.
|
22
|
+
# Marj.discard # Discards the specified job.
|
23
|
+
#
|
24
|
+
# Query methods can also be chained:
|
25
|
+
# Marj.due.where(job_class: SomeJob).next # Returns the next SomeJob that is due
|
26
|
+
#
|
27
|
+
# Note that by default, Marj uses {Marj::Record} to interact with the +jobs+ table. To use a different record class, set
|
28
|
+
# {record_class}. To simply override the table name, set {table_name} before loading +ActiveRecord+.
|
29
|
+
#
|
11
30
|
# See https://github.com/nicholasdower/marj
|
12
31
|
module Marj
|
13
32
|
# The Marj version.
|
14
|
-
VERSION = '
|
33
|
+
VERSION = '4.0.0'
|
15
34
|
|
16
35
|
Kernel.autoload(:Record, File.expand_path(File.join('marj', 'record.rb'), __dir__))
|
17
36
|
|
37
|
+
@table_name = :jobs
|
38
|
+
@record_class = 'Marj::Record'
|
39
|
+
|
18
40
|
class << self
|
41
|
+
include Marj::JobsInterface
|
42
|
+
|
43
|
+
# @!attribute record_class
|
44
|
+
# The name of the +ActiveRecord+ class. Defaults to +Marj::Record+.
|
45
|
+
# @return [Class, String]
|
46
|
+
|
47
|
+
attr_writer :record_class
|
48
|
+
|
49
|
+
def record_class
|
50
|
+
@record_class = @record_class.is_a?(String) ? @record_class.constantize : @record_class
|
51
|
+
end
|
52
|
+
|
53
|
+
# @!attribute table_name
|
54
|
+
# The name of the database table. Defaults to +:jobs+.
|
55
|
+
# @return [Symbol, String]
|
56
|
+
attr_accessor :table_name
|
57
|
+
|
58
|
+
# Returns a {Marj::Relation} for all jobs in the order they should be executed.
|
59
|
+
#
|
60
|
+
# @return [Marj::Relation]
|
61
|
+
def all
|
62
|
+
Marj::Relation.new(Marj.record_class.ordered)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Discards the specified job.
|
66
|
+
#
|
67
|
+
# @return [Integer] the number of discarded jobs
|
68
|
+
def discard(job)
|
69
|
+
all.where(job_id: job.job_id).discard_all
|
70
|
+
end
|
71
|
+
|
19
72
|
private
|
20
73
|
|
21
74
|
# Creates a job instance for the given record which will update the database when successfully executed, enqueued or
|
@@ -36,7 +89,6 @@ module Marj
|
|
36
89
|
|
37
90
|
job.tap { job.deserialize(job_data) }
|
38
91
|
end
|
39
|
-
private :to_job
|
40
92
|
|
41
93
|
# Registers callbacks for the given job which destroy the given database record when the job succeeds or is
|
42
94
|
# discarded.
|
@@ -57,12 +109,11 @@ module Marj
|
|
57
109
|
|
58
110
|
job
|
59
111
|
end
|
60
|
-
private :register_callbacks
|
61
112
|
|
62
113
|
# Enqueue a job for execution at the specified time.
|
63
114
|
#
|
64
115
|
# @param job [ActiveJob::Base] the job to enqueue
|
65
|
-
# @param record_class [Class] the +ActiveRecord+
|
116
|
+
# @param record_class [Class] the +ActiveRecord+ class
|
66
117
|
# @param time [Time, NilClass] optional time at which to execute the job
|
67
118
|
# @return [ActiveJob::Base] the enqueued job
|
68
119
|
def enqueue(job, record_class, time = nil)
|
@@ -101,6 +152,5 @@ module Marj
|
|
101
152
|
end
|
102
153
|
job
|
103
154
|
end
|
104
|
-
private :enqueue
|
105
155
|
end
|
106
156
|
end
|
data/lib/marj_adapter.rb
CHANGED
@@ -4,9 +4,9 @@
|
|
4
4
|
#
|
5
5
|
# See https://github.com/nicholasdower/marj
|
6
6
|
class MarjAdapter
|
7
|
-
# Creates a new adapter which will enqueue jobs using the given +ActiveRecord+
|
7
|
+
# Creates a new adapter which will enqueue jobs using the given +ActiveRecord+ class.
|
8
8
|
#
|
9
|
-
# @param record_class [Class, String] the +ActiveRecord+
|
9
|
+
# @param record_class [Class, String] the +ActiveRecord+ class (or its name) to use to store jobs
|
10
10
|
def initialize(record_class = 'Marj::Record')
|
11
11
|
@record_class = record_class
|
12
12
|
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: 4.0.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-01-
|
11
|
+
date: 2024-01-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activejob
|
@@ -48,10 +48,8 @@ files:
|
|
48
48
|
- LICENSE.txt
|
49
49
|
- README.md
|
50
50
|
- lib/marj.rb
|
51
|
-
- lib/marj/jobs.rb
|
52
51
|
- lib/marj/jobs_interface.rb
|
53
52
|
- lib/marj/record.rb
|
54
|
-
- lib/marj/record_interface.rb
|
55
53
|
- lib/marj/relation.rb
|
56
54
|
- lib/marj_adapter.rb
|
57
55
|
homepage: https://github.com/nicholasdower/marj
|
@@ -59,8 +57,8 @@ licenses:
|
|
59
57
|
- MIT
|
60
58
|
metadata:
|
61
59
|
bug_tracker_uri: https://github.com/nicholasdower/marj/issues
|
62
|
-
changelog_uri: https://github.com/nicholasdower/marj/releases/tag/
|
63
|
-
documentation_uri: https://www.rubydoc.info/github/nicholasdower/marj/
|
60
|
+
changelog_uri: https://github.com/nicholasdower/marj/releases/tag/v4.0.0
|
61
|
+
documentation_uri: https://www.rubydoc.info/github/nicholasdower/marj/v4.0.0
|
64
62
|
homepage_uri: https://github.com/nicholasdower/marj
|
65
63
|
rubygems_mfa_required: 'true'
|
66
64
|
source_code_uri: https://github.com/nicholasdower/marj
|
data/lib/marj/jobs.rb
DELETED
@@ -1,29 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative 'jobs_interface'
|
4
|
-
require_relative 'relation'
|
5
|
-
|
6
|
-
module Marj
|
7
|
-
# Provides methods for querying, performing and discarding jobs. Returns, yields and accepts
|
8
|
-
# +ActiveJob+ objects rather than +ActiveRecord+ objects. To query the database directly, use
|
9
|
-
# {Marj::Record}.
|
10
|
-
#
|
11
|
-
# To create a custom jobs interface, see {Marj::JobsInterface}.
|
12
|
-
module Jobs
|
13
|
-
singleton_class.include Marj::JobsInterface
|
14
|
-
|
15
|
-
# Returns a {Marj::Relation} for all jobs in the order they should be executed.
|
16
|
-
#
|
17
|
-
# @return [Marj::Relation]
|
18
|
-
def self.all
|
19
|
-
Marj::Relation.new(Marj::Record.ordered)
|
20
|
-
end
|
21
|
-
|
22
|
-
# Discards the specified job.
|
23
|
-
#
|
24
|
-
# @return [Integer] the number of discarded jobs
|
25
|
-
def self.discard(job)
|
26
|
-
all.where(job_id: job.job_id).discard_all
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
@@ -1,94 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'active_job/arguments'
|
4
|
-
|
5
|
-
module Marj
|
6
|
-
# Provides base functionality for {Marj::Record}. Can be used to create a custom +ActiveRecord+ model class.
|
7
|
-
#
|
8
|
-
# Example Usage:
|
9
|
-
# class MyRecord < ActiveRecord::Base
|
10
|
-
# include Marj::RecordInterface
|
11
|
-
#
|
12
|
-
# self.table_name = 'my_jobs'
|
13
|
-
# end
|
14
|
-
module RecordInterface
|
15
|
-
# Adds {ClassMethods}, custom serializers and an implicit order column to the including class.
|
16
|
-
#
|
17
|
-
# @param clazz [Class] the including class
|
18
|
-
def self.included(clazz)
|
19
|
-
clazz.extend(ClassMethods)
|
20
|
-
|
21
|
-
# Order by +enqueued_at+ rather than +job_id+ (the default).
|
22
|
-
clazz.implicit_order_column = 'enqueued_at'
|
23
|
-
|
24
|
-
# Using a custom serializer for exception_executions so that we can interact with it as a hash rather than a
|
25
|
-
# string.
|
26
|
-
clazz.serialize(:exception_executions, coder: JSON)
|
27
|
-
|
28
|
-
# Using a custom serializer for arguments so that we can interact with as an array rather than a string.
|
29
|
-
# This enables code like:
|
30
|
-
# Marj::Record.next.arguments.first
|
31
|
-
# Marj::Record.next.update!(arguments: ['foo', 1, Time.now])
|
32
|
-
clazz.serialize(:arguments, coder: Class.new do
|
33
|
-
def self.dump(arguments)
|
34
|
-
return ActiveJob::Arguments.serialize(arguments).to_json if arguments.is_a?(Array)
|
35
|
-
return arguments if arguments.is_a?(String) || arguments.nil?
|
36
|
-
|
37
|
-
raise "invalid arguments: #{arguments}"
|
38
|
-
end
|
39
|
-
|
40
|
-
def self.load(arguments)
|
41
|
-
arguments ? ActiveJob::Arguments.deserialize(JSON.parse(arguments)) : nil
|
42
|
-
end
|
43
|
-
end)
|
44
|
-
|
45
|
-
# Using a custom serializer for job_class so that we can interact with it as a class rather than a string.
|
46
|
-
clazz.serialize(:job_class, coder: Class.new do
|
47
|
-
def self.dump(clazz)
|
48
|
-
return clazz.name if clazz.is_a?(Class)
|
49
|
-
return clazz if clazz.is_a?(String) || clazz.nil?
|
50
|
-
|
51
|
-
raise "invalid class: #{clazz}"
|
52
|
-
end
|
53
|
-
|
54
|
-
def self.load(str)
|
55
|
-
str&.constantize
|
56
|
-
end
|
57
|
-
end)
|
58
|
-
end
|
59
|
-
|
60
|
-
# Class methods for {Marj::RecordInterface}.
|
61
|
-
module ClassMethods
|
62
|
-
# Returns an +ActiveRecord::Relation+ scope for enqueued jobs with a +scheduled_at+ that is either +null+ or in
|
63
|
-
# the past.
|
64
|
-
#
|
65
|
-
# @return [ActiveRecord::Relation]
|
66
|
-
def due
|
67
|
-
where('scheduled_at IS NULL OR scheduled_at <= ?', Time.now.utc)
|
68
|
-
end
|
69
|
-
|
70
|
-
# Returns an +ActiveRecord::Relation+ scope for jobs ordered by +priority+ (+null+ last), then +scheduled_at+
|
71
|
-
# (+null+ last), then +enqueued_at+.
|
72
|
-
#
|
73
|
-
# @return [ActiveRecord::Relation]
|
74
|
-
def ordered
|
75
|
-
order(
|
76
|
-
Arel.sql(<<~SQL.squish, Time.now.utc)
|
77
|
-
CASE WHEN scheduled_at IS NULL OR scheduled_at <= ? THEN 0 ELSE 1 END,
|
78
|
-
CASE WHEN priority IS NULL THEN 1 ELSE 0 END, priority,
|
79
|
-
CASE WHEN scheduled_at IS NULL THEN 1 ELSE 0 END, scheduled_at,
|
80
|
-
enqueued_at
|
81
|
-
SQL
|
82
|
-
)
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
# Returns a job object for this record which will update the database when successfully executed, enqueued or
|
87
|
-
# discarded.
|
88
|
-
#
|
89
|
-
# @return [ActiveJob::Base]
|
90
|
-
def as_job
|
91
|
-
Marj.send(:to_job, self)
|
92
|
-
end
|
93
|
-
end
|
94
|
-
end
|