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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f5fe8134484360b68c0cf69356f6a037897c6896be81bc393a9dfc0fb0684055
4
- data.tar.gz: 4b7575b721d05681876846f3f06d47440d366bc875e02cadeae7de4582270114
3
+ metadata.gz: 42a85f9f7752849bf9e7781eef989294d45420763c16fb5fadee3df3fc7a0c2e
4
+ data.tar.gz: acadf4b197599bd3150629a557857b8d48fa8bed662a2f4140a8dbe16f01e7ba
5
5
  SHA512:
6
- metadata.gz: dbfa1cd4cfb90ca4c47b039f0f320e5cc847aa9863b640cd52b445f5074c13aaa0f2679f4ec950001a086f0b981f58f6aa180a250a4790753691b249dc4fa19f
7
- data.tar.gz: d1903bcb55c22522d270d950b316ee99ce05edca0c279cc055cb39a71ecfd389d02acd5d1bcc029e931636e26c4708bfd872a20ab71ecc9328e69928a3b535c2
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
- Marj is a Minimal ActiveRecord-based Jobs library.
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
- For more information on ActiveJob, see:
13
+ ## Features
11
14
 
12
- - https://edgeguides.rubyonrails.org/active_job_basics.html
13
- - https://www.rubydoc.info/gems/activejob
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
- Add the following to your Gemfile:
40
+ ```shell
41
+ bundle add marj
20
42
 
21
- ```ruby
22
- gem 'marj', '~> 1.0'
43
+ # or
44
+
45
+ gem install marj
23
46
  ```
24
47
 
25
- ### 2. Create the jobs table
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
- Apply a database migration:
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
- ### 3. Configure the queue adapter
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
- record = Marj.first
85
- record.execute
115
+ Marj.first.execute
86
116
 
87
- # Run all available jobs:
88
- Marj.work_off
117
+ # Run all ready jobs:
118
+ Marj.ready.each(&:execute)
89
119
 
90
- # Run all available jobs in a specific queue:
91
- Marj.work_off(source = -> { Marj.where(queue_name: 'foo').available.first })
120
+ # Run all ready jobs, querying each time:
121
+ loop { Marj.ready.first&.tap(&:execute) || break }
92
122
 
93
- # Run jobs as they become available:
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.work_off
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
- Kernel.autoload(:Marj, File.expand_path('../app/models/marj.rb', __dir__))
3
+ # See https://github.com/nicholasdower/marj
4
4
 
5
- # ActiveJob queue adapter for 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
5
+ require_relative 'marj_adapter'
6
+ require_relative 'marj_config'
14
7
 
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
8
+ Kernel.autoload(:Marj, File.expand_path('marj_record.rb', __dir__))
@@ -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
@@ -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 is a Minimal ActiveRecord-based Jobs library.
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 = '1.1.0'
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 implemenation:
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.available
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
- # Executes any available jobs from the specified source.
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: 1.1.0
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-22 00:00:00.000000000 Z
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-based Jobs library
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/v1.1.0
57
- documentation_uri: https://www.rubydoc.info/github/nicholasdower/marj/v1.1.0
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: An ActiveJob queuing backend backed by ActiveRecord.
82
+ summary: Minimal ActiveRecord Jobs
80
83
  test_files: []