workhorse 1.5.2 → 2.0.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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +102 -0
  3. data/README.md +303 -74
  4. data/Rakefile +1 -0
  5. data/VERSION +1 -1
  6. data/bin/rubocop +5 -1
  7. data/lib/generators/workhorse/install_generator.rb +10 -1
  8. data/lib/generators/workhorse/templates/config/initializers/workhorse.rb +55 -0
  9. data/lib/generators/workhorse/templates/create_table_jobs.rb +11 -13
  10. data/lib/generators/workhorse/templates/create_table_workhorse_schedules.rb +29 -0
  11. data/lib/workhorse/daemon.rb +52 -6
  12. data/lib/workhorse/db_job.rb +58 -7
  13. data/lib/workhorse/enqueuer.rb +51 -8
  14. data/lib/workhorse/jobs/cleanup_succeeded_jobs.rb +26 -8
  15. data/lib/workhorse/jobs/detect_late_schedules_job.rb +59 -0
  16. data/lib/workhorse/notifiers/base.rb +55 -0
  17. data/lib/workhorse/notifiers/file_system.rb +66 -0
  18. data/lib/workhorse/notifiers/none.rb +8 -0
  19. data/lib/workhorse/notifiers/redis.rb +227 -0
  20. data/lib/workhorse/performer.rb +28 -0
  21. data/lib/workhorse/poller.rb +289 -47
  22. data/lib/workhorse/pool.rb +12 -6
  23. data/lib/workhorse/schedule.rb +288 -0
  24. data/lib/workhorse/schedules.rb +197 -0
  25. data/lib/workhorse/worker.rb +77 -27
  26. data/lib/workhorse.rb +136 -0
  27. data/test/lib/db_schema.rb +21 -1
  28. data/test/lib/jobs.rb +29 -0
  29. data/test/lib/test_helper.rb +9 -14
  30. data/test/workhorse/daemon_test.rb +33 -0
  31. data/test/workhorse/db_job_test.rb +1 -1
  32. data/test/workhorse/notifier_test.rb +500 -0
  33. data/test/workhorse/poller_test.rb +8 -4
  34. data/test/workhorse/schedule_test.rb +967 -0
  35. data/test/workhorse/worker_test.rb +92 -0
  36. data/workhorse.gemspec +6 -5
  37. metadata +29 -3
@@ -1,6 +1,98 @@
1
1
  require 'test_helper'
2
2
 
3
3
  class Workhorse::WorkerTest < WorkhorseTest
