marj 3.0.0 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7afb73261695ca8eb0005d59b292ec659a9c10124a2b3984242e3f8e209fc897
4
- data.tar.gz: 5f5fea852ab9a8e7eb9ca6fa59ed6cb4e0537651bf3edf6713ec0cad4a1c1ed0
3
+ metadata.gz: 8ea99cb466811e6f77fb6476e7e8ca02b7345c8d297ebdb1954b9109f772bfd9
4
+ data.tar.gz: 35a1ed99e2afe6934a5a7ea9b40375a93d5c79b9c46d304aac49457913794fba
5
5
  SHA512:
6
- metadata.gz: 348506cb34c7956e4c17554223c81d47353905ebdfb684c965d4deeb2b3a47727b5cb971211e2f2c48e039c8c5c1d9a1b02a6e9647be247ca0d86472a357f1b0
7
- data.tar.gz: 94069a23d1882ff62c5b630e5e50357c63f230f8fc4a86828d3e4d785402f6c325fb82fb2d398c5ec937a572018b8c4027f5569fb818dcbc1dd2258e1cc9ac54
6
+ metadata.gz: 0a1568a19c7faeedc80d0a17cf71c536a5dcb9b497645c87772213b5a20ca22b8ae250099826a75654d36260b5d34d77c896b81b28cc87e3ec1e4e181b5440bb
7
+ data.tar.gz: 701727d777c8f9c990bdca6d66f9cf3d76d41eddddd77f1dbcbb1c75cff5b75b7acce065cb5c83b01c23dff3a6878218b4b6f3c4528d0e307ae8bc8e192d91e9
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://www.rubydoc.info/github/nicholasdower/marj <br>
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` model class is provided to query the database directly.
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 jobs table
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
 
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
+
81
111
  ## Jobs Interface
82
112
 
83
- `Marj::Jobs` provides a query interface (`Marj::JobsInterface`) which can be
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`.
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`.
117
+
118
+ Example usage:
87
119
 
88
120
  ```ruby
89
- Marj::Jobs.all # Returns all enqueued jobs.
90
- Marj::Jobs.queue # Returns jobs in the specified queue(s).
91
- Marj::Jobs.due # Returns jobs which are due to be executed.
92
- Marj::Jobs.next # Returns the next job(s) to be executed.
93
- Marj::Jobs.count # Returns the number of enqueued jobs.
94
- Marj::Jobs.where # Returns jobs matching the specified criteria.
95
- Marj::Jobs.perform_all # Executes all jobs.
96
- Marj::Jobs.discard_all # Discards all jobs.
97
- Marj::Jobs.discard # Discards the specified job.
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
- `all`, `queue`, `due` and `where` return a `Marj::Relation` which provides
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::Jobs.due.where(job_class: SomeJob).next
135
+ Marj.due.where(job_class: SomeJob).next # Returns the next SomeJob that is due
105
136
  ```
106
137
 
107
- Note that the `Marj::JobsInterface` can be added to any class or module. For
108
- example, to add it to all jobs classes:
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,12 @@ 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
- ## Customization
161
+ ## Multiple Tables
156
162
 
157
- It is possible to create a custom record class and jobs interface. This enables,
158
- for instance, writing jobs to multiple databases/tables within a single
159
- application.
163
+ It is possible to create a custom record class in order to, for instance,
164
+ write jobs to multiple databases/tables within a single application.
160
165
 
161
- ```
166
+ ```ruby
162
167
  class CreateMyJobs < ActiveRecord::Migration[7.1]
163
168
  def self.up
164
169
  create_table :my_jobs, id: :string, primary_key: :job_id do |table|
@@ -184,28 +189,21 @@ class CreateMyJobs < ActiveRecord::Migration[7.1]
184
189
  end
185
190
  end
186
191
 
187
- class MyRecord < ActiveRecord::Base
188
- include Marj::RecordInterface
189
-
192
+ class MyRecord < Marj::Record
190
193
  self.table_name = 'my_jobs'
191
194
  end
192
195
 
193
196
  CreateMyJobs.migrate(:up)
194
197
 
195
- class ApplicationJob < ActiveJob::Base
198
+ class MyJob < ActiveJob::Base
196
199
  self.queue_adapter = MarjAdapter.new('MyRecord')
197
200
 
198
201
  extend Marj::JobsInterface
199
202
 
200
203
  def self.all
201
- Marj::Relation.new(
202
- self == ApplicationJob ?
203
- MyRecord.ordered : MyRecord.where(job_class: self)
204
- )
204
+ Marj::Relation.new(MyRecord.all)
205
205
  end
206
- end
207
206
 
208
- class MyJob < ApplicationJob
209
207
  def perform(msg)
210
208
  puts msg
211
209
  end
@@ -221,7 +219,7 @@ By default, jobs enqeued during tests will be written to the database. Enqueued
221
219
  jobs can be executed via:
222
220
 
223
221
  ```ruby
