workhorse 1.2.24 → 1.3.0.rc0

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: 85aa56534b17816d2256e85b1929397d7cab79c05bd25aa3efeef6ae538bb7ae
4
+ data.tar.gz: 9e7b16ff371b8113e2de2b475c71512d2fca292873bb7a66b9905107e86a5f4d
5
5
  SHA512:
6
- metadata.gz: 5964079c67b84282e5b30cde7bc3af45554da948149bd41a8f0b8fbdffb0f60caa2ca6f7cd95652bd2a3df0dce10046b47bdf1104f2d85d69fcea16cc68d91a3
7
- data.tar.gz: 72691ebfd441c0314fb4f3e75fafedbd70b0f036a129cb0e14a54e2919d337e637a1b1adc3e8c8af6d05eb8deb0853e43ca8b7239addf6ae3df63a6b01e575a2
6
+ metadata.gz: dbc294e830bc9a594844055de6c392401bf7e18871d078da60580b5b5d7c3c9cc72ca9cc5abd846aaa3a6f5b18c85b80b097b0a94ff68759fcb166f9ed33eff6
7
+ data.tar.gz: 7f1454dd8a7dd0ad2d491050b6b5e1fdbee429cfe99fcfdf5518590bcb1e9e372500c5a3f0c8b2c4faeaa6b277d0304c2547f0e995af9d53ce82cdbdfe24871e
@@ -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,22 @@
1
1
  # Workhorse Changelog
2
2
 
3
+ ## 1.3.0.rc0 - 2025-06-10
4
+
5
+ * Drop official support for Ruby < 3.0
6
+
7
+ * Skip `at_exit` handlers again (as introduced in 1.2.22) when exiting in
8
+ ShellHandler but still release lock file to fix issue originally fixed in
9
+ 1.2.23. This ensures compatibility with the `debug` gem, which would
10
+ otherwise hang when using the Workhorse shell handler.
11
+
12
+ Sitrox reference: #128333.
13
+
14
+ * Improve reliability of worker shutdown
15
+
16
+ * Improve reliability and efficiency of polling mechanism
17
+
18
+ * Improve reliability of automated tests
19
+
3
20
  ## 1.2.24 - 2024-10-21
4
21
 
5
22
  * Fix compatibility with ActiveJob 7.2.x
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
@@ -16,7 +16,6 @@ task :gemspec do
16
16
  spec.add_development_dependency 'rubocop', '~> 1.28.0' # Latest version supported with Ruby 2.5
17
17
  spec.add_development_dependency 'minitest'
18
18
  spec.add_development_dependency 'mysql2'
19
- spec.add_development_dependency 'colorize'
20
19
  spec.add_development_dependency 'benchmark-ips'
21
20
  spec.add_development_dependency 'activejob'
22
21
  spec.add_development_dependency 'pry'
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.2.24
1
+ 1.3.0.rc0
@@ -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.rc0 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.rc0"
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
@@ -21,7 +21,6 @@ Gem::Specification.new do |s|
21
21
  s.add_development_dependency(%q<rubocop>.freeze, ["~> 1.28.0"])
22
22
  s.add_development_dependency(%q<minitest>.freeze, [">= 0"])
23
23
  s.add_development_dependency(%q<mysql2>.freeze, [">= 0"])
24
- s.add_development_dependency(%q<colorize>.freeze, [">= 0"])
25
24
  s.add_development_dependency(%q<benchmark-ips>.freeze, [">= 0"])
26
25
  s.add_development_dependency(%q<activejob>.freeze, [">= 0"])
27
26
  s.add_development_dependency(%q<pry>.freeze, [">= 0"])
metadata CHANGED
@@ -1,14 +1,13 @@
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.rc0
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
12
  - !ruby/object:Gem::Dependency
14
13
  name: bundler
@@ -80,20 +79,6 @@ dependencies:
80
79
  - - ">="
81
80
  - !ruby/object:Gem::Version
82
81
  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
82
  - !ruby/object:Gem::Dependency
98
83
  name: benchmark-ips
99
84
  requirement: !ruby/object:Gem::Requirement
@@ -178,8 +163,6 @@ dependencies:
178
163
  - - ">="
179
164
  - !ruby/object:Gem::Version
180
165
  version: '0'
181
- description:
182
- email:
183
166
  executables: []
184
167
  extensions: []
185
168
  extra_rdoc_files: []
@@ -229,10 +212,8 @@ files:
229
212
  - test/workhorse/pool_test.rb
230
213
  - test/workhorse/worker_test.rb
231
214
  - workhorse.gemspec
232
- homepage:
233
215
  licenses: []
234
216
  metadata: {}
235
- post_install_message:
236
217
  rdoc_options: []
237
218
  require_paths:
238
219
  - lib
@@ -243,12 +224,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
243
224
  version: '0'
244
225
  required_rubygems_version: !ruby/object:Gem::Requirement
245
226
  requirements:
246
- - - ">="
227
+ - - ">"
247
228
  - !ruby/object:Gem::Version
248
- version: '0'
229
+ version: 1.3.1
249
230
  requirements: []
250
- rubygems_version: 3.5.4
251
- signing_key:
231
+ rubygems_version: 3.6.8
252
232
  specification_version: 4
253
233
  summary: Multi-threaded job backend with database queuing for ruby.
254
234
  test_files: