Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# Workhorse Changelog

## 1.4.1 - 2026-xx-xx

* Close inherited lockfile fd in forked worker processes. Previously the
lockfile's file descriptor was inherited by children via `fork`, which could
prevent the POSIX `flock` from being released if the daemon process exited
abnormally.

* Fix `watch` and `kill` commands to actually abort when the lock is
unavailable. Previously the `flock` return value with `LOCK_NB` was not
checked, so the commands would silently proceed without the lock.

* Add error handling to the `HUP` signal handler for log reopening. Exceptions
from `logger.reopen` are now caught and reported via `on_exception`.

Sitrox reference: #120574.

## 1.4.0 - 2026-02-12

* Stable release based on previous RC release.
Expand Down
6 changes: 6 additions & 0 deletions lib/workhorse/daemon.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ def initialize(id, name, &block)
# @private
attr_reader :workers

# @return [File, nil] Lockfile handle to close in forked children
# @private
attr_accessor :lockfile

# Creates a new daemon instance.
#
# @param pidfile [String, nil] Path template for PID files (use %i placeholder for worker ID)
Expand Down Expand Up @@ -261,6 +265,8 @@ def start_worker(worker)

pid = fork do
$0 = process_name(worker)
# Close inherited lockfile fd to prevent holding the flock after parent exits
@lockfile&.close
# Reopen pipes to prevent #107576
$stdin.reopen File.open(File::NULL, 'r')
null_out = File.open File::NULL, 'w'
Expand Down
33 changes: 28 additions & 5 deletions lib/workhorse/daemon/shell_handler.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
module Workhorse
class Daemon::ShellHandler
class LockNotAvailableError < StandardError; end

def self.run(**options, &block)
unless ARGV.one?
usage
Expand All @@ -15,27 +17,43 @@ def self.run(**options, &block)
case ARGV.first
when 'start'
lockfile = acquire_lock(lockfile_path, File::LOCK_EX)
daemon.lockfile = lockfile
status = daemon.start
when 'stop'
lockfile = acquire_lock(lockfile_path, File::LOCK_EX)
daemon.lockfile = lockfile
status = daemon.stop
when 'kill'
lockfile = acquire_lock(lockfile_path, File::LOCK_EX | File::LOCK_NB)
status = daemon.stop(true)
begin
lockfile = acquire_lock(lockfile_path, File::LOCK_EX | File::LOCK_NB)
daemon.lockfile = lockfile
status = daemon.stop(true)
rescue LockNotAvailableError
status = 1
end
when 'status'
lockfile = acquire_lock(lockfile_path, File::LOCK_EX)
daemon.lockfile = lockfile
status = daemon.status
when 'watch'
lockfile = acquire_lock(lockfile_path, File::LOCK_EX | File::LOCK_NB)
status = daemon.watch
begin
lockfile = acquire_lock(lockfile_path, File::LOCK_EX | File::LOCK_NB)
daemon.lockfile = lockfile
status = daemon.watch
rescue LockNotAvailableError
status = 1
end
when 'restart'
lockfile = acquire_lock(lockfile_path, File::LOCK_EX)
daemon.lockfile = lockfile
status = daemon.restart
when 'restart-logging'
lockfile = acquire_lock(lockfile_path, File::LOCK_EX)
daemon.lockfile = lockfile
status = daemon.restart_logging
when 'soft-restart'
lockfile = acquire_lock(lockfile_path, File::LOCK_EX)
daemon.lockfile = lockfile
status = daemon.soft_restart
when 'usage'
usage
Expand Down Expand Up @@ -105,7 +123,12 @@ def self.usage
def self.acquire_lock(lockfile_path, flags)
if Workhorse.lock_shell_commands
lockfile = File.open(lockfile_path, 'a')
lockfile.flock(flags)
result = lockfile.flock(flags)

if result == false
lockfile.close
fail LockNotAvailableError, 'Could not acquire lock. Is another workhorse command already running?'
end

return lockfile
end
Expand Down
5 changes: 4 additions & 1 deletion lib/workhorse/worker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -296,11 +296,14 @@ def current_memory_consumption
def trap_log_reopen
Signal.trap(LOG_REOPEN_SIGNAL) do
Thread.new do
logger.reopen
logger&.reopen

if defined?(ActiveRecord::Base) && ActiveRecord::Base.logger && ActiveRecord::Base.logger != logger
ActiveRecord::Base.logger.reopen
end
rescue Exception => e
log %(Log reopen signal handler error: #{e.message}\n#{e.backtrace.join("\n")}), :error
Workhorse.on_exception.call(e)
end.join
end
end
Expand Down
Loading