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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7afb73261695ca8eb0005d59b292ec659a9c10124a2b3984242e3f8e209fc897
4
- data.tar.gz: 5f5fea852ab9a8e7eb9ca6fa59ed6cb4e0537651bf3edf6713ec0cad4a1c1ed0
3
+ metadata.gz: 4eb6129fa4948ed6e108469f44f380cda908620c6d08392cfa1e8c3bc84a9875
4
+ data.tar.gz: 28faed9a10fa3a8c885b521d6c3465196f814e97af07018d5eea88e580f322a9
5
5
  SHA512:
6
- metadata.gz: 348506cb34c7956e4c17554223c81d47353905ebdfb684c965d4deeb2b3a47727b5cb971211e2f2c48e039c8c5c1d9a1b02a6e9647be247ca0d86472a357f1b0
7
- data.tar.gz: 94069a23d1882ff62c5b630e5e50357c63f230f8fc4a86828d3e4d785402f6c325fb82fb2d398c5ec937a572018b8c4027f5569fb818dcbc1dd2258e1cc9ac54
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://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
 
81
- ## Jobs Interface
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
- `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`.
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,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 < ActiveRecord::Base
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 ApplicationJob < ActiveJob::Base
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::Jobs.due.perform_all
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
- - `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`
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
- - `: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
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
- - `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)`
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
- - `SomeJob.retry_on`
353
- - `SomeJob.discard_on`
354
- - `SomeJob.after_discard`
357
+ ```ruby
358
+ SomeJob.retry_on
359
+ SomeJob.discard_on
360
+ SomeJob.after_discard
361
+ ```
355
362
 
356
363
  ### Creating Jobs
357
364
 
@@ -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.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+ model class
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+ 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.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-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.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