marj 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ ```
@@ -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
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../app/models/marj'
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: []