workhorse 2.0.0.rc0 → 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.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +137 -1
- data/CHANGELOG.md +48 -0
- data/Gemfile +16 -1
- data/README.md +31 -16
- data/VERSION +1 -1
- data/lib/generators/workhorse/templates/create_table_jobs.rb +19 -4
- data/lib/generators/workhorse/templates/create_table_workhorse_schedules.rb +14 -1
- data/lib/workhorse/daemon/shell_handler.rb +4 -1
- data/lib/workhorse/daemon.rb +5 -1
- data/lib/workhorse/db_job.rb +40 -1
- data/lib/workhorse/performer.rb +1 -2
- data/lib/workhorse/poller.rb +55 -15
- data/lib/workhorse/worker.rb +25 -4
- data/test/lib/db_schema.rb +19 -6
- data/test/lib/test_helper.rb +107 -9
- data/test/workhorse/db_job_test.rb +1 -3
- data/test/workhorse/notifier_test.rb +0 -13
- data/test/workhorse/performer_test.rb +7 -9
- data/test/workhorse/poller_test.rb +95 -25
- data/test/workhorse/schedule_test.rb +1 -1
- data/test/workhorse/worker_test.rb +109 -76
- data/workhorse.gemspec +3 -3
- metadata +2 -2
|
@@ -97,14 +97,11 @@ class Workhorse::WorkerTest < WorkhorseTest
|
|
|
97
97
|
with_worker(pool_size: 5, polling_interval: 0.2) do |w|
|
|
98
98
|
assert_equal 5, w.idle
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
Workhorse.enqueue BasicJob.new(sleep_time:
|
|
100
|
+
# Long enough to be seen running at the 0.1s the retries look.
|
|
101
|
+
Workhorse.enqueue BasicJob.new(sleep_time: 1)
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
assert_equal
|
|
105
|
-
|
|
106
|
-
sleep 0.2
|
|
107
|
-
assert_equal 5, w.idle
|
|
103
|
+
with_retries { assert_equal 4, w.idle }
|
|
104
|
+
with_retries { assert_equal 5, w.idle }
|
|
108
105
|
end
|
|
109
106
|
end
|
|
110
107
|
|
|
@@ -122,42 +119,37 @@ class Workhorse::WorkerTest < WorkhorseTest
|
|
|
122
119
|
end
|
|
123
120
|
|
|
124
121
|
def test_perform
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
129
126
|
|
|
130
|
-
|
|
127
|
+
work_until(pool_size: 5, polling_interval: 0.2) do
|
|
128
|
+
assert_equal 'succeeded', Workhorse::DbJob.first.state
|
|
131
129
|
end
|
|
132
|
-
|
|
133
|
-
assert_equal 'succeeded', Workhorse::DbJob.first.state
|
|
134
130
|
end
|
|
135
131
|
|
|
136
132
|
def test_params
|
|
137
133
|
BasicJob.results.clear
|
|
138
134
|
|
|
139
135
|
Workhorse.enqueue BasicJob.new(some_param: 5, sleep_time: 0)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
assert_equal 'succeeded', Workhorse::DbJob.first.state
|
|
136
|
+
work_until(pool_size: 5, polling_interval: 0.2) { assert_equal 'succeeded', Workhorse::DbJob.first.state }
|
|
143
137
|
|
|
144
138
|
assert_equal 1, BasicJob.results.count
|
|
145
139
|
assert_equal 5, BasicJob.results.first
|
|
146
140
|
end
|
|
147
141
|
|
|
148
142
|
def test_term
|
|
149
|
-
with_worker(polling_interval: 0.2) do |w|
|
|
143
|
+
with_worker(polling_interval: 0.2, auto_terminate: true) do |w|
|
|
150
144
|
Process.kill 'TERM', Process.pid
|
|
151
|
-
|
|
152
|
-
w.assert_state! :shutdown
|
|
145
|
+
with_retries { w.assert_state! :shutdown }
|
|
153
146
|
end
|
|
154
147
|
end
|
|
155
148
|
|
|
156
149
|
def test_int
|
|
157
|
-
with_worker(polling_interval: 0.2) do |w|
|
|
150
|
+
with_worker(polling_interval: 0.2, auto_terminate: true) do |w|
|
|
158
151
|
Process.kill 'INT', Process.pid
|
|
159
|
-
|
|
160
|
-
w.assert_state! :shutdown
|
|
152
|
+
with_retries { w.assert_state! :shutdown }
|
|
161
153
|
end
|
|
162
154
|
end
|
|
163
155
|
|
|
@@ -174,6 +166,36 @@ class Workhorse::WorkerTest < WorkhorseTest
|
|
|
174
166
|
FileUtils.rm_f Workhorse::Worker.shutdown_file_for(Process.pid)
|
|
175
167
|
end
|
|
176
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
|
+
|
|
177
199
|
def test_soft_restart_when_busy_waits_for_job
|
|
178
200
|
with_worker(pool_size: 1, polling_interval: 0.2) do |w|
|
|
179
201
|
Workhorse.enqueue BasicJob.new(sleep_time: 0.5)
|
|
@@ -236,7 +258,7 @@ class Workhorse::WorkerTest < WorkhorseTest
|
|
|
236
258
|
end
|
|
237
259
|
|
|
238
260
|
def test_soft_restart_ignored_during_shutdown
|
|
239
|
-
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|
|
|
240
262
|
Process.kill 'TERM', Process.pid
|
|
241
263
|
with_retries { assert_equal :shutdown, w.state }
|
|
242
264
|
|
|
@@ -252,80 +274,61 @@ class Workhorse::WorkerTest < WorkhorseTest
|
|
|
252
274
|
|
|
253
275
|
def test_no_queues
|
|
254
276
|
enqueue_in_multiple_queues
|
|
255
|
-
work 0.2, polling_interval: 0.2
|
|
256
277
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
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
|
|
261
281
|
end
|
|
262
282
|
|
|
263
283
|
def test_nil_queue
|
|
264
284
|
enqueue_in_multiple_queues
|
|
265
|
-
work 0.2, queues: [nil], polling_interval: 0.2
|
|
266
285
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
|
271
291
|
end
|
|
272
292
|
|
|
273
293
|
def test_queues_with_nil
|
|
274
294
|
enqueue_in_multiple_queues
|
|
275
|
-
work 0.2, queues: [nil, :q1], polling_interval: 0.2
|
|
276
295
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
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
|
|
281
299
|
end
|
|
282
300
|
|
|
283
301
|
def test_queues_without_nil
|
|
284
302
|
enqueue_in_multiple_queues
|
|
285
|
-
work 0.2, queues: %i[q1 q2], polling_interval: 0.2
|
|
286
303
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
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
|
|
291
307
|
end
|
|
292
308
|
|
|
293
309
|
def test_queue_not_parallel
|
|
294
310
|
Workhorse::DbJob.delete_all
|
|
295
|
-
Workhorse.enqueue BasicJob.new(sleep_time:
|
|
296
|
-
Workhorse.enqueue BasicJob.new(sleep_time:
|
|
311
|
+
first = Workhorse.enqueue BasicJob.new(sleep_time: 2), queue: :q1
|
|
312
|
+
second = Workhorse.enqueue BasicJob.new(sleep_time: 2), queue: :q1
|
|
297
313
|
|
|
298
|
-
|
|
299
|
-
jobs = Workhorse::DbJob.all.to_a
|
|
300
|
-
assert_equal 'succeeded', jobs[0].state
|
|
301
|
-
assert_equal 'waiting', jobs[1].state
|
|
314
|
+
assert_queued_one_at_a_time [first], [second]
|
|
302
315
|
end
|
|
303
316
|
|
|
304
317
|
def test_multiple_queued_same_queue
|
|
305
318
|
# One queue
|
|
306
|
-
Workhorse.enqueue BasicJob.new(sleep_time:
|
|
307
|
-
Workhorse.enqueue BasicJob.new(sleep_time:
|
|
319
|
+
first = Workhorse.enqueue BasicJob.new(sleep_time: 2), queue: :q1
|
|
320
|
+
second = Workhorse.enqueue BasicJob.new(sleep_time: 2), queue: :q1
|
|
308
321
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
jobs = Workhorse::DbJob.all.to_a
|
|
312
|
-
assert_equal 'succeeded', jobs[0].state
|
|
313
|
-
assert_equal 'waiting', jobs[1].state
|
|
322
|
+
assert_queued_one_at_a_time [first], [second]
|
|
314
323
|
|
|
315
324
|
# Two queues
|
|
316
325
|
Workhorse::DbJob.delete_all
|
|
317
|
-
Workhorse.enqueue BasicJob.new(sleep_time:
|
|
318
|
-
Workhorse.enqueue BasicJob.new(sleep_time:
|
|
319
|
-
Workhorse.enqueue BasicJob.new(sleep_time:
|
|
320
|
-
Workhorse.enqueue BasicJob.new(sleep_time:
|
|
321
|
-
|
|
322
|
-
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
|
|
323
330
|
|
|
324
|
-
|
|
325
|
-
assert_equal 'succeeded', jobs[0].state
|
|
326
|
-
assert_equal 'waiting', jobs[1].state
|
|
327
|
-
assert_equal 'succeeded', jobs[2].state
|
|
328
|
-
assert_equal 'waiting', jobs[3].state
|
|
331
|
+
assert_queued_one_at_a_time [q1_first, q2_first], [q1_second, q2_second]
|
|
329
332
|
end
|
|
330
333
|
|
|
331
334
|
def test_order_with_priorities
|
|
@@ -338,7 +341,9 @@ class Workhorse::WorkerTest < WorkhorseTest
|
|
|
338
341
|
Workhorse.enqueue BasicJob.new(some_param: 1, sleep_time: 0), priority: 0
|
|
339
342
|
|
|
340
343
|
BasicJob.results.clear
|
|
341
|
-
|
|
344
|
+
work_until(pool_size: 1, polling_interval: 0.1) do
|
|
345
|
+
assert_equal 6, BasicJob.results.size
|
|
346
|
+
end
|
|
342
347
|
assert_equal (1..6).to_a, BasicJob.results
|
|
343
348
|
end
|
|
344
349
|
|
|
@@ -352,17 +357,23 @@ class Workhorse::WorkerTest < WorkhorseTest
|
|
|
352
357
|
end
|
|
353
358
|
|
|
354
359
|
def test_perform_at
|
|
355
|
-
Workhorse.enqueue BasicJob.new(sleep_time: 0), perform_at: Time.now
|
|
356
|
-
Workhorse.enqueue BasicJob.new(sleep_time: 0), perform_at: Time.now + 600
|
|
357
|
-
|
|
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
|
|
362
|
+
|
|
363
|
+
work_until(polling_interval: 0.1) do
|
|
364
|
+
assert_equal 'succeeded', due.reload.state
|
|
365
|
+
end
|
|
358
366
|
|
|
359
|
-
|
|
360
|
-
assert_equal 'succeeded', jobs[0].state
|
|
361
|
-
assert_equal 'waiting', jobs[1].state
|
|
367
|
+
assert_equal 'waiting', later.reload.state
|
|
362
368
|
end
|
|
363
369
|
|
|
364
370
|
def test_controlled_shutdown
|
|
365
|
-
|
|
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
|
|
366
377
|
with_daemon do
|
|
367
378
|
pid = with_retries do
|
|
368
379
|
pid = daemon.workers.first.pid
|
|
@@ -460,4 +471,26 @@ class Workhorse::WorkerTest < WorkhorseTest
|
|
|
460
471
|
Workhorse.enqueue BasicJob.new(some_param: :q1), queue: :q1
|
|
461
472
|
Workhorse.enqueue BasicJob.new(some_param: :q2), queue: :q2
|
|
462
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
|
|
463
496
|
end
|
data/workhorse.gemspec
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
|
2
|
-
# stub: workhorse 2.0.0.
|
|
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 = "2.0.0.
|
|
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-09-
|
|
11
|
+
s.date = "2026-09-30"
|
|
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/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]
|
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: 2.0.0.
|
|
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-09-
|
|
10
|
+
date: 2026-09-30 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: activesupport
|