4
+ # Guards the interleaving that deadlocks the worker if #shutdown holds the
5
+ # mutex while waiting for the poller thread: that thread may be in #perform
6
+ # waiting for the same mutex, having just committed the lock on a job.
7
+ def test_shutdown_while_the_poller_posts_a_job
8
+ job = Workhorse.enqueue BasicJob.new(sleep_time: 0)
9
+ w = Workhorse::Worker.new(polling_interval: 60, pool_size: 1)
10
+
11
+ # As the poller would have, in a transaction that has already committed.
12
+ job.mark_locked!(w.id)
13
+
14
+ at_perform = Concurrent::Event.new
15
+ posted = Concurrent::Event.new
16
+
17
+ w.poller.define_singleton_method(:poll) do
18
+ next if @posted_once
19
+
20
+ @posted_once = true
21
+ at_perform.set
22
+ # Give the shutdown below time to get as far as waiting for this thread.
23
+ Kernel.sleep 0.3
24
+ worker.perform(job.id)
25
+ posted.set
26
+ end
27
+
28
+ w.start
29
+
30
+ assert at_perform.wait(5), 'poller did not reach the job posting'
31
+
32
+ Timeout.timeout(15) do
33
+ w.shutdown
34
+ end
35
+
36
+ assert posted.wait(5), 'poller thread never returned from #perform'
37
+
38
+ job.reload
39
+
40
+ assert_equal 'waiting', job.state
41
+ assert_nil job.locked_at
42
+ assert_nil job.locked_by
43
+ end
44
+
45
+ # #shutdown promises that running jobs have finished once it returns. Both
46
+ # TERM and INT are sent by the daemon's stop, so a repeat call has to keep
47
+ # that promise too rather than returning while the first one is still
48
+ # waiting for the pool.
49
+ def test_a_repeated_shutdown_waits_for_the_running_job
50
+ job = Workhorse.enqueue BasicJob.new(sleep_time: 2)
51
+ w = Workhorse::Worker.new(polling_interval: 0.2, pool_size: 1)
52
+ w.start
53
+
54
+ with_retries(60, interval: 0.05) { assert_equal 'started', job.reload.state }
55
+
56
+ states = Concurrent::Array.new
57
+ first = Thread.new { w.shutdown }
58
+ sleep 0.3
59
+ second = Thread.new do
60
+ w.shutdown
61
+ states << job.reload.state
62
+ end
63
+
64
+ Timeout.timeout(15) { second.join }
65
+
66
+ assert_equal ['succeeded'], states.to_a,
67
+ 'a repeated shutdown must not return while a job is still running'
68
+ assert_equal :shutdown, w.state
69
+ ensure
70
+ first&.join
71
+ end
72
+
73
+ def test_shutdown_of_a_worker_that_was_never_started
74
+ w = Workhorse::Worker.new(polling_interval: 60)
75
+
76
+ Timeout.timeout(5) { w.shutdown }
77
+
78
+ assert_equal :initialized, w.state
79
+ end
80
+
81
+ # A job that another worker already reset - or that never got locked - must
82
+ # be left alone rather than reset a second time.
83
+ def test_a_job_that_is_no_longer_locked_is_not_released
84
+ job = Workhorse.enqueue BasicJob.new(sleep_time: 0)
85
+ w = Workhorse::Worker.new(polling_interval: 60, pool_size: 1)
86
+ w.start
87
+ w.shutdown
88
+
89
+ job.update_columns(state: 'succeeded')
90
+
91
+ w.send(:release, job.id)
92
+
93
+ assert_equal 'succeeded', job.reload.state
94
+ end
95
+
4
96
  def test_idle
5
97
  with_worker(pool_size: 5, polling_interval: 0.2) do |w|
6
98
  assert_equal 5, w.idle
data/workhorse.gemspec CHANGED
@@ -1,24 +1,25 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # stub: workhorse 1.5.2 ruby lib
2
+ # stub: workhorse 2.0.0.rc0 ruby lib
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "workhorse".freeze
6
- s.version = "1.5.2".freeze
6
+ s.version = "2.0.0.rc0".freeze
7
7
 
8
8
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
9
9
  s.require_paths = ["lib".freeze]
10
10
  s.authors = ["Sitrox".freeze]
11
- s.date = "2026-08-04"
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, "test/workhorse/yjit_test.rb".freeze, "workhorse.gemspec".freeze]
11
+ s.date = "2026-09-29"
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/generators/workhorse/templates/create_table_workhorse_schedules.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_late_schedules_job.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/notifiers/base.rb".freeze, "lib/workhorse/notifiers/file_system.rb".freeze, "lib/workhorse/notifiers/none.rb".freeze, "lib/workhorse/notifiers/redis.rb".freeze, "lib/workhorse/performer.rb".freeze, "lib/workhorse/poller.rb".freeze, "lib/workhorse/pool.rb".freeze, "lib/workhorse/schedule.rb".freeze, "lib/workhorse/schedules.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/notifier_test.rb".freeze, "test/workhorse/performer_test.rb".freeze, "test/workhorse/poller_test.rb".freeze, "test/workhorse/pool_test.rb".freeze, "test/workhorse/schedule_test.rb".freeze, "test/workhorse/worker_test.rb".freeze, "test/workhorse/yjit_test.rb".freeze, "workhorse.gemspec".freeze]
13
13
  s.homepage = "https://github.com/sitrox/workhorse".freeze
14
14
  s.licenses = ["MIT".freeze]
15
15
  s.rubygems_version = "3.5.18".freeze
16
16
  s.summary = "Multi-threaded job backend with database queuing for ruby.".freeze
