workhorse 1.2.24 → 1.3.0.rc1

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: 97943ac72325488d753fce90321c093208e3503b3071a79dba1495bcb347a22c
4
- data.tar.gz: b3d37af6f6f2910cf1fe96cf82bc445852fec78da2f5088dfdd665090b3f451e
3
+ metadata.gz: 59f7b29bdcd2e10a14e0be9393c2d28b68fcafe424e84b48c70c057e93866bda
4
+ data.tar.gz: 9450ab45109ba400b95bc3c20c77ba23552bd920dd9e328e4347eb22ec1acb36
5
5
  SHA512:
6
- metadata.gz: 5964079c67b84282e5b30cde7bc3af45554da948149bd41a8f0b8fbdffb0f60caa2ca6f7cd95652bd2a3df0dce10046b47bdf1104f2d85d69fcea16cc68d91a3
7
- data.tar.gz: 72691ebfd441c0314fb4f3e75fafedbd70b0f036a129cb0e14a54e2919d337e637a1b1adc3e8c8af6d05eb8deb0853e43ca8b7239addf6ae3df63a6b01e575a2
6
+ metadata.gz: 7d9b1a52e1fdfaa05e97ceb875a343f1f2cf9af4d9e971d4f24101ff4ba4f469485270cc231735cd1720cb8d524735e9767b566cebda6798a81bfbf9bd49f465
7
+ data.tar.gz: 849d677d9d9a807e11a72c903749dae9a5ef353bd37cce8b55fed1a8d3a78749d73aaa506ef79c363a5535976fca580a58ef2df90494835df3f3f7d9953d04e6
@@ -13,9 +13,6 @@ jobs:
13
13
  fail-fast: false
14
14
  matrix:
15
15
  ruby-version:
16
- - '2.5.1'
17
- - '2.6.2'
18
- - '2.7.1'
19
16
  - '3.0.1'
20
17
  - '3.1.2'
21
18
  - '3.2.1'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Workhorse Changelog
2
2
 
3
+ ## 1.3.0.rc1 - 2025-06-10
4
+
5
+ * Move internal development dependencies to Gemfile and lock version of
6
+ activejob
7
+
8
+ ## 1.3.0.rc0 - 2025-06-10
9
+
10
+ * Drop official support for Ruby < 3.0
11
+
12
+ * Skip `at_exit` handlers again (as introduced in 1.2.22) when exiting in
13
+ ShellHandler but still release lock file to fix issue originally fixed in
14
+ 1.2.23. This ensures compatibility with the `debug` gem, which would
15
+ otherwise hang when using the Workhorse shell handler.
16
+
17
+ Sitrox reference: #128333.
18
+
19
+ * Improve reliability of worker shutdown
20
+
21
+ * Improve reliability and efficiency of polling mechanism
22
+
23
+ * Improve reliability of automated tests
24
+
3
25
  ## 1.2.24 - 2024-10-21
4
26
 
5
27
  * Fix compatibility with ActiveJob 7.2.x
data/Gemfile CHANGED
@@ -2,3 +2,12 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify gem dependencies in the .gemspec file
4
4
  gemspec
5
+
6
+ gem 'bundler'
7
+ gem 'rake'
8
+ gem 'rubocop', '~> 1.28.0' # Latest version supported with Ruby 2.5
9
+ gem 'minitest'
10
+ gem 'mysql2'
11
+ gem 'benchmark-ips'
12
+ gem 'activejob', '~> 7.1.3'
13
+ gem 'pry'
data/README.md CHANGED
@@ -33,7 +33,7 @@ What it does not do:
33
33
 
34
34
  ### Requirements
35
35
 
36
- * Ruby `>= 2.0.0`
36
+ * Ruby `>= 3.0` (may work with earlier versions but is untested)
37
37
  * Rails `>= 3.2`
38
38
  * A database and table handler that properly supports row-level locking (such as
39
39
  MySQL with InnoDB, PostgreSQL, or Oracle).
data/Rakefile CHANGED
@@ -11,15 +11,6 @@ task :gemspec do
11
11
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
12
12
  spec.require_paths = ['lib']
13
13
 
14
- spec.add_development_dependency 'bundler'
15
- spec.add_development_dependency 'rake'
16
- spec.add_development_dependency 'rubocop', '~> 1.28.0' # Latest version supported with Ruby 2.5
17
- spec.add_development_dependency 'minitest'
18
- spec.add_development_dependency 'mysql2'
19
- spec.add_development_dependency 'colorize'
20
- spec.add_development_dependency 'benchmark-ips'
21
- spec.add_development_dependency 'activejob'
22
- spec.add_development_dependency 'pry'
23
14
  spec.add_dependency 'activesupport'
