marj 1.0.0 → 1.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: 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'