marj 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5fd21edb773f1e25cc23e4099e7b0e6a3e2bb58485a988b063c37fdb71f31b82
4
- data.tar.gz: aadc1963f4283a092a000bf0c8f732cd57a53f0ed69ad4c83c8c4dbd053907a3
3
+ metadata.gz: f5fe8134484360b68c0cf69356f6a037897c6896be81bc393a9dfc0fb0684055
4
+ data.tar.gz: 4b7575b721d05681876846f3f06d47440d366bc875e02cadeae7de4582270114
5
5
  SHA512:
6
- metadata.gz: 7ddc1d33db20fb9d9ee3a587a67e66c54dcffb8d7f04dc366f53d40605b3239642c5466196e27debe86b1a8c74e10138557f2797232324c7f7a4d49b5df0134c
7
- data.tar.gz: 04a4170a8e39b64648bf125da0f1fe1eae1e5888bd33412ff896dc0358f1a6c5b37bd4df1305fb9b2be715812318a49354b0eae1ccc756ceff65bf96879060cc
6
+ metadata.gz: dbfa1cd4cfb90ca4c47b039f0f320e5cc847aa9863b640cd52b445f5074c13aaa0f2679f4ec950001a086f0b981f58f6aa180a250a4790753691b249dc4fa19f
7
+ data.tar.gz: d1903bcb55c22522d270d950b316ee99ce05edca0c279cc055cb39a71ecfd389d02acd5d1bcc029e931636e26c4708bfd872a20ab71ecc9328e69928a3b535c2
data/README.md CHANGED
@@ -55,32 +55,18 @@ end
55
55
 
56
56
  ### 3. Configure the queue adapter
57
57
 
58
- If using Rails, configure the queue adapter via `Rails::Application`:
59
-
60
58
  ```ruby
61
59
  require 'marj'
62
60
 
63
- # Configure via Rails::Application:
61
+ # With rails:
64
62
  class MyApplication < Rails::Application
65
63
  config.active_job.queue_adapter = :marj
66
64
  end
67
65
 
68
- # Or for specific jobs:
69
- class SomeJob < ActiveJob::Base
70
- self.queue_adapter = :marj
71
- end
72
- ```
73
-
74
- If not using Rails:
75
-
76
- ```ruby
77
- require 'marj'
78
- require 'marj/record' # Loads ActiveRecord
79
-
80
- # Configure via ActiveJob::Base:
66
+ # Without Rails:
81
67
  ActiveJob::Base.queue_adapter = :marj
82
68
 
83
- # Or for specific jobs:
69
+ # Or for specific jobs (with or without Rails):
84
70
  class SomeJob < ActiveJob::Base
85
71
  self.queue_adapter = :marj
86
72
  end
@@ -95,11 +81,15 @@ job.perform_now
95
81
 
96
82
  # Enqueue, retrieve and manually run a job:
97
83
  SomeJob.perform_later('foo')
98
- Marj.first.execute
84
+ record = Marj.first
85
+ record.execute
99
86
 
100
87
  # Run all available jobs:
101
88
  Marj.work_off
102
89
 
90
+ # Run all available jobs in a specific queue:
91
+ Marj.work_off(source = -> { Marj.where(queue_name: 'foo').available.first })
92
+
103
93
  # Run jobs as they become available:
104
94
  loop do
105
95
  Marj.work_off
@@ -127,7 +117,7 @@ SomeJob.queue_adapter = :foo # Instantiates FooAdapter
127
117
  SomeJob.queue_adapter = FooAdapter.new # Uses FooAdapter directly
128
118
  ```
129
119
 
130
- ## Configuration
120
+ ### Configuration
131
121
 
132
122
  - `config.active_job.default_queue_name`
133
123
  - `config.active_job.queue_name_prefix`
@@ -158,7 +148,7 @@ SomeJob.queue_adapter = FooAdapter.new # Uses FooAdapter directly
158
148
  - `ActiveJob::Callbacks.singleton_class.set_callback(:execute, :after, &block)`