24
15
  spec.add_dependency 'activerecord'
25
16
  spec.add_dependency 'concurrent-ruby'
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.2.24
1
+ 1.3.0.rc1
@@ -19,32 +19,32 @@ module Workhorse
19
19
  begin
20
20
  case ARGV.first
21
21
  when 'start'
22
- exit daemon.start
22
+ status = daemon.start
23
23
  when 'stop'
24
- exit daemon.stop
24
+ status = daemon.stop
25
25
  when 'kill'
26
- exit daemon.stop(true)
26
+ status = daemon.stop(true)
27
27
  when 'status'
28
- exit daemon.status
28
+ status = daemon.status
29
29
  when 'watch'
30
- exit daemon.watch
30
+ status = daemon.watch
31
31
  when 'restart'
32
- exit daemon.restart
32
+ status = daemon.restart
33
33
  when 'restart-logging'
34
- exit daemon.restart_logging
34
+ status = daemon.restart_logging
35
35
  when 'usage'
36
36
  usage
37
- exit 99
37
+ status = 0
38
38
  else
39
39
  usage
40
+ status = 99
40
41
  end
41
-
42
- exit 0
43
42
  rescue StandardError => e
44
43
  warn "#{e.message}\n#{e.backtrace.join("\n")}"
45
- exit 99
44
+ status = 99
46
45
  ensure
47
46
  lockfile&.flock(File::LOCK_UN)
47
+ exit! status
48
48
  end
49
49
  end
50
50
 
@@ -206,9 +206,9 @@ module Workhorse
206
206
 
207
207
  timeout = [MIN_LOCK_TIMEOUT, [MAX_LOCK_TIMEOUT, worker.polling_interval].min].max
208
208
  with_global_lock timeout: timeout do
209
- job_ids = []
210
-
211
209
  Workhorse.tx_callback.call do
210
+ job_ids = []
211
+
212
212
  # As we are the only thread posting into the worker pool, it is safe to
213
213
  # get the number of idle threads without mutex synchronization. The
214
214
  # actual number of idle workers at time of posting can only be larger
@@ -225,9 +225,14 @@ module Workhorse
225
225
  job_ids << job.id
226
226
  end
227
227
  end
228
- end
229
228
 
230
- job_ids.each { |job_id| worker.perform(job_id) }
229
+ unless running?
230
+ worker.log 'Rolling back transaction to unlock jobs, as worker has been shut down in the meantime'
231
+ fail ActiveRecord::Rollback
232
+ end
233
+
234
+ job_ids.each { |job_id| worker.perform(job_id) }
235
+ end
231
236
  end
232
237
  end
233
238
 
@@ -358,8 +363,10 @@ module Workhorse
358
363
  # Get the names of all valid queues. The extra project here allows
359
364
  # selecting the last value in each row of the resulting array and getting
360
365
  # the queue name.
366
+ select.projections = []
361
367
  queues = select.project(:queue)
362
- return Workhorse::DbJob.find_by_sql(queues.to_sql).map(&:queue).uniq
368
+
369
+ return Workhorse::DbJob.connection.execute(queues.distinct.to_sql).to_a.flatten
363
370
  end
364
371
  end
365
372
  end
@@ -116,6 +116,9 @@ module Workhorse
116
116
  # final state this worker can be in.
117
117
  return if @state == :shutdown
118
118
 
119
+ # TODO: There is a race-condition with this shutdown:
120
+ # - If the poller is currently locking a job, it may call
121
+ # "worker.perform", which in turn tries to synchronize the same mutex.
119
122
  mutex.synchronize do
120
123
  assert_state! :running
121
124
 
@@ -2,7 +2,6 @@ require 'minitest/autorun'
2
2
  require 'active_record'
3
3
  require 'active_job'
4
4
  require 'pry'
5
- require 'colorize'
6
5
  require 'mysql2'
7
6
  require 'benchmark'
8
7
  require 'concurrent'
@@ -35,6 +34,7 @@ end
35
34
  class WorkhorseTest < ActiveSupport::TestCase
36
35
  def setup
37
36
  remove_pids!
37
+ clear_locks_and_db_threads!
38
38
  Workhorse.silence_watcher = true
