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 +4 -4
- data/README.md +12 -22
- data/app/models/marj.rb +86 -38
- data/lib/marj.rb +2 -3
- metadata +3 -4
- data/lib/marj/record.rb +0 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f5fe8134484360b68c0cf69356f6a037897c6896be81bc393a9dfc0fb0684055
|
4
|
+
data.tar.gz: 4b7575b721d05681876846f3f06d47440d366bc875e02cadeae7de4582270114
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
#
|
61
|
+
# With rails:
|
64
62
|
class MyApplication < Rails::Application
|
65
63
|
config.active_job.queue_adapter = :marj
|
66
64
|
end
|
67
65
|
|
68
|
-
#
|
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
|
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
|
-
|
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
|
-
|
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.
|
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.
|
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.
|
11
|
+
VERSION = '1.1.0'
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
34
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|
-
|
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.
|
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.
|
58
|
-
documentation_uri: https://www.rubydoc.info/github/nicholasdower/marj/v1.
|
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