224
- Marj::Jobs.due.perform_all
222
+ Marj.due.perform_all
225
223
  ```
226
224
 
227
225
  Alternatively, to use [ActiveJob::QueueAdapters::TestAdapter](https://api.rubyonrails.org/classes/ActiveJob/QueueAdapters/TestAdapter.html):
@@ -318,40 +316,48 @@ SomeJob.queue_adapter = FooAdapter.new # Uses FooAdapter directly
318
316
 
319
317
  ### Configuration
320
318
 
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`
319
+ ```ruby
320
+ config.active_job.default_queue_name
321
+ config.active_job.queue_name_prefix
322
+ config.active_job.queue_name_delimiter
323
+ config.active_job.retry_jitter
324
+ SomeJob.queue_name
325
+ SomeJob.queue_as
326
+ SomeJob.queue_name_prefix
327
+ SomeJob.queue_name_delimiter
328
+ SomeJob.retry_jitter
329
+ ```
330
330
 
331
331
  ### Options
332
332
 
333
- - `:wait` - Enqueues the job with the specified delay
334
- - `:wait_until` - Enqueues the job at the time specified
335
- - `:queue` - Enqueues the job on the specified queue
336
- - `:priority` - Enqueues the job with the specified priority
333
+ ```ruby
334
+ :wait # Enqueues the job with the specified delay
335
+ :wait_until # Enqueues the job at the time specified
336
+ :queue # Enqueues the job on the specified queue
337
+ :priority # Enqueues the job with the specified priority
338
+ ```
337
339
 
338
340
  ### Callbacks
339
341
 
340
- - `SomeJob.before_enqueue`
341
- - `SomeJob.after_enqueue`
342
- - `SomeJob.around_enqueue`
343
- - `SomeJob.before_perform`
344
- - `SomeJob.after_perform`
345
- - `SomeJob.around_perform`
346
- - `ActiveJob::Callbacks.singleton_class.set_callback(:execute, :before, &block)`
347
- - `ActiveJob::Callbacks.singleton_class.set_callback(:execute, :after, &block)`
348
- - `ActiveJob::Callbacks.singleton_class.set_callback(:execute, :around, &block)`
342
+ ```ruby
343
+ SomeJob.before_enqueue
344
+ SomeJob.after_enqueue
345
+ SomeJob.around_enqueue
346
+ SomeJob.before_perform
347
+ SomeJob.after_perform
348
+ SomeJob.around_perform
349
+ ActiveJob::Callbacks.singleton_class.set_callback(:execute, :before, &block)
350
+ ActiveJob::Callbacks.singleton_class.set_callback(:execute, :after, &block)
351
+ ActiveJob::Callbacks.singleton_class.set_callback(:execute, :around, &block)
352
+ ```
349
353
 
350
354
  ### Handling Exceptions
351
355
 
352
- - `SomeJob.retry_on`
353
- - `SomeJob.discard_on`
354
- - `SomeJob.after_discard`
356
+ ```ruby
357
+ SomeJob.retry_on
358
+ SomeJob.discard_on
359
+ SomeJob.after_discard
360
+ ```
355
361
 