159
149
  - `ActiveJob::Callbacks.singleton_class.set_callback(:execute, :around, &block)`
160
150
 
161
- ## Handling Exceptions
151
+ ### Handling Exceptions
162
152
 
163
153
  - `SomeJob.retry_on`
164
154
  - `SomeJob.discard_on`
@@ -207,9 +197,9 @@ SomeJob.set(options).perform_all_later(SomeJob.new, SomeJob.new, options:)
207
197
  # Executed without enqueueing, enqueued on failure if retries configured
208
198
  SomeJob.new(args).perform_now
209
199
  SomeJob.perform_now(args)
210
- ActiveJob::Base.exeucute(SomeJob.new(args).serialize)
200
+ ActiveJob::Base.execute(SomeJob.new(args).serialize)
211
201
 
212
202
  # Executed after enqueueing
213
203
  SomeJob.perform_later(args).perform_now
214
- ActiveJob::Base.exeucute(SomeJob.perform_later(args).serialize)
204
+ ActiveJob::Base.execute(SomeJob.perform_later(args).serialize)
215
205
  ```
data/app/models/marj.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_job'
3
4
  require 'active_record'
4
5
 
5
6
  # Marj is a Minimal ActiveRecord-based Jobs library.
@@ -7,36 +8,31 @@ require 'active_record'
7
8
  # See https://github.com/nicholasdower/marj
8
9
  class Marj < ActiveRecord::Base
9
10
  # The Marj version.
10
- VERSION = '1.0.0'
11
+ VERSION = '1.1.0'
11
12
 
12
- self.table_name = 'jobs'
13
- self.implicit_order_column = 'enqueued_at' # Order by +enqueued_at+ rather than +job_id+ (the default)
14
-
15
- serialize(:exception_executions, coder: JSON)
16
- serialize(:arguments, coder: Class.new do
17
- def self.dump(arguments)
18
- return ActiveJob::Arguments.serialize(arguments).to_json if arguments.is_a?(Array)
19
- return arguments if arguments.is_a?(String) || arguments.nil?
20
-
21
- raise "invalid arguments: #{arguments}"
22
- end
13
+ # Executes the job associated with this record and returns the result.
14
+ def execute
15
+ # Normally we would call ActiveJob::Base#execute which has the following implemenation:
16
+ # ActiveJob::Callbacks.run_callbacks(:execute) do
17
+ # job = deserialize(job_data)
18
+ # job.perform_now
19
+ # end
20
+ # However, we need to instantiate the job ourselves in order to register callbacks before execution.
21
+ ActiveJob::Callbacks.run_callbacks(:execute) do
22
+ # See register_callbacks for details on how callbacks are used.
23
+ job = job_class.new.tap { Marj.send(:register_callbacks, _1, self) }
23
24
 
24
- def self.load(arguments)
25
- arguments ? ActiveJob::Arguments.deserialize(JSON.parse(arguments)) : nil
26
- end
27
- end)
28
- serialize(:job_class, coder: Class.new do
29
- def self.dump(clazz)
30
- return clazz.name if clazz.is_a?(Class)
31
- return clazz if clazz.is_a?(String) || clazz.nil?
25
+ # ActiveJob::Base#deserialize expects serialized arguments. But the record arguments have already been
26
+ # deserialized by a custom ActiveRecord serializer (see below). So instead we use the raw arguments string.
27
+ job_data = attributes.merge('arguments' => JSON.parse(read_attribute_before_type_cast(:arguments)))
32
28
 
33
- raise "invalid class: #{clazz}"
34
- end
29
+ # ActiveJob::Base#deserialize expects dates to be strings rather than Time objects.
30
+ job_data = job_data.to_h { |k, v| [k, %w[enqueued_at scheduled_at].include?(k) ? v&.iso8601 : v] }
31
+ job.deserialize(job_data)
35
32
 
36
- def self.load(str)
37
- str&.constantize
33
+ job.perform_now
38
34
  end
39
- end)
35
+ end
40
36
 
41
37
  # Returns an ActiveRecord::Relation scope for enqueued jobs with a +scheduled_at+ that is either +null+ or in the
42
38
  # past. Jobs are ordered by +priority+ (+null+ last), then +scheduled_at+ (+null+ last), then +enqueued_at+.
@@ -48,7 +44,7 @@ class Marj < ActiveRecord::Base
48
44
  CASE WHEN priority IS NULL THEN 1 ELSE 0 END, priority,
49
45
  CASE WHEN scheduled_at IS NULL THEN 1 ELSE 0 END, scheduled_at,
50
46
  enqueued_at
51
- SQL
47
+ SQL
52
48
  )
53
49
  end
54
50
 
@@ -68,13 +64,56 @@ class Marj < ActiveRecord::Base
68
64
  end
69
65
  end
70
66
 
67
+ self.table_name = 'jobs'
68
+
69
+ # Order by +enqueued_at+ rather than +job_id+ (the default)
70
+ self.implicit_order_column = 'enqueued_at'
71
+
72
+ # Using a custom serializer for exception_executions so that we can interact with it as a hash rather than a string.
73
+ serialize(:exception_executions, coder: JSON)
74
+
75
+ # Using a custom serializer for arguments so that we can interact with as an array rather than a string.
76
+ # This enables code like:
77
+ # Marj.first.arguments.first
78
+ # Marj.first.update!(arguments: ['foo', 1, Time.now])
79
+ serialize(:arguments, coder: Class.new do
80
+ def self.dump(arguments)
81
+ return ActiveJob::Arguments.serialize(arguments).to_json if arguments.is_a?(Array)
82
+ return arguments if arguments.is_a?(String) || arguments.nil?
83
+
84
+ raise "invalid arguments: #{arguments}"
85
+ end
86
+
87
+ def self.load(arguments)
88
+ arguments ? ActiveJob::Arguments.deserialize(JSON.parse(arguments)) : nil
89
+ end
90
+ end)
91
+
92
+ # Using a custom serializer for job_class so that we can interact with it as a class rather than a string.
93
+ serialize(:job_class, coder: Class.new do
94
+ def self.dump(clazz)
95
+ return clazz.name if clazz.is_a?(Class)
96
+ return clazz if clazz.is_a?(String) || clazz.nil?
97
+
98
+ raise "invalid class: #{clazz}"
99
+ end
100
+
101
+ def self.load(str)
102
+ str&.constantize
103
+ end
104
+ end)
105
+
71
106
  # Registers job callbacks used to keep the database record for the specified job in sync.
72
107
  #
73
108
  # @param job [ActiveJob::Base]
74
109
  # @return [ActiveJob::Base]
75
110
  def self.register_callbacks(job, record)
76
- return if job.singleton_class.instance_variable_get(:@__marj)
111
+ raise 'callbacks already registered' if job.singleton_class.instance_variable_get(:@__marj)
77
112
 
113
+ # We need to detect three cases:
114
+ # - If a job succeeds, after_perform will be called.
115
+ # - If a job fails and should be retried, enqueue will be called. This is handled by the enqueue method.
116
+ # - If a job exceeds its max attempts, after_discard will be called.
78
117
  job.singleton_class.after_perform { |_j| record.destroy! }
79
118
  job.singleton_class.after_discard { |_j, _exception| record.destroy! }
80
119
  job.singleton_class.instance_variable_set(:@__marj, record)
@@ -89,22 +128,31 @@ class Marj < ActiveRecord::Base
89
128
  # @return [ActiveJob::Base] the enqueued job
90
129
  def self.enqueue(job, time = nil)
91
130
  job.scheduled_at = time
131
+ # Argument serialization is done by ActiveJob. ActiveRecord expects deserialized arguments.
92
132
  serialized = job.serialize.symbolize_keys!.without(:provider_job_id).merge(arguments: job.arguments)
133
+
134
+ # When a job is enqueued, we must create/update the corresponding database record. We also must ensure callbacks are
135
+ # registered on the job instance so that when the job is executed, the database record is deleted or updated
136
+ # (depending on the result).
137
+ #
138
+ # There are three cases:
139
+ # - The first time a job is enqueued, we need to create the record and register callbacks.
140
+ # - If a previously enqueued job instance is re-enqueued, for instance after execution fails, callbacks have
141
+ # already been registered. In this case we only need to update the record.
142
+ # - It is also possible for new job instance to be created for a job that is already in the database. In this case
143
+ # we need to update the record and register callbacks.
144
+ #
145
+ # We keep track of whether callbacks have been registered by setting the @__marj instance variable on the job's
146
+ # singleton class. This holds a reference to the record. This allows us to update the record without re-fetching it
147
+ # and also ensures that if execute is called on a record any updates to the database are reflected on that record
148
+ # instance.
93
149
  if (record = job.singleton_class.instance_variable_get(:@__marj))
94
150
  record.update!(serialized)
95
151
  else
96
- record = Marj.find_by(job_id: job.job_id)&.update!(serialized) || Marj.create!(serialized)
152
+ record = Marj.find_or_create_by!(job_id: job.job_id) { _1.assign_attributes(serialized) }
153
+ register_callbacks(job, record)
97
154
  end
98
- register_callbacks(job, record)
155
+ job
99
156
  end
100
157
  private_class_method :enqueue
101
-
102
- # Executes the job associated with this record and returns the result.
103
- def execute
104
- job = Marj.send(:register_callbacks, job_class.new, self)
105
- job_data = attributes.merge('arguments' => JSON.parse(read_attribute_before_type_cast(:arguments)))
106
- job_data = job_data.to_h { |k, v| [k, %w[enqueued_at scheduled_at].include?(k) ? v&.iso8601 : v] }
107
- job.deserialize(job_data)
108
- ActiveJob::Callbacks.run_callbacks(:execute) { job.perform_now }
109
- end
110
158
  end
data/lib/marj.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ Kernel.autoload(:Marj, File.expand_path('../app/models/marj.rb', __dir__))
4
+
3
5
  # ActiveJob queue adapter for Marj.
4
6
  class MarjAdapter
5
7
  # Enqueue a job for immediate execution.
@@ -19,6 +21,3 @@ class MarjAdapter
19
21
  Marj.send(:enqueue, job, timestamp ? Time.at(timestamp).utc : nil)
20
22
  end
21
23
  end
22
-
23
- # Enable auto-loading when running in Rails.
24
- class MarjEngine < Rails::Engine; end if defined?(Rails)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: marj
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Dower
@@ -48,14 +48,13 @@ files:
48
48
  - README.md
49
49
  - app/models/marj.rb
50
50
  - lib/marj.rb
51
- - lib/marj/record.rb
52
51
  homepage: https://github.com/nicholasdower/marj
53
52
  licenses:
54
53
  - MIT
55
54
  metadata:
56
55
  bug_tracker_uri: https://github.com/nicholasdower/marj/issues
57
- changelog_uri: https://github.com/nicholasdower/marj/releases/tag/v1.0.0
58
- documentation_uri: https://www.rubydoc.info/github/nicholasdower/marj/v1.0.0
56
+ changelog_uri: https://github.com/nicholasdower/marj/releases/tag/v1.1.0
57
+ documentation_uri: https://www.rubydoc.info/github/nicholasdower/marj/v1.1.0
59
58
  homepage_uri: https://github.com/nicholasdower/marj
60
59
  rubygems_mfa_required: 'true'
61
60
  source_code_uri: https://github.com/nicholasdower/marj
data/lib/marj/record.rb DELETED
@@ -1,3 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative '../../app/models/marj'