marj 1.1.0 → 2.0.1

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