356
362
  ### Creating Jobs
357
363
 
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Marj
4
- # The interface provided by {Marj::Jobs} and {Marj::Relation}. Include to create a custom jobs interface.
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.next
16
- # SomeJob.next
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
- # To create a jobs interface for a single job class:
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, Array<Symbol>] more queues to query
51
+ # @param queues [Array<String>, Array<Symbol>] more queues to query
33
52
  # @return [Marj::Relation]
34
53
  def queue(queue, *queues)
35
- all.queue(queue, *queues)
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.next(limit)
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, &block)
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
- all.perform_all(batch_size: batch_size)
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.discard_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 Marj ActiveRecord model class.
9
- #
10
- # See https://github.com/nicholasdower/marj
7
+ # The default +ActiveRecord+ class.
11
8
  class Record < ActiveRecord::Base
12
- include Marj::RecordInterface
13
- extend Marj::RecordInterface::ClassMethods # Added explicitly to generate docs
9
+ self.table_name = Marj.table_name
14
10
 
15
- self.table_name = 'jobs'
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
- # Returns a Marj::Relation which wraps the specified +ActiveRecord+ relation.
12
- def initialize(ar_relation)
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
- # (see Marj::JobsInterface#where)
32
- def where(*args)
33
- Marj::Relation.new(@ar_relation.where(*args))
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
- @ar_relation.map(&:as_job).each(&block)
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::Jobs.all
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::Jobs.all
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 = '3.0.0'
33
+ VERSION = '4.1.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
@@ -34,9 +87,16 @@ module Marj
34
87
  # ActiveJob::Base#deserialize expects dates to be strings rather than Time objects.
35
88
  job_data = job_data.to_h { |k, v| [k, %w[enqueued_at scheduled_at].include?(k) ? v&.iso8601 : v] }
36
89
 
37
- job.tap { job.deserialize(job_data) }
90
+ job.deserialize(job_data)
91
+
92
+ # ActiveJob deserializes arguments on demand when a job is performed. Until then they are empty. That's strange.
93
+ # Instead, deserialize them now. Also, clear `serialized_arguments` to prevent ActiveJob from overwriting changes
94
+ # to arguments when serializing later.
95
+ job.arguments = record.arguments
96
+ job.serialized_arguments = nil
97
+
98
+ job
38
99
  end
39
- private :to_job
40
100
 
41
101
  # Registers callbacks for the given job which destroy the given database record when the job succeeds or is
42
102
  # discarded.
@@ -57,12 +117,11 @@ module Marj
57
117
 
58
118
  job
59
119
  end
60
- private :register_callbacks
61
120
 
62
121
  # Enqueue a job for execution at the specified time.
63
122
  #
64
123
  # @param job [ActiveJob::Base] the job to enqueue
65
- # @param record_class [Class] the +ActiveRecord+ model class
124
+ # @param record_class [Class] the +ActiveRecord+ class
66
125
  # @param time [Time, NilClass] optional time at which to execute the job
67
126
  # @return [ActiveJob::Base] the enqueued job
68
127
  def enqueue(job, record_class, time = nil)
@@ -101,6 +160,5 @@ module Marj
101
160
  end
102
161
  job
103
162
  end
104
- private :enqueue
105
163
  end
106
164
  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+ model class.
7
+ # Creates a new adapter which will enqueue jobs using the given +ActiveRecord+ class.
8
8
  #
9
- # @param record_class [Class, String] the +ActiveRecord+ model class (or its name) to use to store jobs
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: 3.0.0
4
+ version: 4.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-01-27 00:00:00.000000000 Z
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/v3.0.0
63
- documentation_uri: https://www.rubydoc.info/github/nicholasdower/marj/v3.0.0
60
+ changelog_uri: https://github.com/nicholasdower/marj/releases/tag/v4.1.0
61
+ documentation_uri: https://www.rubydoc.info/github/nicholasdower/marj/v4.1.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