Skip to content

Fix critical race conditions and signal safety issues - #1228

Open
benroeder wants to merge 6 commits into
circus-tent:masterfrom
sohonetlabs:master
Open

Fix critical race conditions and signal safety issues#1228
benroeder wants to merge 6 commits into
circus-tent:masterfrom
sohonetlabs:master

Conversation

@benroeder

Copy link
Copy Markdown

Summary

  • Fixed async-signal-safe signal handling to prevent deadlocks in signal handlers
  • Made start_watchers idempotent to reduce ConflictError during arbiter startup
  • Fixed file descriptor duplication error in stream redirector
  • Properly yield _start_watchers in manage_watchers to prevent race conditions

Test plan

  • All existing tests continue to pass
  • Added regression tests for each specific issue fixed
  • Verified signal handling no longer performs unsafe operations in signal context
  • Confirmed start_watchers properly skips already-running watchers

These fixes address critical stability issues that were causing production failures, particularly around signal handling deadlocks and race
conditions during process management.

benroeder added 6 commits May 23, 2025 15:35
Root cause: The stream redirector could raise "ValueError: fd X added twice"
when there was a state mismatch between its internal _active dictionary and
the Tornado IOLoop's registered handlers. This occurred during rapid process
respawning or when exceptions left the redirector's state inconsistent.

The fix adds defensive programming to _start_one() and _stop_one():
- Before adding a handler, attempt to remove any existing one first
- Improve cleanup to handle both IOLoop and internal state consistently
- Maintain state consistency even if operations partially fail

This prevents the ValueError and allows the redirector to recover gracefully
from state mismatches that can occur due to race conditions or improper
cleanup during process lifecycle events.

Added regression test that reproduces the exact error condition from
production and verifies the fix prevents it.
Root cause: The synchronized decorator prevents concurrent execution of
arbiter_start_watchers. During arbiter initialization, start_watchers()
is called. If a client sends a 'start' command at that exact moment,
it receives ConflictError because the command is already running.

This fix makes start_watchers check if watchers are already running
(using watcher.is_stopped()) before attempting to start them. Watchers
that are already running are skipped, making the operation idempotent.

Impact:
- Reduces ConflictError occurrences in production environments
- Makes concurrent start commands more resilient
- No breaking changes - this is purely defensive programming

The error typically occurs when monitoring systems or automated scripts
send start commands while the arbiter is still initializing or when
all watchers are already running.
Root cause: In manage_watchers(), when handling on-demand watchers,
the code was calling _start_watchers() without yielding it. Since
_start_watchers is a coroutine decorated with @gen.coroutine, this
meant the coroutine was not being executed properly - it would just
return a Future object instead of actually starting the watchers.

This fix adds the missing yield statement to ensure _start_watchers
executes correctly when triggered by socket activity for on-demand
watchers.

Impact:
- On-demand watchers now start correctly when socket activity occurs
- Eliminates potential race conditions from unyielded coroutines
- No breaking changes

Note: The ConflictError between manage_watchers and watcher_stop seen
in production logs is expected behavior - the synchronized decorator
prevents these operations from running concurrently.
After testing IOLoop behavior, discovered that:
- remove_handler() never throws exceptions, even for non-existent handlers
- The try/except blocks in _stop_one were unnecessary
- The error handling in _start_one was overly complex

This refactor simplifies the code by:
- Removing unnecessary exception handling in _stop_one
- Simplifying _start_one to just call remove_handler before add_handler
- Removing unreachable code paths

The fix still prevents the 'fd added twice' error but with cleaner,
more maintainable code. Added tests to verify IOLoop behavior and
ensure the simplified approach works correctly.
Root cause: Signal handlers were performing unsafe operations including
logging, dictionary lookups, and string formatting. These operations can
cause deadlocks if a signal interrupts code holding locks that the
signal handler tries to acquire, or crashes from memory corruption.

The fix moves all unsafe operations from signal context to the main
thread using Tornado's add_callback_from_signal(). The signal handler
now only performs the single safe operation of scheduling a callback.

Changes:
- Signal handler only calls add_callback_from_signal()
- All logging, lookups, and processing moved to _handle_signal_in_main_thread()
- quit() and reload() methods updated since they now run in main thread
- Added emergency fallback using only write() and _exit() if callback fails

Impact:
- Prevents deadlocks from signal handlers trying to acquire locks
- Eliminates undefined behavior from non-async-signal-safe operations
- Makes signal handling robust under high load conditions

This is a critical fix for production stability.
The test now verifies that our signal handler implementation is safe,
rather than demonstrating the old unsafe behavior.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant