workhorse 1.5.2 → 2.0.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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +137 -1
  3. data/CHANGELOG.md +150 -0
  4. data/Gemfile +16 -1
  5. data/README.md +316 -72
  6. data/Rakefile +1 -0
  7. data/VERSION +1 -1
  8. data/bin/rubocop +5 -1
  9. data/lib/generators/workhorse/install_generator.rb +10 -1
  10. data/lib/generators/workhorse/templates/config/initializers/workhorse.rb +55 -0
  11. data/lib/generators/workhorse/templates/create_table_jobs.rb +15 -2
  12. data/lib/generators/workhorse/templates/create_table_workhorse_schedules.rb +42 -0
  13. data/lib/workhorse/daemon/shell_handler.rb +4 -1
  14. data/lib/workhorse/daemon.rb +57 -7
  15. data/lib/workhorse/db_job.rb +98 -8
  16. data/lib/workhorse/enqueuer.rb +51 -8
  17. data/lib/workhorse/jobs/cleanup_succeeded_jobs.rb +26 -8
  18. data/lib/workhorse/jobs/detect_late_schedules_job.rb +59 -0
  19. data/lib/workhorse/notifiers/base.rb +55 -0
  20. data/lib/workhorse/notifiers/file_system.rb +66 -0
  21. data/lib/workhorse/notifiers/none.rb +8 -0
  22. data/lib/workhorse/notifiers/redis.rb +227 -0
  23. data/lib/workhorse/performer.rb +29 -2
  24. data/lib/workhorse/poller.rb +303 -21
  25. data/lib/workhorse/pool.rb +12 -6
  26. data/lib/workhorse/schedule.rb +288 -0
  27. data/lib/workhorse/schedules.rb +197 -0
  28. data/lib/workhorse/worker.rb +102 -31
  29. data/lib/workhorse.rb +136 -0
  30. data/test/lib/db_schema.rb +36 -3
  31. data/test/lib/jobs.rb +29 -0
  32. data/test/lib/test_helper.rb +113 -20
  33. data/test/workhorse/daemon_test.rb +33 -0
  34. data/test/workhorse/db_job_test.rb +2 -4
  35. data/test/workhorse/notifier_test.rb +487 -0
  36. data/test/workhorse/performer_test.rb +7 -9
  37. data/test/workhorse/poller_test.rb +97 -23
  38. data/test/workhorse/schedule_test.rb +967 -0
  39. data/test/workhorse/worker_test.rb +201 -76
  40. data/workhorse.gemspec +6 -5
  41. metadata +29 -3
@@ -1,18 +1,107 @@
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
7
99
 
8
- sleep 0.05
9
- Workhorse.enqueue BasicJob.new(sleep_time: 0.2)
100
+ # Long enough to be seen running at the 0.1s the retries look.
101
+ Workhorse.enqueue BasicJob.new(sleep_time: 1)
10
102
 
11
- sleep 0.25
12
- assert_equal 4, w.idle
13
-
14
- sleep 0.2
15
- assert_equal 5, w.idle
103
+ with_retries { assert_equal 4, w.idle }
104
+ with_retries { assert_equal 5, w.idle }
16
105
  end
17
106
  end
18
107
 
@@ -30,42 +119,37 @@ class Workhorse::WorkerTest < WorkhorseTest
30
119
  end
31
120
 
32
121
  def test_perform
33
- with_worker(polling_interval: 0.2) do
34
- sleep 0.1
35
- Workhorse.enqueue BasicJob.new(sleep_time: 0.1)
36
- assert_equal 'waiting', Workhorse::DbJob.first.state
122
+ # Enqueued before the worker starts, as a running one may pick the job up
123
+ # before its state can be looked at.
124
+ Workhorse.enqueue BasicJob.new(sleep_time: 0.1)
125
+ assert_equal 'waiting', Workhorse::DbJob.first.state
37
126
 
38
- sleep 0.3
127
+ work_until(pool_size: 5, polling_interval: 0.2) do
128
+ assert_equal 'succeeded', Workhorse::DbJob.first.state
39
129
  end
40
-
41
- assert_equal 'succeeded', Workhorse::DbJob.first.state
42
130
  end
43
131
 
44
132
  def test_params
45
133
  BasicJob.results.clear
46
134
 
47
135
  Workhorse.enqueue BasicJob.new(some_param: 5, sleep_time: 0)
48
- work 0.5
49
-
50
- assert_equal 'succeeded', Workhorse::DbJob.first.state
136
+ work_until(pool_size: 5, polling_interval: 0.2) { assert_equal 'succeeded', Workhorse::DbJob.first.state }
51
137
 
