workhorse 1.5.0 → 1.5.1
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/CHANGELOG.md +12 -0
- data/VERSION +1 -1
- data/lib/workhorse/daemon.rb +4 -0
- data/lib/workhorse/poller.rb +6 -0
- data/lib/workhorse/worker.rb +39 -0
- data/test/workhorse/worker_test.rb +47 -0
- data/workhorse.gemspec +3 -3
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5b370f18088a986d133800af502b072749a8ae3b1b56585abc7cd6e086a40f7f
|
|
4
|
+
data.tar.gz: 7554e03f2927f9915013993d0da0401772bf201a993ae56b54e3631d295faa0b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d75f21bb4784267b96dd047cf04f652458265b335a9020f3ae101d5aa85e18078ef37ed16d67c3bb2b6a4f47f696f4b9286817b3138b902644ef7ee799614140
|
|
7
|
+
data.tar.gz: 67aaa64b06d6523e3d4bfa90dfa1053134e79f877208e0077e3adaa6b61fb8a5c6ae10c0c61e6d1f4254131e0106e9cc3ca32546db3bbafd41d6dbff7af938a6
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Workhorse Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.1 - 2026-07-22
|
|
4
|
+
|
|
5
|
+
* Add per-worker heartbeat files. On every successful poll, each worker touches
|
|
6
|
+
`tmp/pids/workhorse.<daemon_id>.heartbeat` (keyed by daemon worker id, the same
|
|
7
|
+
id used for the pidfile). The file's mtime provides a stable per-worker
|
|
8
|
+
"last polled at" clock that survives restarts, letting external monitoring
|
|
9
|
+
detect dead, crash-looping, or hung workers. The daemon exposes the worker id
|
|
10
|
+
to the forked worker process via the `WORKHORSE_DAEMON_WORKER_ID` environment
|
|
11
|
+
variable.
|
|
12
|
+
|
|
13
|
+
Sitrox reference: #151020.
|
|
14
|
+
|
|
3
15
|
## 1.5.0 - 2026-07-01
|
|
4
16
|
|
|
5
17
|
* Change `DetectStaleJobsJob` to accept `locked_to_started_threshold` and
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.5.
|
|
1
|
+
1.5.1
|
data/lib/workhorse/daemon.rb
CHANGED
|
@@ -314,6 +314,10 @@ module Workhorse
|
|
|
314
314
|
$stdout.reopen null_out
|
|
315
315
|
$stderr.reopen null_out
|
|
316
316
|
|
|
317
|
+
# Expose the daemon worker id so the worker can key its heartbeat file
|
|
318
|
+
# by slot (see Workhorse::Worker#heartbeat!). Same id as the pidfile.
|
|
319
|
+
ENV['WORKHORSE_DAEMON_WORKER_ID'] = worker.id.to_s
|
|
320
|
+
|
|
317
321
|
worker.block.call
|
|
318
322
|
end
|
|
319
323
|
worker.pid = pid
|
data/lib/workhorse/poller.rb
CHANGED
|
@@ -302,6 +302,12 @@ module Workhorse
|
|
|
302
302
|
# while we were acquiring the lock or querying jobs.
|
|
303
303
|
job_ids.each { |job_id| worker.perform(job_id) } if running? && worker.accepting_jobs?
|
|
304
304
|
end
|
|
305
|
+
|
|
306
|
+
# Record that this worker successfully polled. Done at the very end so it
|
|
307
|
+
# only advances when the poll actually completed (a poll that raises never
|
|
308
|
+
# reaches here). Skipped on the early return above when the worker is no
|
|
309
|
+
# longer accepting jobs, so a soft-restarting worker correctly ages out.
|
|
310
|
+
worker.heartbeat!
|
|
305
311
|
end
|
|
306
312
|
|
|
307
313
|
# Returns an array of {Workhorse::DbJob}s that can be started.
|
data/lib/workhorse/worker.rb
CHANGED
|
@@ -65,6 +65,23 @@ module Workhorse
|
|
|
65
65
|
Rails.root.join('tmp', 'pids', "workhorse.#{pid}.shutdown")
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
+
# Returns the path to the heartbeat file for a given daemon worker id.
|
|
69
|
+
#
|
|
70
|
+
# The heartbeat file is touched on every successful poll (see {#heartbeat!})
|
|
71
|
+
# and lets external monitoring determine, from its mtime, how long ago a
|
|
72
|
+
# worker last polled. It is keyed by the daemon worker id (the same id used
|
|
73
|
+
# for the pidfile) rather than the pid, so the mtime survives restarts and
|
|
74
|
+
# forms a stable per-slot "last polled at" clock.
|
|
75
|
+
#
|
|
76
|
+
# @param daemon_id [String, Integer] Daemon worker id (see
|
|
77
|
+
# {Workhorse::Daemon}).
|
|
78
|
+
# @return [Pathname, nil] Path to heartbeat file or nil if not in Rails
|
|
79
|
+
# @private
|
|
80
|
+
def self.heartbeat_file_for(daemon_id)
|
|
81
|
+
return nil unless defined?(Rails)
|
|
82
|
+
Rails.root.join('tmp', 'pids', "workhorse.#{daemon_id}.heartbeat")
|
|
83
|
+
end
|
|
84
|
+
|
|
68
85
|
# Instantiates a new worker. The worker is not automatically started.
|
|
69
86
|
#
|
|
70
87
|
# @param queues [Array] The queues you want this worker to process. If an
|
|
@@ -228,6 +245,28 @@ module Workhorse
|
|
|
228
245
|
@soft_restart_requested.false?
|
|
229
246
|
end
|
|
230
247
|
|
|
248
|
+
# Touches this worker's heartbeat file to record that it just polled
|
|
249
|
+
# successfully. Called at the end of each poll (see {Workhorse::Poller#poll}).
|
|
250
|
+
#
|
|
251
|
+
# Keyed by the daemon worker id provided via the
|
|
252
|
+
# `WORKHORSE_DAEMON_WORKER_ID` environment variable (set by
|
|
253
|
+
# {Workhorse::Daemon} when forking the worker). No-op when the id is absent
|
|
254
|
+
# (e.g. workers not started through the daemon) or when not running in Rails.
|
|
255
|
+
#
|
|
256
|
+
# This is best-effort: any error is swallowed so that a heartbeat failure can
|
|
257
|
+
# never take down the worker.
|
|
258
|
+
#
|
|
259
|
+
# @return [void]
|
|
260
|
+
def heartbeat!
|
|
261
|
+
daemon_id = ENV.fetch('WORKHORSE_DAEMON_WORKER_ID', nil)
|
|
262
|
+
return unless daemon_id
|
|
263
|
+
|
|
264
|
+
path = self.class.heartbeat_file_for(daemon_id)
|
|
265
|
+
FileUtils.touch(path) if path
|
|
266
|
+
rescue StandardError => e
|
|
267
|
+
Workhorse.debug_log("[Job worker #{id}] Heartbeat touch failed: #{e.class}: #{e.message}")
|
|
268
|
+
end
|
|
269
|
+
|
|
231
270
|
# Schedules a job for execution in the thread pool.
|
|
232
271
|
#
|
|
233
272
|
# @param db_job_id [Integer] The ID of the {Workhorse::DbJob} to perform
|
|
@@ -306,6 +306,53 @@ class Workhorse::WorkerTest < WorkhorseTest
|
|
|
306
306
|
Workhorse.max_worker_memory_mb = 0
|
|
307
307
|
end
|
|
308
308
|
|
|
309
|
+
def test_heartbeat_file_for
|
|
310
|
+
assert_equal(
|
|
311
|
+
Rails.root.join('tmp', 'pids', 'workhorse.3.heartbeat').to_s,
|
|
312
|
+
Workhorse::Worker.heartbeat_file_for(3).to_s
|
|
313
|
+
)
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def test_heartbeat_noop_without_daemon_id
|
|
317
|
+
ENV.delete('WORKHORSE_DAEMON_WORKER_ID')
|
|
318
|
+
path = Workhorse::Worker.heartbeat_file_for('42')
|
|
319
|
+
FileUtils.rm_f(path)
|
|
320
|
+
|
|
321
|
+
Workhorse::Worker.new.heartbeat!
|
|
322
|
+
|
|
323
|
+
assert_not File.exist?(path), 'heartbeat must not be touched without a daemon id'
|
|
324
|
+
ensure
|
|
325
|
+
FileUtils.rm_f(path)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def test_heartbeat_touches_file
|
|
329
|
+
ENV['WORKHORSE_DAEMON_WORKER_ID'] = '42'
|
|
330
|
+
path = Workhorse::Worker.heartbeat_file_for('42')
|
|
331
|
+
FileUtils.rm_f(path)
|
|
332
|
+
|
|
333
|
+
Workhorse::Worker.new.heartbeat!
|
|
334
|
+
|
|
335
|
+
assert File.exist?(path), 'heartbeat file expected to be touched'
|
|
336
|
+
ensure
|
|
337
|
+
ENV.delete('WORKHORSE_DAEMON_WORKER_ID')
|
|
338
|
+
FileUtils.rm_f(path)
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def test_poll_touches_heartbeat
|
|
342
|
+
ENV['WORKHORSE_DAEMON_WORKER_ID'] = '42'
|
|
343
|
+
path = Workhorse::Worker.heartbeat_file_for('42')
|
|
344
|
+
FileUtils.rm_f(path)
|
|
345
|
+
|
|
346
|
+
with_worker(pool_size: 1, polling_interval: 0.1) do
|
|
347
|
+
with_retries do
|
|
348
|
+
assert File.exist?(path), 'expected a successful poll to touch the heartbeat file'
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
ensure
|
|
352
|
+
ENV.delete('WORKHORSE_DAEMON_WORKER_ID')
|
|
353
|
+
FileUtils.rm_f(path)
|
|
354
|
+
end
|
|
355
|
+
|
|
309
356
|
private
|
|
310
357
|
|
|
311
358
|
def assert_process(pid)
|
data/workhorse.gemspec
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
|
2
|
-
# stub: workhorse 1.5.
|
|
2
|
+
# stub: workhorse 1.5.1 ruby lib
|
|
3
3
|
|
|
4
4
|
Gem::Specification.new do |s|
|
|
5
5
|
s.name = "workhorse".freeze
|
|
6
|
-
s.version = "1.5.
|
|
6
|
+
s.version = "1.5.1".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-07-
|
|
11
|
+
s.date = "2026-07-22"
|
|
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, "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: 1.5.
|
|
4
|
+
version: 1.5.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sitrox
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-07-
|
|
10
|
+
date: 2026-07-22 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: activesupport
|