39
39
  Workhorse::DbJob.delete_all
40
40
  end
@@ -43,6 +43,22 @@ class WorkhorseTest < ActiveSupport::TestCase
43
43
 
44
44
  attr_reader :daemon
45
45
 
46
+ def clear_locks_and_db_threads!
47
+ Workhorse::DbJob.connection.execute('SELECT RELEASE_ALL_LOCKS()')
48
+
49
+ pids = Workhorse::DbJob.connection.execute(<<~SQL.squish).to_a.flatten
50
+ SELECT ID FROM INFORMATION_SCHEMA.PROCESSLIST WHERE ID != CONNECTION_ID()
51
+ SQL
52
+
53
+ begin
54
+ pids.each { |pid| Workhorse::DbJob.connection.execute("KILL QUERY #{pid}") }
55
+ rescue ActiveRecord::StatementInvalid
56
+ # Ignore
57
+ end
58
+
59
+ Workhorse::DbJob.connection.execute('SELECT RELEASE_ALL_LOCKS()')
60
+ end
61
+
46
62
  def remove_pids!
47
63
  Dir[Rails.root.join('tmp', 'pids', '*')].each do |file|
48
64
  FileUtils.rm file
@@ -74,6 +90,7 @@ class WorkhorseTest < ActiveSupport::TestCase
74
90
  def work(time = 2, options = {})
75
91
  options[:pool_size] ||= 5
76
92
  options[:polling_interval] ||= 1
93
+ options[:auto_terminate] = options.fetch(:auto_terminate, false)
77
94
 
78
95
  with_worker(options) do
79
96
  sleep time
@@ -81,6 +98,8 @@ class WorkhorseTest < ActiveSupport::TestCase
81
98
  end
82
99
 
83
100
  def work_until(max: 50, interval: 0.1, **options, &block)
101
+ options[:auto_terminate] = options.fetch(:auto_terminate, false)
102
+
84
103
  w = Workhorse::Worker.new(**options)
85
104
  w.start
86
105
  return with_retries(max, interval: interval, &block)
@@ -143,6 +162,7 @@ ActiveRecord::Base.establish_connection(
143
162
  username: ENV.fetch('DB_USERNAME', nil) || 'root',
144
163
  password: ENV.fetch('DB_PASSWORD', nil) || '',
145
164
  host: ENV.fetch('DB_HOST', nil) || '127.0.0.1',
165
+ port: ENV.fetch('DB_PORT', nil) || 3306,
146
166
  pool: 10
147
167
  )
148
168
 
@@ -3,17 +3,13 @@ require 'test_helper'
3
3
  class Workhorse::DbJobTest < WorkhorseTest
4
4
  def test_reset_succeeded
5
5
  job = Workhorse.enqueue(BasicJob.new(sleep_time: 0))
6
- work 0.5
7
- job.reload
8
- assert_equal 'succeeded', job.state
9
-
6
+ work_until { assert_equal 'succeeded', job.reload.state }
10
7
  job.reset!
11
-
12
- assert_clean job
8
+ assert_clean job.reload
13
9
  end
14
10
 
15
11
  def test_reset_failed
16
- job = Workhorse.enqueue FailingTestJob
12
+ job = Workhorse.enqueue FailingTestJob.new
17
13
  work 0.5
18
14
  job.reload
19
15
  assert_equal 'failed', job.state
@@ -21,7 +21,7 @@ class Workhorse::PerformerTest < WorkhorseTest
21
21
  end
22
22
 
23
23
  def test_exception
24
- Workhorse.enqueue FailingTestJob
24
+ Workhorse.enqueue FailingTestJob.new
25
25
  work 0.2, polling_interval: 0.2
26
26
  assert_equal 'failed', Workhorse::DbJob.first.state
27
27
  end
@@ -219,26 +219,27 @@ class Workhorse::PollerTest < WorkhorseTest
219
219
  [true, false].each do |clean|
220
220
  Workhorse::DbJob.delete_all
221
221
 
222
- Workhorse.clean_stuck_jobs = true
222
+ Workhorse.clean_stuck_jobs = clean
223
223
  with_daemon do
224
224
  Workhorse.enqueue BasicJob.new(sleep_time: 5)
225
- sleep 0.2
226
- kill_deamon_workers
227
225
 
228
- assert_equal 1, Workhorse::DbJob.count
229
- assert_equal 'started', Workhorse::DbJob.first.state
226
+ with_retries do
227
+ assert_equal 'started', Workhorse::DbJob.first.state
228
+ end
230
229
 