52
138
  assert_equal 1, BasicJob.results.count
53
139
  assert_equal 5, BasicJob.results.first
54
140
  end
55
141
 
56
142
  def test_term
57
- with_worker(polling_interval: 0.2) do |w|
143
+ with_worker(polling_interval: 0.2, auto_terminate: true) do |w|
58
144
  Process.kill 'TERM', Process.pid
59
- sleep 0.2
60
- w.assert_state! :shutdown
145
+ with_retries { w.assert_state! :shutdown }
61
146
  end
62
147
  end
63
148
 
64
149
  def test_int
65
- with_worker(polling_interval: 0.2) do |w|
150
+ with_worker(polling_interval: 0.2, auto_terminate: true) do |w|
66
151
  Process.kill 'INT', Process.pid
67
- sleep 0.2
68
- w.assert_state! :shutdown
152
+ with_retries { w.assert_state! :shutdown }
69
153
  end
70
154
  end
71
155
 
@@ -82,6 +166,36 @@ class Workhorse::WorkerTest < WorkhorseTest
82
166
  FileUtils.rm_f Workhorse::Worker.shutdown_file_for(Process.pid)
83
167
  end
84
168
 
169
+ # A fresh checkout or deployment has no tmp/pids. The soft restart touches
170
+ # its shutdown file there after it has stopped accepting jobs, so failing on
171
+ # it used to leave a worker that took no jobs and never exited.
172
+ def test_soft_restart_without_a_pids_directory
173
+ FileUtils.rm_rf(Rails.root.join('tmp', 'pids'))
174
+
175
+ with_worker(pool_size: 1, polling_interval: 0.2) do |w|
176
+ Process.kill 'USR1', Process.pid
177
+
178
+ with_retries { assert_equal :shutdown, w.state }
179
+ assert File.exist?(Workhorse::Worker.shutdown_file_for(Process.pid))
180
+ end
181
+ ensure
182
+ FileUtils.mkdir_p(Rails.root.join('tmp', 'pids'))
183
+ FileUtils.rm_f Workhorse::Worker.shutdown_file_for(Process.pid)
184
+ end
185
+
186
+ def test_heartbeat_without_a_pids_directory
187
+ FileUtils.rm_rf(Rails.root.join('tmp', 'pids'))
188
+ ENV['WORKHORSE_DAEMON_WORKER_ID'] = '42'
189
+
190
+ Workhorse::Worker.new.heartbeat!
191
+
192
+ assert File.exist?(Workhorse::Worker.heartbeat_file_for('42'))
193
+ ensure
194
+ ENV.delete('WORKHORSE_DAEMON_WORKER_ID')
195
+ FileUtils.mkdir_p(Rails.root.join('tmp', 'pids'))
196
+ FileUtils.rm_f(Workhorse::Worker.heartbeat_file_for('42'))
197
+ end
198
+
85
199
  def test_soft_restart_when_busy_waits_for_job
86
200
  with_worker(pool_size: 1, polling_interval: 0.2) do |w|
87
201
  Workhorse.enqueue BasicJob.new(sleep_time: 0.5)
@@ -144,7 +258,7 @@ class Workhorse::WorkerTest < WorkhorseTest
144
258
  end
145
259
 
146
260
  def test_soft_restart_ignored_during_shutdown
147
- with_worker(pool_size: 1, polling_interval: 0.2) do |w|
261
+ with_worker(pool_size: 1, polling_interval: 0.2, auto_terminate: true) do |w|
148
262
  Process.kill 'TERM', Process.pid
149
263
  with_retries { assert_equal :shutdown, w.state }
150
264
 
@@ -160,80 +274,61 @@ class Workhorse::WorkerTest < WorkhorseTest
160
274
 
161
275
  def test_no_queues
162
276
  enqueue_in_multiple_queues
163
- work 0.2, polling_interval: 0.2
164
277
 
165
- jobs = Workhorse::DbJob.order(queue: :asc).to_a
166
- assert_equal 'succeeded', jobs[0].state
167
- assert_equal 'succeeded', jobs[1].state
168
- assert_equal 'succeeded', jobs[2].state
278
+ work_until(pool_size: 5, polling_interval: 0.2) do
279
+ assert_equal %w[succeeded succeeded succeeded], jobs_by_queue.map(&:state)
280
+ end
169
281
  end
170
282
 
171
283
  def test_nil_queue
172
284
  enqueue_in_multiple_queues
