marj 1.1.0 → 2.0.1
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 +134 -20
- data/lib/marj.rb +4 -19
- data/lib/marj_adapter.rb +23 -0
- data/lib/marj_config.rb +15 -0
- data/{app/models/marj.rb → lib/marj_record.rb} +8 -22
- metadata +10 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 42a85f9f7752849bf9e7781eef989294d45420763c16fb5fadee3df3fc7a0c2e
|
4
|
+
data.tar.gz: acadf4b197599bd3150629a557857b8d48fa8bed662a2f4140a8dbe16f01e7ba
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a30fe477c647ca284fdcde1bcccc8b000f3b4ef78b62de84f486376fa65450fcf3a52c7132df520fd45ee7e0f2856d8812a16b804ce89baa93a8d2353dc8348f
|
7
|
+
data.tar.gz: 72a63c15a36f6bade30e93bb5f84d5d8c6bbab33b12f4c92c78579f07eb86689e8e7609dae9ec16b94334140e8e4c2828b6e6cb19a580ad9a42730f27afab8cb
|
data/README.md
CHANGED
@@ -1,30 +1,61 @@
|
|
1
|
-
# Marj
|
1
|
+
# Marj - Minimal ActiveRecord Jobs
|
2
2
|
|
3
|
-
|
3
|
+
The simplest database-backed ActiveJob queueing backend.
|
4
|
+
|
5
|
+
## Quick Links
|
4
6
|
|
5
7
|
API docs: https://www.rubydoc.info/github/nicholasdower/marj <br>
|
6
8
|
RubyGems: https://rubygems.org/gems/marj <br>
|
7
9
|
Changelog: https://github.com/nicholasdower/marj/releases <br>
|
8
|
-
Issues: https://github.com/nicholasdower/marj/issues
|
10
|
+
Issues: https://github.com/nicholasdower/marj/issues <br>
|
11
|
+
Development: https://github.com/nicholasdower/marj/blob/master/CONTRIBUTING.md
|
9
12
|
|
10
|
-
|
13
|
+
## Features
|
11
14
|
|
12
|
-
-
|
13
|
-
-
|
15
|
+
- Enqueued jobs are written to the database.
|
16
|
+
- Successfully executed jobs are deleted from the database.
|
17
|
+
- Failed jobs which should be retried are updated in the database.
|
18
|
+
- Failed jobs which should not be retried are deleted from the database.
|
19
|
+
|
20
|
+
## Features Not Provided
|
21
|
+
|
22
|
+
- Workers
|
23
|
+
- Timeouts
|
24
|
+
- Concurrency Controls
|
25
|
+
- Observability
|
26
|
+
- A User Interace
|
27
|
+
- Anything else you might dream up.
|
28
|
+
|
29
|
+
## Interface
|
30
|
+
|
31
|
+
- `Marj` - An ActiveRecord model class
|
32
|
+
- `Marj.ready` - Used to retrieve jobs ready to be executed
|
33
|
+
- `Marj#execute` - Used to execute jobs retrieved from the database
|
34
|
+
- `MarjConfig.table_name=` - Used to override the default table name
|
14
35
|
|
15
36
|
## Setup
|
16
37
|
|
17
38
|
### 1. Install
|
18
39
|
|
19
|
-
|
40
|
+
```shell
|
41
|
+
bundle add marj
|
20
42
|
|
21
|
-
|
22
|
-
|
43
|
+
# or
|
44
|
+
|
45
|
+
gem install marj
|
23
46
|
```
|
24
47
|
|
25
|
-
### 2.
|
48
|
+
### 2. Configure
|
49
|
+
|
50
|
+
By default, the database table is named "jobs". To use a different table name:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
require 'marj'
|
54
|
+
|
55
|
+
MarjConfig.table_name = 'some_name'
|
56
|
+
```
|
26
57
|
|
27
|
-
|
58
|
+
### 3. Create the database table
|
28
59
|
|
29
60
|
```ruby
|
30
61
|
class CreateJobs < ActiveRecord::Migration[7.1]
|
@@ -53,7 +84,7 @@ class CreateJobs < ActiveRecord::Migration[7.1]
|
|
53
84
|
end
|
54
85
|
```
|
55
86
|
|
56
|
-
###
|
87
|
+
### 4. Configure the queue adapter
|
57
88
|
|
58
89
|
```ruby
|
59
90
|
require 'marj'
|
@@ -81,24 +112,107 @@ job.perform_now
|
|
81
112
|
|
82
113
|
# Enqueue, retrieve and manually run a job:
|
83
114
|
SomeJob.perform_later('foo')
|
84
|
-
|
85
|
-
record.execute
|
115
|
+
Marj.first.execute
|
86
116
|
|
87
|
-
# Run all
|
88
|
-
Marj.
|
117
|
+
# Run all ready jobs:
|
118
|
+
Marj.ready.each(&:execute)
|
89
119
|
|
90
|
-
# Run all
|
91
|
-
|
120
|
+
# Run all ready jobs, querying each time:
|
121
|
+
loop { Marj.ready.first&.tap(&:execute) || break }
|
92
122
|
|
93
|
-
# Run jobs
|
123
|
+
# Run all ready jobs in a specific queue:
|
124
|
+
loop { Marj.where(queue_name: 'foo').ready.first&.tap(&:execute) || break }
|
125
|
+
|
126
|
+
# Run jobs as they become ready:
|
94
127
|
loop do
|
95
|
-
Marj.
|
128
|
+
loop { Marj.ready.first&.tap(&:execute) || break }
|
129
|
+
rescue Exception => e
|
130
|
+
logger.error(e)
|
131
|
+
ensure
|
96
132
|
sleep 5.seconds
|
97
133
|
end
|
98
134
|
```
|
99
135
|
|
136
|
+
## Testing
|
137
|
+
|
138
|
+
By default, jobs enqeued during tests will be written to the database. Enqueued jobs can be executed via:
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
Marj.ready.each(&:execute)
|
142
|
+
```
|
143
|
+
|
144
|
+
Alternatively, to use [ActiveJob::QueueAdapters::TestAdapter](https://api.rubyonrails.org/classes/ActiveJob/QueueAdapters/TestAdapter.html):
|
145
|
+
```ruby
|
146
|
+
ActiveJob::Base.queue_adapter = :test
|
147
|
+
```
|
148
|
+
|
149
|
+
## Extension Examples
|
150
|
+
|
151
|
+
### Timeouts
|
152
|
+
|
153
|
+
```ruby
|
154
|
+
class ApplicationJob < ActiveJob::Base
|
155
|
+
def self.timeout_after(duration)
|
156
|
+
@timeout = duration
|
157
|
+
end
|
158
|
+
|
159
|
+
around_perform do |job, block|
|
160
|
+
if (timeout = job.class.instance_variable_get(:@timeout))
|
161
|
+
::Timeout.timeout(timeout, StandardError, 'execution expired') { block.call }
|
162
|
+
else
|
163
|
+
block.call
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
```
|
168
|
+
|
169
|
+
### Last Error
|
170
|
+
|
171
|
+
```ruby
|
172
|
+
class AddLastErrorToJobs < ActiveRecord::Migration[7.1]
|
173
|
+
def self.up
|
174
|
+
add_column :jobs, :last_error, :text
|
175
|
+
end
|
176
|
+
|
177
|
+
def self.down
|
178
|
+
remove_column :jobs, :last_error
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
class ApplicationJob < ActiveJob::Base
|
183
|
+
attr_reader :last_error
|
184
|
+
|
185
|
+
def last_error=(error)
|
186
|
+
if error.is_a?(Exception)
|
187
|
+
backtrace = error.backtrace&.map { |line| "\t#{line}" }&.join("\n")
|
188
|
+
error = backtrace ? "#{error.class}: #{error.message}\n#{backtrace}" : "#{error.class}: #{error.message}"
|
189
|
+
end
|
190
|
+
|
191
|
+
@last_error = error&.truncate(10_000, omission: '… (truncated)')
|
192
|
+
end
|
193
|
+
|
194
|
+
def set(options = {})
|
195
|
+
super.tap { self.last_error = options[:error] if options[:error] }
|
196
|
+
end
|
197
|
+
|
198
|
+
def serialize
|
199
|
+
super.merge('last_error' => @last_error)
|
200
|
+
end
|
201
|
+
|
202
|
+
def deserialize(job_data)
|
203
|
+
super.tap { self.last_error = job_data['last_error'] }
|
204
|
+
end
|
205
|
+
end
|
206
|
+
```
|
207
|
+
|
100
208
|
## ActiveJob Cheatsheet
|
101
209
|
|
210
|
+
For more information on ActiveJob, see:
|
211
|
+
|
212
|
+
- https://edgeguides.rubyonrails.org/active_job_basics.html
|
213
|
+
- https://www.rubydoc.info/gems/activejob
|
214
|
+
- https://github.com/nicholasdower/marj/blob/master/README.md#activejob-cheatsheet
|
215
|
+
|
102
216
|
### Configuring a Queue Adapter
|
103
217
|
|
104
218
|
```ruby
|
data/lib/marj.rb
CHANGED
@@ -1,23 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
# See https://github.com/nicholasdower/marj
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
# Enqueue a job for immediate execution.
|
8
|
-
#
|
9
|
-
# @param job [ActiveJob::Base] the job to enqueue
|
10
|
-
# @return [ActiveJob::Base] the enqueued job
|
11
|
-
def enqueue(job)
|
12
|
-
Marj.send(:enqueue, job)
|
13
|
-
end
|
5
|
+
require_relative 'marj_adapter'
|
6
|
+
require_relative 'marj_config'
|
14
7
|
|
15
|
-
|
16
|
-
#
|
17
|
-
# @param job [ActiveJob::Base] the job to enqueue
|
18
|
-
# @param timestamp [Numeric, NilClass] optional number of seconds since Unix epoch at which to execute the job
|
19
|
-
# @return [ActiveJob::Base] the enqueued job
|
20
|
-
def enqueue_at(job, timestamp)
|
21
|
-
Marj.send(:enqueue, job, timestamp ? Time.at(timestamp).utc : nil)
|
22
|
-
end
|
23
|
-
end
|
8
|
+
Kernel.autoload(:Marj, File.expand_path('marj_record.rb', __dir__))
|
data/lib/marj_adapter.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# ActiveJob queue adapter for Marj.
|
4
|
+
#
|
5
|
+
# See https://github.com/nicholasdower/marj
|
6
|
+
class MarjAdapter
|
7
|
+
# Enqueue a job for immediate execution.
|
8
|
+
#
|
9
|
+
# @param job [ActiveJob::Base] the job to enqueue
|
10
|
+
# @return [ActiveJob::Base] the enqueued job
|
11
|
+
def enqueue(job)
|
12
|
+
Marj.send(:enqueue, job)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Enqueue a job for execution at the specified time.
|
16
|
+
#
|
17
|
+
# @param job [ActiveJob::Base] the job to enqueue
|
18
|
+
# @param timestamp [Numeric, NilClass] optional number of seconds since Unix epoch at which to execute the job
|
19
|
+
# @return [ActiveJob::Base] the enqueued job
|
20
|
+
def enqueue_at(job, timestamp)
|
21
|
+
Marj.send(:enqueue, job, timestamp ? Time.at(timestamp).utc : nil)
|
22
|
+
end
|
23
|
+
end
|
data/lib/marj_config.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Marj configuration.
|
4
|
+
#
|
5
|
+
# See https://github.com/nicholasdower/marj
|
6
|
+
class MarjConfig
|
7
|
+
@table_name = 'jobs'
|
8
|
+
|
9
|
+
class << self
|
10
|
+
# The name of the database table. Defaults to "jobs".
|
11
|
+
#
|
12
|
+
# @return [String]
|
13
|
+
attr_accessor :table_name
|
14
|
+
end
|
15
|
+
end
|
@@ -2,17 +2,18 @@
|
|
2
2
|
|
3
3
|
require 'active_job'
|
4
4
|
require 'active_record'
|
5
|
+
require_relative 'marj_config'
|
5
6
|
|
6
|
-
# Marj
|
7
|
+
# The Marj ActiveRecord model class.
|
7
8
|
#
|
8
9
|
# See https://github.com/nicholasdower/marj
|
9
10
|
class Marj < ActiveRecord::Base
|
10
11
|
# The Marj version.
|
11
|
-
VERSION = '
|
12
|
+
VERSION = '2.0.1'
|
12
13
|
|
13
14
|
# Executes the job associated with this record and returns the result.
|
14
15
|
def execute
|
15
|
-
# Normally we would call ActiveJob::Base#execute which has the following
|
16
|
+
# Normally we would call ActiveJob::Base#execute which has the following implementation:
|
16
17
|
# ActiveJob::Callbacks.run_callbacks(:execute) do
|
17
18
|
# job = deserialize(job_data)
|
18
19
|
# job.perform_now
|
@@ -28,8 +29,8 @@ class Marj < ActiveRecord::Base
|
|
28
29
|
|
29
30
|
# ActiveJob::Base#deserialize expects dates to be strings rather than Time objects.
|
30
31
|
job_data = job_data.to_h { |k, v| [k, %w[enqueued_at scheduled_at].include?(k) ? v&.iso8601 : v] }
|
31
|
-
job.deserialize(job_data)
|
32
32
|
|
33
|
+
job.deserialize(job_data)
|
33
34
|
job.perform_now
|
34
35
|
end
|
35
36
|
end
|
@@ -38,7 +39,7 @@ class Marj < ActiveRecord::Base
|
|
38
39
|
# past. Jobs are ordered by +priority+ (+null+ last), then +scheduled_at+ (+null+ last), then +enqueued_at+.
|
39
40
|
#
|
40
41
|
# @return [ActiveRecord::Relation]
|
41
|
-
def self.
|
42
|
+
def self.ready
|
42
43
|
where('scheduled_at is null or scheduled_at <= ?', Time.now.utc).order(
|
43
44
|
Arel.sql(<<~SQL.squish)
|
44
45
|
CASE WHEN priority IS NULL THEN 1 ELSE 0 END, priority,
|
@@ -48,23 +49,7 @@ class Marj < ActiveRecord::Base
|
|
48
49
|
)
|
49
50
|
end
|
50
51
|
|
51
|
-
|
52
|
-
#
|
53
|
-
# @param source [Proc] a job source
|
54
|
-
# @return [NilClass]
|
55
|
-
def self.work_off(source = -> { Marj.available.first })
|
56
|
-
while (record = source.call)
|
57
|
-
executions = record.executions
|
58
|
-
begin
|
59
|
-
record.execute
|
60
|
-
rescue Exception
|
61
|
-
# The job should either be discarded or updated. Otherwise, something went wrong.
|
62
|
-
raise unless record.destroyed? || (record.executions == (executions + 1) && !record.changed?)
|
63
|
-
end
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
self.table_name = 'jobs'
|
52
|
+
self.table_name = MarjConfig.table_name
|
68
53
|
|
69
54
|
# Order by +enqueued_at+ rather than +job_id+ (the default)
|
70
55
|
self.implicit_order_column = 'enqueued_at'
|
@@ -117,6 +102,7 @@ class Marj < ActiveRecord::Base
|
|
117
102
|
job.singleton_class.after_perform { |_j| record.destroy! }
|
118
103
|
job.singleton_class.after_discard { |_j, _exception| record.destroy! }
|
119
104
|
job.singleton_class.instance_variable_set(:@__marj, record)
|
105
|
+
|
120
106
|
job
|
121
107
|
end
|
122
108
|
private_class_method :register_callbacks
|
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:
|
4
|
+
version: 2.0.1
|
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-
|
11
|
+
date: 2024-01-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activejob
|
@@ -38,7 +38,8 @@ dependencies:
|
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '7.1'
|
41
|
-
description: Minimal ActiveRecord
|
41
|
+
description: Marj (Minimal ActiveRecord Jobs) is the simplest database-backed ActiveJob
|
42
|
+
queueing backend.
|
42
43
|
email: nicholasdower@gmail.com
|
43
44
|
executables: []
|
44
45
|
extensions: []
|
@@ -46,15 +47,17 @@ extra_rdoc_files: []
|
|
46
47
|
files:
|
47
48
|
- LICENSE.txt
|
48
49
|
- README.md
|
49
|
-
- app/models/marj.rb
|
50
50
|
- lib/marj.rb
|
51
|
+
- lib/marj_adapter.rb
|
52
|
+
- lib/marj_config.rb
|
53
|
+
- lib/marj_record.rb
|
51
54
|
homepage: https://github.com/nicholasdower/marj
|
52
55
|
licenses:
|
53
56
|
- MIT
|
54
57
|
metadata:
|
55
58
|
bug_tracker_uri: https://github.com/nicholasdower/marj/issues
|
56
|
-
changelog_uri: https://github.com/nicholasdower/marj/releases/tag/
|
57
|
-
documentation_uri: https://www.rubydoc.info/github/nicholasdower/marj/
|
59
|
+
changelog_uri: https://github.com/nicholasdower/marj/releases/tag/v2.0.1
|
60
|
+
documentation_uri: https://www.rubydoc.info/github/nicholasdower/marj/v2.0.1
|
58
61
|
homepage_uri: https://github.com/nicholasdower/marj
|
59
62
|
rubygems_mfa_required: 'true'
|
60
63
|
source_code_uri: https://github.com/nicholasdower/marj
|
@@ -76,5 +79,5 @@ requirements: []
|
|
76
79
|
rubygems_version: 3.5.3
|
77
80
|
signing_key:
|
78
81
|
specification_version: 4
|
79
|
-
summary:
|
82
|
+
summary: Minimal ActiveRecord Jobs
|
80
83
|
test_files: []
|