marj 1.0.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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +215 -0
- data/app/models/marj.rb +110 -0
- data/lib/marj/record.rb +3 -0
- data/lib/marj.rb +24 -0
- metadata +81 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 5fd21edb773f1e25cc23e4099e7b0e6a3e2bb58485a988b063c37fdb71f31b82
|
4
|
+
data.tar.gz: aadc1963f4283a092a000bf0c8f732cd57a53f0ed69ad4c83c8c4dbd053907a3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7ddc1d33db20fb9d9ee3a587a67e66c54dcffb8d7f04dc366f53d40605b3239642c5466196e27debe86b1a8c74e10138557f2797232324c7f7a4d49b5df0134c
|
7
|
+
data.tar.gz: 04a4170a8e39b64648bf125da0f1fe1eae1e5888bd33412ff896dc0358f1a6c5b37bd4df1305fb9b2be715812318a49354b0eae1ccc756ceff65bf96879060cc
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2024 Nicholas Dower
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,215 @@
|
|
1
|
+
# Marj
|
2
|
+
|
3
|
+
Marj is a Minimal ActiveRecord-based Jobs library.
|
4
|
+
|
5
|
+
API docs: https://www.rubydoc.info/github/nicholasdower/marj <br>
|
6
|
+
RubyGems: https://rubygems.org/gems/marj <br>
|
7
|
+
Changelog: https://github.com/nicholasdower/marj/releases <br>
|
8
|
+
Issues: https://github.com/nicholasdower/marj/issues
|
9
|
+
|
10
|
+
For more information on ActiveJob, see:
|
11
|
+
|
12
|
+
- https://edgeguides.rubyonrails.org/active_job_basics.html
|
13
|
+
- https://www.rubydoc.info/gems/activejob
|
14
|
+
|
15
|
+
## Setup
|
16
|
+
|
17
|
+
### 1. Install
|
18
|
+
|
19
|
+
Add the following to your Gemfile:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
gem 'marj', '~> 1.0'
|
23
|
+
```
|
24
|
+
|
25
|
+
### 2. Create the jobs table
|
26
|
+
|
27
|
+
Apply a database migration:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
class CreateJobs < ActiveRecord::Migration[7.1]
|
31
|
+
def self.up
|
32
|
+
create_table :jobs, id: :string, primary_key: :job_id do |table|
|
33
|
+
table.string :job_class, null: false
|
34
|
+
table.text :arguments, null: false
|
35
|
+
table.string :queue_name, null: false
|
36
|
+
table.integer :priority
|
37
|
+
table.integer :executions, null: false
|
38
|
+
table.text :exception_executions, null: false
|
39
|
+
table.datetime :enqueued_at, null: false
|
40
|
+
table.datetime :scheduled_at
|
41
|
+
table.string :locale, null: false
|
42
|
+
table.string :timezone, null: false
|
43
|
+
end
|
44
|
+
|
45
|
+
add_index :jobs, %i[enqueued_at]
|
46
|
+
add_index :jobs, %i[scheduled_at]
|
47
|
+
add_index :jobs, %i[priority scheduled_at enqueued_at]
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.down
|
51
|
+
drop_table :jobs
|
52
|
+
end
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
56
|
+
### 3. Configure the queue adapter
|
57
|
+
|
58
|
+
If using Rails, configure the queue adapter via `Rails::Application`:
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
require 'marj'
|
62
|
+
|
63
|
+
# Configure via Rails::Application:
|
64
|
+
class MyApplication < Rails::Application
|
65
|
+
config.active_job.queue_adapter = :marj
|
66
|
+
end
|
67
|
+
|
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:
|
81
|
+
ActiveJob::Base.queue_adapter = :marj
|
82
|
+
|
83
|
+
# Or for specific jobs:
|
84
|
+
class SomeJob < ActiveJob::Base
|
85
|
+
self.queue_adapter = :marj
|
86
|
+
end
|
87
|
+
```
|
88
|
+
|
89
|
+
## Example Usage
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
# Enqueue and manually run a job:
|
93
|
+
job = SomeJob.perform_later('foo')
|
94
|
+
job.perform_now
|
95
|
+
|
96
|
+
# Enqueue, retrieve and manually run a job:
|
97
|
+
SomeJob.perform_later('foo')
|
98
|
+
Marj.first.execute
|
99
|
+
|
100
|
+
# Run all available jobs:
|
101
|
+
Marj.work_off
|
102
|
+
|
103
|
+
# Run jobs as they become available:
|
104
|
+
loop do
|
105
|
+
Marj.work_off
|
106
|
+
sleep 5.seconds
|
107
|
+
end
|
108
|
+
```
|
109
|
+
|
110
|
+
## ActiveJob Cheatsheet
|
111
|
+
|
112
|
+
### Configuring a Queue Adapter
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
# With Rails
|
116
|
+
class MyApplication < Rails::Application
|
117
|
+
config.active_job.queue_adapter = :foo # Instantiates FooAdapter
|
118
|
+
config.active_job.queue_adapter = FooAdapter.new # Uses FooAdapter directly
|
119
|
+
end
|
120
|
+
|
121
|
+
# Without Rails
|
122
|
+
ActiveJob::Base.queue_adapter = :foo # Instantiates FooAdapter
|
123
|
+
ActiveJob::Base.queue_adapter = FooAdapter.new # Uses FooAdapter directly
|
124
|
+
|
125
|
+
# Single Job
|
126
|
+
SomeJob.queue_adapter = :foo # Instantiates FooAdapter
|
127
|
+
SomeJob.queue_adapter = FooAdapter.new # Uses FooAdapter directly
|
128
|
+
```
|
129
|
+
|
130
|
+
## Configuration
|
131
|
+
|
132
|
+
- `config.active_job.default_queue_name`
|
133
|
+
- `config.active_job.queue_name_prefix`
|
134
|
+
- `config.active_job.queue_name_delimiter`
|
135
|
+
- `config.active_job.retry_jitter`
|
136
|
+
- `SomeJob.queue_name_prefix`
|
137
|
+
- `SomeJob.queue_name_delimiter`
|
138
|
+
- `SomeJob.retry_jitter`
|
139
|
+
- `SomeJob.queue_name`
|
140
|
+
- `SomeJob.queue_as`
|
141
|
+
|
142
|
+
### Options
|
143
|
+
|
144
|
+
- `:wait` - Enqueues the job with the specified delay
|
145
|
+
- `:wait_until` - Enqueues the job at the time specified
|
146
|
+
- `:queue` - Enqueues the job on the specified queue
|
147
|
+
- `:priority` - Enqueues the job with the specified priority
|
148
|
+
|
149
|
+
### Callbacks
|
150
|
+
|
151
|
+
- `SomeJob.before_enqueue`
|
152
|
+
- `SomeJob.after_enqueue`
|
153
|
+
- `SomeJob.around_enqueue`
|
154
|
+
- `SomeJob.before_perform`
|
155
|
+
- `SomeJob.after_perform`
|
156
|
+
- `SomeJob.around_perform`
|
157
|
+
- `ActiveJob::Callbacks.singleton_class.set_callback(:execute, :before, &block)`
|
158
|
+
- `ActiveJob::Callbacks.singleton_class.set_callback(:execute, :after, &block)`
|
159
|
+
- `ActiveJob::Callbacks.singleton_class.set_callback(:execute, :around, &block)`
|
160
|
+
|
161
|
+
## Handling Exceptions
|
162
|
+
|
163
|
+
- `SomeJob.retry_on`
|
164
|
+
- `SomeJob.discard_on`
|
165
|
+
- `SomeJob.after_discard`
|
166
|
+
|
167
|
+
### Creating Jobs
|
168
|
+
|
169
|
+
```ruby
|
170
|
+
# Create without enqueueing
|
171
|
+
job = SomeJob.new
|
172
|
+
job = SomeJob.new(args)
|
173
|
+
|
174
|
+
# Create and enqueue
|
175
|
+
job = SomeJob.perform_later
|
176
|
+
job = SomeJob.perform_later(args)
|
177
|
+
|
178
|
+
# Create and run (enqueued on failure)
|
179
|
+
SomeJob.perform_now
|
180
|
+
SomeJob.perform_now(args)
|
181
|
+
```
|
182
|
+
|
183
|
+
### Enqueueing Jobs
|
184
|
+
|
185
|
+
```ruby
|
186
|
+
SomeJob.new(args).enqueue
|
187
|
+
SomeJob.new(args).enqueue(options)
|
188
|
+
|
189
|
+
SomeJob.perform_later(args)
|
190
|
+
SomeJob.set(options).perform_later(args)
|
191
|
+
|
192
|
+
# Enqueued on failure
|
193
|
+
SomeJob.perform_now(args)
|
194
|
+
|
195
|
+
# Enqueue multiple
|
196
|
+
ActiveJob.perform_all_later(SomeJob.new, SomeJob.new)
|
197
|
+
ActiveJob.perform_all_later(SomeJob.new, SomeJob.new, options:)
|
198
|
+
|
199
|
+
# Enqueue multiple
|
200
|
+
SomeJob.set(options).perform_all_later(SomeJob.new, SomeJob.new)
|
201
|
+
SomeJob.set(options).perform_all_later(SomeJob.new, SomeJob.new, options:)
|
202
|
+
```
|
203
|
+
|
204
|
+
### Executing Jobs
|
205
|
+
|
206
|
+
```ruby
|
207
|
+
# Executed without enqueueing, enqueued on failure if retries configured
|
208
|
+
SomeJob.new(args).perform_now
|
209
|
+
SomeJob.perform_now(args)
|
210
|
+
ActiveJob::Base.exeucute(SomeJob.new(args).serialize)
|
211
|
+
|
212
|
+
# Executed after enqueueing
|
213
|
+
SomeJob.perform_later(args).perform_now
|
214
|
+
ActiveJob::Base.exeucute(SomeJob.perform_later(args).serialize)
|
215
|
+
```
|
data/app/models/marj.rb
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_record'
|
4
|
+
|
5
|
+
# Marj is a Minimal ActiveRecord-based Jobs library.
|
6
|
+
#
|
7
|
+
# See https://github.com/nicholasdower/marj
|
8
|
+
class Marj < ActiveRecord::Base
|
9
|
+
# The Marj version.
|
10
|
+
VERSION = '1.0.0'
|
11
|
+
|
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
|
23
|
+
|
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?
|
32
|
+
|
33
|
+
raise "invalid class: #{clazz}"
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.load(str)
|
37
|
+
str&.constantize
|
38
|
+
end
|
39
|
+
end)
|
40
|
+
|
41
|
+
# Returns an ActiveRecord::Relation scope for enqueued jobs with a +scheduled_at+ that is either +null+ or in the
|
42
|
+
# past. Jobs are ordered by +priority+ (+null+ last), then +scheduled_at+ (+null+ last), then +enqueued_at+.
|
43
|
+
#
|
44
|
+
# @return [ActiveRecord::Relation]
|
45
|
+
def self.available
|
46
|
+
where('scheduled_at is null or scheduled_at <= ?', Time.now.utc).order(
|
47
|
+
Arel.sql(<<~SQL.squish)
|
48
|
+
CASE WHEN priority IS NULL THEN 1 ELSE 0 END, priority,
|
49
|
+
CASE WHEN scheduled_at IS NULL THEN 1 ELSE 0 END, scheduled_at,
|
50
|
+
enqueued_at
|
51
|
+
SQL
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Executes any available jobs from the specified source.
|
56
|
+
#
|
57
|
+
# @param source [Proc] a job source
|
58
|
+
# @return [NilClass]
|
59
|
+
def self.work_off(source = -> { Marj.available.first })
|
60
|
+
while (record = source.call)
|
61
|
+
executions = record.executions
|
62
|
+
begin
|
63
|
+
record.execute
|
64
|
+
rescue Exception
|
65
|
+
# The job should either be discarded or updated. Otherwise, something went wrong.
|
66
|
+
raise unless record.destroyed? || (record.executions == (executions + 1) && !record.changed?)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Registers job callbacks used to keep the database record for the specified job in sync.
|
72
|
+
#
|
73
|
+
# @param job [ActiveJob::Base]
|
74
|
+
# @return [ActiveJob::Base]
|
75
|
+
def self.register_callbacks(job, record)
|
76
|
+
return if job.singleton_class.instance_variable_get(:@__marj)
|
77
|
+
|
78
|
+
job.singleton_class.after_perform { |_j| record.destroy! }
|
79
|
+
job.singleton_class.after_discard { |_j, _exception| record.destroy! }
|
80
|
+
job.singleton_class.instance_variable_set(:@__marj, record)
|
81
|
+
job
|
82
|
+
end
|
83
|
+
private_class_method :register_callbacks
|
84
|
+
|
85
|
+
# Enqueue a job for execution at the specified time.
|
86
|
+
#
|
87
|
+
# @param job [ActiveJob::Base] the job to enqueue
|
88
|
+
# @param time [Time, NilClass] optional time at which to execute the job
|
89
|
+
# @return [ActiveJob::Base] the enqueued job
|
90
|
+
def self.enqueue(job, time = nil)
|
91
|
+
job.scheduled_at = time
|
92
|
+
serialized = job.serialize.symbolize_keys!.without(:provider_job_id).merge(arguments: job.arguments)
|
93
|
+
if (record = job.singleton_class.instance_variable_get(:@__marj))
|
94
|
+
record.update!(serialized)
|
95
|
+
else
|
96
|
+
record = Marj.find_by(job_id: job.job_id)&.update!(serialized) || Marj.create!(serialized)
|
97
|
+
end
|
98
|
+
register_callbacks(job, record)
|
99
|
+
end
|
100
|
+
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
|
+
end
|
data/lib/marj/record.rb
ADDED
data/lib/marj.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# ActiveJob queue adapter for Marj.
|
4
|
+
class MarjAdapter
|
5
|
+
# Enqueue a job for immediate execution.
|
6
|
+
#
|
7
|
+
# @param job [ActiveJob::Base] the job to enqueue
|
8
|
+
# @return [ActiveJob::Base] the enqueued job
|
9
|
+
def enqueue(job)
|
10
|
+
Marj.send(:enqueue, job)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Enqueue a job for execution at the specified time.
|
14
|
+
#
|
15
|
+
# @param job [ActiveJob::Base] the job to enqueue
|
16
|
+
# @param timestamp [Numeric, NilClass] optional number of seconds since Unix epoch at which to execute the job
|
17
|
+
# @return [ActiveJob::Base] the enqueued job
|
18
|
+
def enqueue_at(job, timestamp)
|
19
|
+
Marj.send(:enqueue, job, timestamp ? Time.at(timestamp).utc : nil)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Enable auto-loading when running in Rails.
|
24
|
+
class MarjEngine < Rails::Engine; end if defined?(Rails)
|
metadata
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: marj
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Nick Dower
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-01-22 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activejob
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '7.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '7.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activerecord
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '7.1'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '7.1'
|
41
|
+
description: Minimal ActiveRecord-based Jobs library
|
42
|
+
email: nicholasdower@gmail.com
|
43
|
+
executables: []
|
44
|
+
extensions: []
|
45
|
+
extra_rdoc_files: []
|
46
|
+
files:
|
47
|
+
- LICENSE.txt
|
48
|
+
- README.md
|
49
|
+
- app/models/marj.rb
|
50
|
+
- lib/marj.rb
|
51
|
+
- lib/marj/record.rb
|
52
|
+
homepage: https://github.com/nicholasdower/marj
|
53
|
+
licenses:
|
54
|
+
- MIT
|
55
|
+
metadata:
|
56
|
+
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
|
59
|
+
homepage_uri: https://github.com/nicholasdower/marj
|
60
|
+
rubygems_mfa_required: 'true'
|
61
|
+
source_code_uri: https://github.com/nicholasdower/marj
|
62
|
+
post_install_message:
|
63
|
+
rdoc_options: []
|
64
|
+
require_paths:
|
65
|
+
- lib
|
66
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: 2.7.0
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
requirements: []
|
77
|
+
rubygems_version: 3.5.3
|
78
|
+
signing_key:
|
79
|
+
specification_version: 4
|
80
|
+
summary: An ActiveJob queuing backend backed by ActiveRecord.
|
81
|
+
test_files: []
|