173
- work 0.2, queues: [nil], polling_interval: 0.2
174
285
 
175
- jobs = Workhorse::DbJob.order(queue: :asc).to_a
176
- assert_equal 'succeeded', jobs[0].state
177
- assert_equal 'waiting', jobs[1].state
178
- assert_equal 'waiting', jobs[2].state
286
+ # A job the worker wrongly took would be taken in the same poll as the
287
+ # one it rightly took, so is locked by the time that one has succeeded.
288
+ work_until(queues: [nil], pool_size: 5, polling_interval: 0.2) do
289
+ assert_equal %w[succeeded waiting waiting], jobs_by_queue.map(&:state)
290
+ end
179
291
  end
180
292
 
181
293
  def test_queues_with_nil
182
294
  enqueue_in_multiple_queues
183
- work 0.2, queues: [nil, :q1], polling_interval: 0.2
184
295
 
185
- jobs = Workhorse::DbJob.order(queue: :asc).to_a
186
- assert_equal 'succeeded', jobs[0].state
187
- assert_equal 'succeeded', jobs[1].state
188
- assert_equal 'waiting', jobs[2].state
296
+ work_until(queues: [nil, :q1], pool_size: 5, polling_interval: 0.2) do
297
+ assert_equal %w[succeeded succeeded waiting], jobs_by_queue.map(&:state)
298
+ end
189
299
  end
190
300
 
191
301
  def test_queues_without_nil
192
302
  enqueue_in_multiple_queues
193
- work 0.2, queues: %i[q1 q2], polling_interval: 0.2
194
303
 
195
- jobs = Workhorse::DbJob.order(queue: :asc).to_a
196
- assert_equal 'waiting', jobs[0].state
197
- assert_equal 'succeeded', jobs[1].state
198
- assert_equal 'succeeded', jobs[2].state
304
+ work_until(queues: %i[q1 q2], pool_size: 5, polling_interval: 0.2) do
305
+ assert_equal %w[waiting succeeded succeeded], jobs_by_queue.map(&:state)
306
+ end
199
307
  end
200
308
 
201
309
  def test_queue_not_parallel
202
310
  Workhorse::DbJob.delete_all
203
- Workhorse.enqueue BasicJob.new(sleep_time: 0.2), queue: :q1
204
- Workhorse.enqueue BasicJob.new(sleep_time: 0.2), queue: :q1
311
+ first = Workhorse.enqueue BasicJob.new(sleep_time: 2), queue: :q1
312
+ second = Workhorse.enqueue BasicJob.new(sleep_time: 2), queue: :q1
205
313
 
206
- work 0.2, polling_interval: 0.2
207
- jobs = Workhorse::DbJob.all.to_a
208
- assert_equal 'succeeded', jobs[0].state
209
- assert_equal 'waiting', jobs[1].state
314
+ assert_queued_one_at_a_time [first], [second]
210
315
  end
211
316
 
212
317
  def test_multiple_queued_same_queue
213
318
  # One queue
214
- Workhorse.enqueue BasicJob.new(sleep_time: 0.2), queue: :q1
215
- Workhorse.enqueue BasicJob.new(sleep_time: 0.2), queue: :q1
319
+ first = Workhorse.enqueue BasicJob.new(sleep_time: 2), queue: :q1
320
+ second = Workhorse.enqueue BasicJob.new(sleep_time: 2), queue: :q1
216
321
 
217
- work 0.2, polling_interval: 0.2
218
-
219
- jobs = Workhorse::DbJob.all.to_a
220
- assert_equal 'succeeded', jobs[0].state
221
- assert_equal 'waiting', jobs[1].state
322
+ assert_queued_one_at_a_time [first], [second]
222
323
 
223
324
  # Two queues
224
325
  Workhorse::DbJob.delete_all
225
- Workhorse.enqueue BasicJob.new(sleep_time: 0.2), queue: :q1
226
- Workhorse.enqueue BasicJob.new(sleep_time: 0.2), queue: :q1
227
- Workhorse.enqueue BasicJob.new(sleep_time: 0.2), queue: :q2
228
- Workhorse.enqueue BasicJob.new(sleep_time: 0.2), queue: :q2
229
-
230
- work 0.2, polling_interval: 0.2
326
+ q1_first = Workhorse.enqueue BasicJob.new(sleep_time: 2), queue: :q1
327
+ q1_second = Workhorse.enqueue BasicJob.new(sleep_time: 2), queue: :q1
328
+ q2_first = Workhorse.enqueue BasicJob.new(sleep_time: 2), queue: :q2
329
+ q2_second = Workhorse.enqueue BasicJob.new(sleep_time: 2), queue: :q2
231
330
 