17
- s.test_files = ["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, "test/workhorse/yjit_test.rb".freeze]
17
+ s.test_files = ["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/notifier_test.rb".freeze, "test/workhorse/performer_test.rb".freeze, "test/workhorse/poller_test.rb".freeze, "test/workhorse/pool_test.rb".freeze, "test/workhorse/schedule_test.rb".freeze, "test/workhorse/worker_test.rb".freeze, "test/workhorse/yjit_test.rb".freeze]
18
18
 
19
19
  s.specification_version = 4
20
20
 
21
21
  s.add_runtime_dependency(%q<activesupport>.freeze, [">= 7.0.0".freeze])
22
22
  s.add_runtime_dependency(%q<activerecord>.freeze, [">= 7.0.0".freeze])
23
23
  s.add_runtime_dependency(%q<concurrent-ruby>.freeze, [">= 0".freeze])
24
+ s.add_runtime_dependency(%q<fugit>.freeze, [">= 0".freeze])
24
25
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: workhorse
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.2
4
+ version: 2.0.0.rc0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sitrox
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-08-04 00:00:00.000000000 Z
10
+ date: 2026-09-29 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activesupport
@@ -51,6 +51,20 @@ dependencies:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
53
  version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: fugit
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
54
68
  executables: []
55
69
  extensions: []
56
70
  extra_rdoc_files: []
@@ -73,6 +87,7 @@ files:
73
87
  - lib/generators/workhorse/templates/bin/workhorse.rb
74
88
  - lib/generators/workhorse/templates/config/initializers/workhorse.rb
75
89
  - lib/generators/workhorse/templates/create_table_jobs.rb
90
+ - lib/generators/workhorse/templates/create_table_workhorse_schedules.rb
76
91
  - lib/workhorse.rb
77
92
  - lib/workhorse/active_job_extension.rb
78
93
  - lib/workhorse/daemon.rb
@@ -80,12 +95,19 @@ files:
80
95
  - lib/workhorse/db_job.rb
81
96
  - lib/workhorse/enqueuer.rb
82
97
  - lib/workhorse/jobs/cleanup_succeeded_jobs.rb
98
+ - lib/workhorse/jobs/detect_late_schedules_job.rb
83
99
  - lib/workhorse/jobs/detect_stale_jobs_job.rb
84
100
  - lib/workhorse/jobs/run_active_job.rb
85
101
  - lib/workhorse/jobs/run_rails_op.rb
102
+ - lib/workhorse/notifiers/base.rb
103
+ - lib/workhorse/notifiers/file_system.rb
104
+ - lib/workhorse/notifiers/none.rb
105
+ - lib/workhorse/notifiers/redis.rb
86
106
  - lib/workhorse/performer.rb
87
107
  - lib/workhorse/poller.rb
88
108
  - lib/workhorse/pool.rb
109
+ - lib/workhorse/schedule.rb
110
+ - lib/workhorse/schedules.rb
89
111
  - lib/workhorse/scoped_env.rb
90
112
  - lib/workhorse/worker.rb
91
113
  - test/active_job/queue_adapters/workhorse_adapter_test.rb
@@ -95,9 +117,11 @@ files:
95
117
  - test/workhorse/daemon_test.rb
96
118
  - test/workhorse/db_job_test.rb
97
119
  - test/workhorse/enqueuer_test.rb
120
+ - test/workhorse/notifier_test.rb
98
121
  - test/workhorse/performer_test.rb
99
122
  - test/workhorse/poller_test.rb
100
123
  - test/workhorse/pool_test.rb
124
+ - test/workhorse/schedule_test.rb
101
125
  - test/workhorse/worker_test.rb
102
126
  - test/workhorse/yjit_test.rb
103
127
  - workhorse.gemspec
@@ -119,7 +143,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
119
143
  - !ruby/object:Gem::Version
120
144
  version: '0'
121
145
  requirements: []
122
- rubygems_version: 4.0.11
146
+ rubygems_version: 4.0.15
123
147
  specification_version: 4
124
148
  summary: Multi-threaded job backend with database queuing for ruby.
125
149
  test_files:
@@ -130,8 +154,10 @@ test_files:
130
154
  - test/workhorse/daemon_test.rb
131
155
  - test/workhorse/db_job_test.rb
132
156
  - test/workhorse/enqueuer_test.rb
157
+ - test/workhorse/notifier_test.rb
133
158
  - test/workhorse/performer_test.rb
134
159
  - test/workhorse/poller_test.rb
135
160
  - test/workhorse/pool_test.rb
161
+ - test/workhorse/schedule_test.rb
136
162
  - test/workhorse/worker_test.rb
137
163
  - test/workhorse/yjit_test.rb