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 +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