marj 3.0.0 → 4.0.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 +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
|