workhorse 1.4.0 → 1.4.2
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 +28 -0
- data/README.md +17 -1
- data/VERSION +1 -1
- data/lib/workhorse/daemon/shell_handler.rb +50 -6
- data/lib/workhorse/daemon.rb +29 -0
- data/lib/workhorse/worker.rb +59 -5
- data/lib/workhorse.rb +25 -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: 0facaea70a5e826980ea41034914a0609a22fb3ed93c9fbe4a7607eb5a3854c9
|
|
4
|
+
data.tar.gz: 35cc28b0a31206fa5f38df60858c5c2054c28fd7c47a4bf0b4983c9cbbcc02d0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 551453702ecb4b89060a7e5fdba4639eccbb60c09c85ebd2fd561eec91787c1b8166be81e81b2bd2da46f250ba70c8426198bca404d1cf1e5fbf1c6c9b5096ee
|
|
7
|
+
data.tar.gz: e1ee8b9ae1267271f8be2befd5455d90614e02c8708b2413bc90f957b8d5fb852b2f805ea4d42b6362844c2e7ed774e2a0a727e83687d570d0c430174e264227
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Workhorse Changelog
|
|
2
2
|
|
|
3
|
+
## 1.4.2 - 2026-02-20
|
|
4
|
+
|
|
5
|
+
* Detach forked worker processes into their own session using `Process.setsid`.
|
|
6
|
+
Previously, when the ShellHandler was the session leader (e.g. started via
|
|
7
|
+
cron), its exit would cause the kernel to send `SIGHUP` to all forked workers,
|
|
8
|
+
potentially crashing them during startup before signal handlers were installed.
|
|
9
|
+
|
|
10
|
+
* Add optional debug logging (`config.debug_log_path`) for diagnosing issues
|
|
11
|
+
with signal handling, process lifecycle, log rotation, and daemon commands.
|
|
12
|
+
|
|
13
|
+
Sitrox reference: #120574.
|
|
14
|
+
|
|
15
|
+
## 1.4.1 - 2026-02-18
|
|
16
|
+
|
|
17
|
+
* Close inherited lockfile fd in forked worker processes. Previously the
|
|
18
|
+
lockfile's file descriptor was inherited by children via `fork`, which could
|
|
19
|
+
prevent the POSIX `flock` from being released if the daemon process exited
|
|
20
|
+
abnormally.
|
|
21
|
+
|
|
22
|
+
* Fix `watch` and `kill` commands to actually abort when the lock is
|
|
23
|
+
unavailable. Previously the `flock` return value with `LOCK_NB` was not
|
|
24
|
+
checked, so the commands would silently proceed without the lock.
|
|
25
|
+
|
|
26
|
+
* Add error handling to the `HUP` signal handler for log reopening. Exceptions
|
|
27
|
+
from `logger.reopen` are now caught and reported via `on_exception`.
|
|
28
|
+
|
|
29
|
+
Sitrox reference: #120574.
|
|
30
|
+
|
|
3
31
|
## 1.4.0 - 2026-02-12
|
|
4
32
|
|
|
5
33
|
* Stable release based on previous RC release.
|
data/README.md
CHANGED
|
@@ -522,7 +522,6 @@ Gem-internal model class `Workhorse::DbJob`, for example:
|
|
|
522
522
|
|
|
523
523
|
```ruby
|
|
524
524
|
# config/initializers/workhorse.rb
|
|
525
|
-
|
|
526
525
|
ActiveSupport.on_load :workhorse_db_job do
|
|
527
526
|
# Code within this block will be run inside of the model class
|
|
528
527
|
# Workhorse::DbJob.
|
|
@@ -530,6 +529,23 @@ ActiveSupport.on_load :workhorse_db_job do
|
|
|
530
529
|
end
|
|
531
530
|
```
|
|
532
531
|
|
|
532
|
+
## Debug logging
|
|
533
|
+
|
|
534
|
+
Workhorse includes an optional debug log for diagnosing issues with signal
|
|
535
|
+
handling, process lifecycle, log rotation, and daemon commands. To enable,
|
|
536
|
+
set `debug_log_path` to a writable file path:
|
|
537
|
+
|
|
538
|
+
```ruby
|
|
539
|
+
# config/initializers/workhorse.rb
|
|
540
|
+
Workhorse.setup do |config|
|
|
541
|
+
config.debug_log_path = Rails.root.join('log', 'workhorse.debug.log')
|
|
542
|
+
end
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
The debug log is designed to be safe for production use: all writes are
|
|
546
|
+
best-effort and silently ignore errors to avoid interfering with normal
|
|
547
|
+
operation. Set `debug_log_path` to `nil` (the default) to disable.
|
|
548
|
+
|
|
533
549
|
## Caveats
|
|
534
550
|
|
|
535
551
|
### Errors during polling / crashed workers
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.4.
|
|
1
|
+
1.4.2
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
module Workhorse
|
|
2
2
|
class Daemon::ShellHandler
|
|
3
|
+
class LockNotAvailableError < StandardError; end
|
|
4
|
+
|
|
3
5
|
def self.run(**options, &block)
|
|
4
6
|
unless ARGV.one?
|
|
5
7
|
usage
|
|
@@ -14,28 +16,62 @@ module Workhorse
|
|
|
14
16
|
begin
|
|
15
17
|
case ARGV.first
|
|
16
18
|
when 'start'
|
|
19
|
+
Workhorse.debug_log('ShellHandler: start command invoked')
|
|
17
20
|
lockfile = acquire_lock(lockfile_path, File::LOCK_EX)
|
|
21
|
+
Workhorse.debug_log('ShellHandler: lock acquired for start')
|
|
22
|
+
daemon.lockfile = lockfile
|
|
18
23
|
status = daemon.start
|
|
19
24
|
when 'stop'
|
|
25
|
+
Workhorse.debug_log('ShellHandler: stop command invoked')
|
|
20
26
|
lockfile = acquire_lock(lockfile_path, File::LOCK_EX)
|
|
27
|
+
Workhorse.debug_log('ShellHandler: lock acquired for stop')
|
|
28
|
+
daemon.lockfile = lockfile
|
|
21
29
|
status = daemon.stop
|
|
22
30
|
when 'kill'
|
|
23
|
-
|
|
24
|
-
|
|
31
|
+
Workhorse.debug_log('ShellHandler: kill command invoked')
|
|
32
|
+
begin
|
|
33
|
+
lockfile = acquire_lock(lockfile_path, File::LOCK_EX | File::LOCK_NB)
|
|
34
|
+
Workhorse.debug_log('ShellHandler: lock acquired for kill')
|
|
35
|
+
daemon.lockfile = lockfile
|
|
36
|
+
status = daemon.stop(true)
|
|
37
|
+
rescue LockNotAvailableError
|
|
38
|
+
Workhorse.debug_log('ShellHandler: lock not available for kill')
|
|
39
|
+
status = 1
|
|
40
|
+
end
|
|
25
41
|
when 'status'
|
|
42
|
+
Workhorse.debug_log('ShellHandler: status command invoked')
|
|
26
43
|
lockfile = acquire_lock(lockfile_path, File::LOCK_EX)
|
|
44
|
+
Workhorse.debug_log('ShellHandler: lock acquired for status')
|
|
45
|
+
daemon.lockfile = lockfile
|
|
27
46
|
status = daemon.status
|
|
28
47
|
when 'watch'
|
|
29
|
-
|
|
30
|
-
|
|
48
|
+
Workhorse.debug_log('ShellHandler: watch command invoked')
|
|
49
|
+
begin
|
|
50
|
+
lockfile = acquire_lock(lockfile_path, File::LOCK_EX | File::LOCK_NB)
|
|
51
|
+
Workhorse.debug_log('ShellHandler: lock acquired for watch')
|
|
52
|
+
daemon.lockfile = lockfile
|
|
53
|
+
status = daemon.watch
|
|
54
|
+
rescue LockNotAvailableError
|
|
55
|
+
Workhorse.debug_log('ShellHandler: lock not available for watch')
|
|
56
|
+
status = 1
|
|
57
|
+
end
|
|
31
58
|
when 'restart'
|
|
59
|
+
Workhorse.debug_log('ShellHandler: restart command invoked')
|
|
32
60
|
lockfile = acquire_lock(lockfile_path, File::LOCK_EX)
|
|
61
|
+
Workhorse.debug_log('ShellHandler: lock acquired for restart')
|
|
62
|
+
daemon.lockfile = lockfile
|
|
33
63
|
status = daemon.restart
|
|
34
64
|
when 'restart-logging'
|
|
65
|
+
Workhorse.debug_log('ShellHandler: restart-logging command invoked')
|
|
35
66
|
lockfile = acquire_lock(lockfile_path, File::LOCK_EX)
|
|
67
|
+
Workhorse.debug_log('ShellHandler: lock acquired for restart-logging')
|
|
68
|
+
daemon.lockfile = lockfile
|
|
36
69
|
status = daemon.restart_logging
|
|
37
70
|
when 'soft-restart'
|
|
71
|
+
Workhorse.debug_log('ShellHandler: soft-restart command invoked')
|
|
38
72
|
lockfile = acquire_lock(lockfile_path, File::LOCK_EX)
|
|
73
|
+
Workhorse.debug_log('ShellHandler: lock acquired for soft-restart')
|
|
74
|
+
daemon.lockfile = lockfile
|
|
39
75
|
status = daemon.soft_restart
|
|
40
76
|
when 'usage'
|
|
41
77
|
usage
|
|
@@ -48,7 +84,10 @@ module Workhorse
|
|
|
48
84
|
warn "#{e.message}\n#{e.backtrace.join("\n")}"
|
|
49
85
|
status = 99
|
|
50
86
|
ensure
|
|
51
|
-
lockfile
|
|
87
|
+
if lockfile
|
|
88
|
+
Workhorse.debug_log("ShellHandler: releasing lock for #{ARGV.first}")
|
|
89
|
+
lockfile.flock(File::LOCK_UN)
|
|
90
|
+
end
|
|
52
91
|
exit! status
|
|
53
92
|
end
|
|
54
93
|
end
|
|
@@ -105,7 +144,12 @@ module Workhorse
|
|
|
105
144
|
def self.acquire_lock(lockfile_path, flags)
|
|
106
145
|
if Workhorse.lock_shell_commands
|
|
107
146
|
lockfile = File.open(lockfile_path, 'a')
|
|
108
|
-
lockfile.flock(flags)
|
|
147
|
+
result = lockfile.flock(flags)
|
|
148
|
+
|
|
149
|
+
if result == false
|
|
150
|
+
lockfile.close
|
|
151
|
+
fail LockNotAvailableError, 'Could not acquire lock. Is another workhorse command already running?'
|
|
152
|
+
end
|
|
109
153
|
|
|
110
154
|
return lockfile
|
|
111
155
|
end
|
data/lib/workhorse/daemon.rb
CHANGED
|
@@ -34,6 +34,10 @@ module Workhorse
|
|
|
34
34
|
# @private
|
|
35
35
|
attr_reader :workers
|
|
36
36
|
|
|
37
|
+
# @return [File, nil] Lockfile handle to close in forked children
|
|
38
|
+
# @private
|
|
39
|
+
attr_accessor :lockfile
|
|
40
|
+
|
|
37
41
|
# Creates a new daemon instance.
|
|
38
42
|
#
|
|
39
43
|
# @param pidfile [String, nil] Path template for PID files (use %i placeholder for worker ID)
|
|
@@ -197,20 +201,27 @@ module Workhorse
|
|
|
197
201
|
def restart_logging
|
|
198
202
|
code = 0
|
|
199
203
|
|
|
204
|
+
Workhorse.debug_log("restart_logging: sending HUP to #{@workers.count} worker(s)")
|
|
205
|
+
|
|
200
206
|
for_each_worker do |worker|
|
|
201
207
|
_pid_file, pid, active = read_pid(worker)
|
|
202
208
|
|
|
209
|
+
Workhorse.debug_log("restart_logging: worker ##{worker.id} (#{worker.name}): pid=#{pid.inspect}, active=#{active.inspect}")
|
|
210
|
+
|
|
203
211
|
next unless pid && active
|
|
204
212
|
|
|
205
213
|
begin
|
|
206
214
|
Process.kill 'HUP', pid
|
|
215
|
+
Workhorse.debug_log("restart_logging: HUP sent successfully to PID #{pid}")
|
|
207
216
|
puts "Worker (#{worker.name}) ##{worker.id}: Sent signal for restart-logging"
|
|
208
217
|
rescue Errno::ESRCH
|
|
218
|
+
Workhorse.debug_log("restart_logging: HUP failed for PID #{pid}: process not found")
|
|
209
219
|
warn "Worker (#{worker.name}) ##{worker.id}: Could not send signal for restart-logging, process not found"
|
|
210
220
|
code = 2
|
|
211
221
|
end
|
|
212
222
|
end
|
|
213
223
|
|
|
224
|
+
Workhorse.debug_log("restart_logging: done, exit code=#{code}")
|
|
214
225
|
return code
|
|
215
226
|
end
|
|
216
227
|
|
|
@@ -223,20 +234,27 @@ module Workhorse
|
|
|
223
234
|
def soft_restart
|
|
224
235
|
code = 0
|
|
225
236
|
|
|
237
|
+
Workhorse.debug_log("Daemon: sending USR1 to #{@workers.count} worker(s)")
|
|
238
|
+
|
|
226
239
|
for_each_worker do |worker|
|
|
227
240
|
_pid_file, pid, active = read_pid(worker)
|
|
228
241
|
|
|
242
|
+
Workhorse.debug_log("Daemon soft_restart: worker ##{worker.id} (#{worker.name}): pid=#{pid.inspect}, active=#{active.inspect}")
|
|
243
|
+
|
|
229
244
|
next unless pid && active
|
|
230
245
|
|
|
231
246
|
begin
|
|
232
247
|
Process.kill 'USR1', pid
|
|
248
|
+
Workhorse.debug_log("Daemon: USR1 sent successfully to PID #{pid}")
|
|
233
249
|
puts "Worker (#{worker.name}) ##{worker.id}: Sent soft-restart signal"
|
|
234
250
|
rescue Errno::ESRCH
|
|
251
|
+
Workhorse.debug_log("Daemon: USR1 failed for PID #{pid}: process not found")
|
|
235
252
|
warn "Worker (#{worker.name}) ##{worker.id}: Process not found"
|
|
236
253
|
code = 2
|
|
237
254
|
end
|
|
238
255
|
end
|
|
239
256
|
|
|
257
|
+
Workhorse.debug_log("Daemon soft_restart: done, exit code=#{code}")
|
|
240
258
|
return code
|
|
241
259
|
end
|
|
242
260
|
|
|
@@ -259,8 +277,16 @@ module Workhorse
|
|
|
259
277
|
def start_worker(worker)
|
|
260
278
|
check_rails_env if defined?(Rails)
|
|
261
279
|
|
|
280
|
+
Workhorse.debug_log("Daemon: forking worker ##{worker.id} (#{worker.name})")
|
|
262
281
|
pid = fork do
|
|
282
|
+
# Detach from the parent's session so that the worker is not killed by
|
|
283
|
+
# SIGHUP when the parent (ShellHandler) exits. Without this, the kernel
|
|
284
|
+
# sends SIGHUP to the foreground process group when the session leader
|
|
285
|
+
# (e.g. a cron- or systemd-started ShellHandler) terminates.
|
|
286
|
+
Process.setsid
|
|
263
287
|
$0 = process_name(worker)
|
|
288
|
+
# Close inherited lockfile fd to prevent holding the flock after parent exits
|
|
289
|
+
@lockfile&.close
|
|
264
290
|
# Reopen pipes to prevent #107576
|
|
265
291
|
$stdin.reopen File.open(File::NULL, 'r')
|
|
266
292
|
null_out = File.open File::NULL, 'w'
|
|
@@ -272,6 +298,7 @@ module Workhorse
|
|
|
272
298
|
worker.pid = pid
|
|
273
299
|
File.write(pid_file_for(worker), pid)
|
|
274
300
|
Process.detach(pid)
|
|
301
|
+
Workhorse.debug_log("Daemon: worker ##{worker.id} (#{worker.name}) forked with PID #{pid}")
|
|
275
302
|
end
|
|
276
303
|
|
|
277
304
|
# Stops a single worker process.
|
|
@@ -284,6 +311,7 @@ module Workhorse
|
|
|
284
311
|
def stop_worker(pid_file, pid, kill: false)
|
|
285
312
|
signals = kill ? %w[KILL] : %w[TERM INT]
|
|
286
313
|
|
|
314
|
+
Workhorse.debug_log("Daemon: stopping PID #{pid} with signals #{signals.join(', ')}")
|
|
287
315
|
loop do
|
|
288
316
|
begin
|
|
289
317
|
signals.each { |signal| Process.kill(signal, pid) }
|
|
@@ -294,6 +322,7 @@ module Workhorse
|
|
|
294
322
|
sleep 1
|
|
295
323
|
end
|
|
296
324
|
|
|
325
|
+
Workhorse.debug_log("Daemon: PID #{pid} stopped")
|
|
297
326
|
File.delete(pid_file)
|
|
298
327
|
end
|
|
299
328
|
|
data/lib/workhorse/worker.rb
CHANGED
|
@@ -158,6 +158,8 @@ module Workhorse
|
|
|
158
158
|
@poller.start
|
|
159
159
|
log 'Started up'
|
|
160
160
|
|
|
161
|
+
Workhorse.debug_log("[Job worker #{id}] Started: PID=#{pid}, logger=#{describe_logger(logger)}")
|
|
162
|
+
|
|
161
163
|
trap_termination if @auto_terminate
|
|
162
164
|
trap_log_reopen
|
|
163
165
|
trap_soft_restart
|
|
@@ -189,12 +191,14 @@ module Workhorse
|
|
|
189
191
|
mutex.synchronize do
|
|
190
192
|
assert_state! :running
|
|
191
193
|
|
|
194
|
+
Workhorse.debug_log("[Job worker #{id}] Shutdown starting")
|
|
192
195
|
log 'Shutting down'
|
|
193
196
|
@state = :shutdown
|
|
194
197
|
|
|
195
198
|
@poller.shutdown
|
|
196
199
|
@pool.shutdown
|
|
197
200
|
log 'Shut down'
|
|
201
|
+
Workhorse.debug_log("[Job worker #{id}] Shutdown complete")
|
|
198
202
|
end
|
|
199
203
|
end
|
|
200
204
|
|
|
@@ -267,6 +271,8 @@ module Workhorse
|
|
|
267
271
|
|
|
268
272
|
return true unless exceeded
|
|
269
273
|
|
|
274
|
+
Workhorse.debug_log("[Job worker #{id}] Memory limit exceeded: #{mem}MB > #{max}MB, initiating shutdown")
|
|
275
|
+
|
|
270
276
|
if defined?(Rails)
|
|
271
277
|
FileUtils.touch self.class.shutdown_file_for(pid)
|
|
272
278
|
end
|
|
@@ -296,11 +302,16 @@ module Workhorse
|
|
|
296
302
|
def trap_log_reopen
|
|
297
303
|
Signal.trap(LOG_REOPEN_SIGNAL) do
|
|
298
304
|
Thread.new do
|
|
299
|
-
logger
|
|
305
|
+
Workhorse.debug_log("[Job worker #{id}] HUP received, logger state before reopen: #{describe_logger(logger)}")
|
|
300
306
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
307
|
+
logger&.reopen
|
|
308
|
+
Workhorse.debug_log("[Job worker #{id}] Logger state after reopen: #{describe_logger(logger)}")
|
|
309
|
+
|
|
310
|
+
Workhorse.debug_log("[Job worker #{id}] HUP handling complete")
|
|
311
|
+
rescue Exception => e
|
|
312
|
+
Workhorse.debug_log("[Job worker #{id}] Logger reopen failed: #{e.class}: #{e.message}")
|
|
313
|
+
log %(Log reopen signal handler error: #{e.message}\n#{e.backtrace.join("\n")}), :error
|
|
314
|
+
Workhorse.on_exception.call(e)
|
|
304
315
|
end.join
|
|
305
316
|
end
|
|
306
317
|
end
|
|
@@ -317,6 +328,7 @@ module Workhorse
|
|
|
317
328
|
# quickly when called multiple times, this does not pose a risk of
|
|
318
329
|
# keeping open a big number of "shutdown threads".
|
|
319
330
|
Thread.new do
|
|
331
|
+
Workhorse.debug_log("[Job worker #{id}] #{signal} received, shutting down")
|
|
320
332
|
log "\nCaught #{signal}, shutting worker down..."
|
|
321
333
|
shutdown
|
|
322
334
|
end.join
|
|
@@ -336,9 +348,14 @@ module Workhorse
|
|
|
336
348
|
|
|
337
349
|
return unless @soft_restart_requested.make_true
|
|
338
350
|
|
|
351
|
+
Workhorse.debug_log("[Job worker #{id}] Soft restart initiated")
|
|
352
|
+
|
|
339
353
|
# Create shutdown file for watch to detect
|
|
340
354
|
shutdown_file = self.class.shutdown_file_for(pid)
|
|
341
|
-
|
|
355
|
+
if shutdown_file
|
|
356
|
+
FileUtils.touch(shutdown_file)
|
|
357
|
+
Workhorse.debug_log("[Job worker #{id}] Shutdown file created: #{shutdown_file}")
|
|
358
|
+
end
|
|
342
359
|
|
|
343
360
|
# Monitor in a separate thread to avoid blocking the signal handler
|
|
344
361
|
@soft_restart_thread = Thread.new do
|
|
@@ -358,6 +375,7 @@ module Workhorse
|
|
|
358
375
|
# Start a new thread as certain functionality (such as logging) is not
|
|
359
376
|
# available from within a trap context.
|
|
360
377
|
Thread.new do
|
|
378
|
+
Workhorse.debug_log("[Job worker #{id}] #{SOFT_RESTART_SIGNAL} received, initiating soft restart")
|
|
361
379
|
log "\nCaught #{SOFT_RESTART_SIGNAL}, initiating soft restart..."
|
|
362
380
|
soft_restart
|
|
363
381
|
rescue Exception => e
|
|
@@ -369,16 +387,52 @@ module Workhorse
|
|
|
369
387
|
end
|
|
370
388
|
end
|
|
371
389
|
|
|
390
|
+
# Returns a human-readable description of a logger's internal state.
|
|
391
|
+
# Used for debug logging to diagnose log rotation issues.
|
|
392
|
+
#
|
|
393
|
+
# @param lgr [Logger, nil] The logger to describe
|
|
394
|
+
# @return [String] Description of the logger's state
|
|
395
|
+
# @private
|
|
396
|
+
def describe_logger(lgr)
|
|
397
|
+
return 'nil' unless lgr
|
|
398
|
+
|
|
399
|
+
parts = ["class=#{lgr.class}"]
|
|
400
|
+
|
|
401
|
+
logdev = lgr.instance_variable_get(:@logdev)
|
|
402
|
+
if logdev
|
|
403
|
+
parts << "filename=#{logdev.filename.inspect}" if logdev.respond_to?(:filename)
|
|
404
|
+
|
|
405
|
+
dev = logdev.respond_to?(:dev) ? logdev.dev : nil
|
|
406
|
+
if dev
|
|
407
|
+
parts << "closed=#{dev.closed?}"
|
|
408
|
+
unless dev.closed?
|
|
409
|
+
fileno = dev.fileno
|
|
410
|
+
parts << "fd=#{fileno}"
|
|
411
|
+
fd_path = "/proc/self/fd/#{fileno}"
|
|
412
|
+
parts << "fd_target=#{File.readlink(fd_path).inspect}" if File.exist?(fd_path)
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
else
|
|
416
|
+
parts << 'logdev=nil'
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
parts.join(', ')
|
|
420
|
+
rescue Exception => e
|
|
421
|
+
"error describing logger: #{e.class}: #{e.message}"
|
|
422
|
+
end
|
|
423
|
+
|
|
372
424
|
# Waits for all jobs to complete, then shuts down the worker.
|
|
373
425
|
# Called asynchronously from soft_restart.
|
|
374
426
|
#
|
|
375
427
|
# @return [void]
|
|
376
428
|
# @private
|
|
377
429
|
def wait_for_idle_then_shutdown
|
|
430
|
+
Workhorse.debug_log("[Job worker #{id}] Waiting for idle before soft restart shutdown (pool_size=#{@pool_size})")
|
|
378
431
|
loop do
|
|
379
432
|
break if @state == :shutdown
|
|
380
433
|
|
|
381
434
|
if idle == @pool_size
|
|
435
|
+
Workhorse.debug_log("[Job worker #{id}] All threads idle, proceeding with soft restart shutdown")
|
|
382
436
|
log 'All jobs completed, shutting down for soft restart'
|
|
383
437
|
shutdown
|
|
384
438
|
break
|
data/lib/workhorse.rb
CHANGED
|
@@ -107,6 +107,31 @@ module Workhorse
|
|
|
107
107
|
mattr_accessor :max_worker_memory_mb
|
|
108
108
|
self.max_worker_memory_mb = 0
|
|
109
109
|
|
|
110
|
+
# Path to a debug log file for diagnosing log rotation and signal handling issues.
|
|
111
|
+
# When set, Workhorse writes timestamped debug entries to this file at key points
|
|
112
|
+
# (worker startup, HUP signal handling, restart-logging command flow).
|
|
113
|
+
# Set to nil to disable (default).
|
|
114
|
+
#
|
|
115
|
+
# @return [String, nil] Path to debug log file
|
|
116
|
+
mattr_accessor :debug_log_path
|
|
117
|
+
self.debug_log_path = nil
|
|
118
|
+
|
|
119
|
+
# Writes a debug message to the debug log file.
|
|
120
|
+
# Does nothing if {.debug_log_path} is nil.
|
|
121
|
+
# Silently ignores all exceptions to avoid interfering with normal operation.
|
|
122
|
+
#
|
|
123
|
+
# @param message [String] The message to log
|
|
124
|
+
# @return [void]
|
|
125
|
+
def self.debug_log(message)
|
|
126
|
+
return unless debug_log_path
|
|
127
|
+
|
|
128
|
+
File.open(debug_log_path, 'a') do |f|
|
|
129
|
+
f.write("[#{Time.now.iso8601(3)}] [PID #{Process.pid}] #{message}\n")
|
|
130
|
+
f.flush
|
|
131
|
+
end
|
|
132
|
+
rescue Exception # rubocop:disable Lint/SuppressedException
|
|
133
|
+
end
|
|
134
|
+
|
|
110
135
|
# Configuration method for setting up Workhorse options.
|
|
111
136
|
#
|
|
112
137
|
# @yield [self] Configuration block
|
data/workhorse.gemspec
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
|
2
|
-
# stub: workhorse 1.4.
|
|
2
|
+
# stub: workhorse 1.4.2 ruby lib
|
|
3
3
|
|
|
4
4
|
Gem::Specification.new do |s|
|
|
5
5
|
s.name = "workhorse".freeze
|
|
6
|
-
s.version = "1.4.
|
|
6
|
+
s.version = "1.4.2"
|
|
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-02-
|
|
11
|
+
s.date = "2026-02-20"
|
|
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.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.4.
|
|
4
|
+
version: 1.4.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sitrox
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-02-
|
|
10
|
+
date: 2026-02-20 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: activesupport
|