232
- jobs = Workhorse::DbJob.order(queue: :asc).to_a
233
- assert_equal 'succeeded', jobs[0].state
234
- assert_equal 'waiting', jobs[1].state
235
- assert_equal 'succeeded', jobs[2].state
236
- assert_equal 'waiting', jobs[3].state
331
+ assert_queued_one_at_a_time [q1_first, q2_first], [q1_second, q2_second]
237
332
  end
238
333
 
239
334
  def test_order_with_priorities
@@ -246,7 +341,9 @@ class Workhorse::WorkerTest < WorkhorseTest
246
341
  Workhorse.enqueue BasicJob.new(some_param: 1, sleep_time: 0), priority: 0
247
342
 
248
343
  BasicJob.results.clear
249
- work 1, pool_size: 1, polling_interval: 0.1
344
+ work_until(pool_size: 1, polling_interval: 0.1) do
345
+ assert_equal 6, BasicJob.results.size
346
+ end
250
347
  assert_equal (1..6).to_a, BasicJob.results
251
348
  end
252
349
 
@@ -260,17 +357,23 @@ class Workhorse::WorkerTest < WorkhorseTest
260
357
  end
261
358
 
262
359
  def test_perform_at
263
- Workhorse.enqueue BasicJob.new(sleep_time: 0), perform_at: Time.now
264
- Workhorse.enqueue BasicJob.new(sleep_time: 0), perform_at: Time.now + 600
265
- work 0.1, polling_interval: 0.1
360
+ due = Workhorse.enqueue BasicJob.new(sleep_time: 0), perform_at: Time.now
361
+ later = Workhorse.enqueue BasicJob.new(sleep_time: 0), perform_at: Time.now + 600
266
362
 
267
- jobs = Workhorse::DbJob.all.to_a
268
- assert_equal 'succeeded', jobs[0].state
269
- assert_equal 'waiting', jobs[1].state
363
+ work_until(polling_interval: 0.1) do
364
+ assert_equal 'succeeded', due.reload.state
365
+ end
366
+
367
+ assert_equal 'waiting', later.reload.state
270
368
  end
271
369
 
272
370
  def test_controlled_shutdown
273
- Workhorse.max_worker_memory_mb = 100
371
+ # Relative to what this process already uses, as the daemon's workers are
372
+ # forks of it: a fixed 100MB is exceeded from the start once the Oracle
373
+ # client library is loaded, and the worker then stops before its first job.
374
+ # MemHungryJob allocates 250MB, so it still crosses the line.
375
+ baseline_mb = `ps -p #{Process.pid} -o rss=`.strip.to_i / 1024
376
+ Workhorse.max_worker_memory_mb = baseline_mb + 100
274
377
  with_daemon do
275
378
  pid = with_retries do
276
379
  pid = daemon.workers.first.pid
@@ -368,4 +471,26 @@ class Workhorse::WorkerTest < WorkhorseTest
368
471
  Workhorse.enqueue BasicJob.new(some_param: :q1), queue: :q1
369
472
  Workhorse.enqueue BasicJob.new(some_param: :q2), queue: :q2
370
473
  end
474
+
475
+ # Jobs ordered by queue, the job without a queue first. Sorted here rather
476
+ # than with ORDER BY, which puts NULL first on MySQL and last on Oracle.
477
+ # Within a queue, in the order they were enqueued.
478
+ def jobs_by_queue
479
+ return Workhorse::DbJob.all.sort_by { |job| [job.queue.nil? ? 0 : 1, job.queue.to_s, job.id] }
480
+ end
481
+
482
+ # Asserts that while the first job of each queue runs, the ones queued behind
483
+ # it wait. Looked at while they run rather than after a fixed time: the job
484
+ # behind is rightly taken as soon as the one ahead has finished.
485
+ def assert_queued_one_at_a_time(firsts, followers)
486
+ with_worker(pool_size: 5, polling_interval: 0.2, auto_terminate: false) do
487
+ with_retries { assert_equal(%w[started] * firsts.size, firsts.map { |job| job.reload.state }) }
488
+
489
+ # Room for further polls, each a chance to take a follower wrongly, and
490
+ # well short of the 2s the jobs ahead take.
491
+ sleep 0.5
492
+
493
+ assert_equal(%w[waiting] * followers.size, followers.map { |job| job.reload.state })
494
+ end
495
+ end
371
496
  end
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.rc1 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.rc1".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-30"
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.rc1
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-30 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