231
- work 0.1 if clean
230
+ kill_deamon_workers
232
231
 
233
- assert_equal 1, Workhorse::DbJob.count
232
+ assert_equal 'started', Workhorse::DbJob.first.state
234
233
 
235
- Workhorse::DbJob.first.tap do |job|
236
- if clean
237
- assert_equal 'failed', job.state
238
- assert_match(/started by PID #{daemon.workers.first.pid}/, job.last_error)
239
- assert_match(/on host #{Socket.gethostname}/, job.last_error)
240
- else
241
- assert_equal 'started', job.state
234
+ work_until do
235
+ Workhorse::DbJob.first.tap do |job|
236
+ if clean
237
+ assert_equal 'failed', job.state
238
+ assert_match(/started by PID #{daemon.workers.first.pid}/, job.last_error)
239
+ assert_match(/on host #{Socket.gethostname}/, job.last_error)
240
+ else
241
+ assert_equal 'started', job.state
242
+ end
242
243
  end
243
244
  end
244
245
  end
data/workhorse.gemspec CHANGED
@@ -1,14 +1,14 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # stub: workhorse 1.2.24 ruby lib
2
+ # stub: workhorse 1.3.0.rc1 ruby lib
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "workhorse".freeze
6
- s.version = "1.2.24"
6
+ s.version = "1.3.0.rc1"
7
7
 
8
- s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
8
+ s.required_rubygems_version = Gem::Requirement.new("> 1.3.1".freeze) if s.respond_to? :required_rubygems_version=
9
9
  s.require_paths = ["lib".freeze]
10
10
  s.authors = ["Sitrox".freeze]
11
- s.date = "2024-10-21"
11
+ s.date = "2025-06-10"
12
12
  s.files = [".github/workflows/ruby.yml".freeze, ".gitignore".freeze, ".releaser_config".freeze, ".rubocop.yml".freeze, "CHANGELOG.md".freeze, "FAQ.md".freeze, "Gemfile".freeze, "LICENSE".freeze, "README.md".freeze, "RUBY_VERSION".freeze, "Rakefile".freeze, "VERSION".freeze, "bin/rubocop".freeze, "lib/active_job/queue_adapters/workhorse_adapter.rb".freeze, "lib/generators/workhorse/install_generator.rb".freeze, "lib/generators/workhorse/templates/bin/workhorse.rb".freeze, "lib/generators/workhorse/templates/config/initializers/workhorse.rb".freeze, "lib/generators/workhorse/templates/create_table_jobs.rb".freeze, "lib/workhorse.rb".freeze, "lib/workhorse/active_job_extension.rb".freeze, "lib/workhorse/daemon.rb".freeze, "lib/workhorse/daemon/shell_handler.rb".freeze, "lib/workhorse/db_job.rb".freeze, "lib/workhorse/enqueuer.rb".freeze, "lib/workhorse/jobs/cleanup_succeeded_jobs.rb".freeze, "lib/workhorse/jobs/detect_stale_jobs_job.rb".freeze, "lib/workhorse/jobs/run_active_job.rb".freeze, "lib/workhorse/jobs/run_rails_op.rb".freeze, "lib/workhorse/performer.rb".freeze, "lib/workhorse/poller.rb".freeze, "lib/workhorse/pool.rb".freeze, "lib/workhorse/scoped_env.rb".freeze, "lib/workhorse/worker.rb".freeze, "test/active_job/queue_adapters/workhorse_adapter_test.rb".freeze, "test/lib/db_schema.rb".freeze, "test/lib/jobs.rb".freeze, "test/lib/test_helper.rb".freeze, "test/workhorse/daemon_test.rb".freeze, "test/workhorse/db_job_test.rb".freeze, "test/workhorse/enqueuer_test.rb".freeze, "test/workhorse/performer_test.rb".freeze, "test/workhorse/poller_test.rb".freeze, "test/workhorse/pool_test.rb".freeze, "test/workhorse/worker_test.rb".freeze, "workhorse.gemspec".freeze]
13
13
  s.rubygems_version = "3.4.6".freeze
14
14
  s.summary = "Multi-threaded job backend with database queuing for ruby.".freeze
@@ -16,15 +16,6 @@ Gem::Specification.new do |s|
16
16
 
17
17
  s.specification_version = 4
18
18
 
19
- s.add_development_dependency(%q<bundler>.freeze, [">= 0"])
20
- s.add_development_dependency(%q<rake>.freeze, [">= 0"])
21
- s.add_development_dependency(%q<rubocop>.freeze, ["~> 1.28.0"])
22
- s.add_development_dependency(%q<minitest>.freeze, [">= 0"])
23
- s.add_development_dependency(%q<mysql2>.freeze, [">= 0"])
24
- s.add_development_dependency(%q<colorize>.freeze, [">= 0"])
25
- s.add_development_dependency(%q<benchmark-ips>.freeze, [">= 0"])
26
- s.add_development_dependency(%q<activejob>.freeze, [">= 0"])
27
- s.add_development_dependency(%q<pry>.freeze, [">= 0"])
28
19
  s.add_runtime_dependency(%q<activesupport>.freeze, [">= 0"])
29
20
  s.add_runtime_dependency(%q<activerecord>.freeze, [">= 0"])
30
21
  s.add_runtime_dependency(%q<concurrent-ruby>.freeze, [">= 0"])
metadata CHANGED
@@ -1,141 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: workhorse
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.24
4
+ version: 1.3.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sitrox
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-10-21 00:00:00.000000000 Z
10
+ date: 2025-06-10 00:00:00.000000000 Z
12
11
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: bundler
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :development
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '0'
27
- - !ruby/object:Gem::Dependency
28
- name: rake
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '0'
41
- - !ruby/object:Gem::Dependency
42
- name: rubocop
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: 1.28.0
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: 1.28.0
55
- - !ruby/object:Gem::Dependency
56
- name: minitest
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: '0'
69
- - !ruby/object:Gem::Dependency
70
- name: mysql2
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - ">="
74
- - !ruby/object:Gem::Version
75
- version: '0'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: '0'
83
- - !ruby/object:Gem::Dependency
84
- name: colorize
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- version: '0'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - ">="
95
- - !ruby/object:Gem::Version
96
- version: '0'
97
- - !ruby/object:Gem::Dependency
98
- name: benchmark-ips
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - ">="
102
- - !ruby/object:Gem::Version
103
- version: '0'
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - ">="
109
- - !ruby/object:Gem::Version
110
- version: '0'
111
- - !ruby/object:Gem::Dependency
112
- name: activejob
113
- requirement: !ruby/object:Gem::Requirement
114
- requirements:
115
- - - ">="
116
- - !ruby/object:Gem::Version
117
- version: '0'
118
- type: :development
119
- prerelease: false
120
- version_requirements: !ruby/object:Gem::Requirement
121
- requirements:
122
- - - ">="
123
- - !ruby/object:Gem::Version
124
- version: '0'
125
- - !ruby/object:Gem::Dependency
126
- name: pry
127
- requirement: !ruby/object:Gem::Requirement
128
- requirements:
129
- - - ">="
130
- - !ruby/object:Gem::Version
131
- version: '0'
132
- type: :development
133
- prerelease: false
134
- version_requirements: !ruby/object:Gem::Requirement
135
- requirements:
136
- - - ">="
137
- - !ruby/object:Gem::Version
138
- version: '0'
139
12
  - !ruby/object:Gem::Dependency
140
13
  name: activesupport
141
14
  requirement: !ruby/object:Gem::Requirement
@@ -178,8 +51,6 @@ dependencies:
178
51
  - - ">="
179
52
  - !ruby/object:Gem::Version
180
53
  version: '0'
181
- description:
182
- email:
183
54
  executables: []
184
55
  extensions: []
185
56
  extra_rdoc_files: []
@@ -229,10 +100,8 @@ files:
229
100
  - test/workhorse/pool_test.rb
230
101
  - test/workhorse/worker_test.rb
231
102
  - workhorse.gemspec
232
- homepage:
233
103
  licenses: []
234
104
  metadata: {}
235
- post_install_message:
236
105
  rdoc_options: []
237
106
  require_paths:
238
107
  - lib
@@ -243,12 +112,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
243
112
  version: '0'
244
113
  required_rubygems_version: !ruby/object:Gem::Requirement
245
114
  requirements:
246
- - - ">="
115
+ - - ">"
247
116
  - !ruby/object:Gem::Version
248
- version: '0'
117
+ version: 1.3.1
249
118
  requirements: []
250
- rubygems_version: 3.5.4
251
- signing_key:
119
+ rubygems_version: 3.6.8
252
120
  specification_version: 4
253
121
  summary: Multi-threaded job backend with database queuing for ruby.
254
122
  test_files: