marj 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|