Skip to content

Tolerate EBADF in control-bus reads; keep the control watcher alive - #33

Open
TON14 wants to merge 1 commit into
AMAP-ML:mainfrom
TON14:fix/control-bus-ebadf-at-shutdown
Open

Tolerate EBADF in control-bus reads; keep the control watcher alive#33
TON14 wants to merge 1 commit into
AMAP-ML:mainfrom
TON14:fix/control-bus-ebadf-at-shutdown

Conversation

@TON14

@TON14 TON14 commented Aug 13, 2026

Copy link
Copy Markdown

Fixes #32.

A run that completed all rounds and wrote its final reply crashed at shutdown with OSError: [Errno 9] Bad file descriptor raised from os.close() in _read_json_file: the control-watcher task had died on that exception mid-run, and the stored error resurfaced when the CLI awaited the watcher in its finally block — turning a fully successful run into a traceback and a non-zero exit (full analysis in #32).

Three small layers, all consistent with the tolerant-close style control_bus already uses in its walk-cleanup paths (_walk_nofollow, _ensure_dir_nofollow):

  1. control_bus: the two remaining bare os.close() calls in finally blocks (_read_json_file and the control-log reader) now tolerate OSError — a stray double-close elsewhere in the process can recycle the descriptor number between our open and close, and crashing the reader over it takes down an otherwise healthy run.
  2. cli / watch_control: one failed poll costs that poll, not the watcher — a dead watcher silently stops honoring stop/abort requests for the rest of the run.
  3. cli shutdown: an OSError stored in the watcher no longer overrides the outcome of the finished run.

Testing

  • New tests/test_control_bus_close.py: _read_json_file still returns the parsed payload when the close of its own descriptor raises EBADF (directory closes inside the path walk stay real, so the walk itself is untouched).
  • Full suite: 169 passed, 1 skipped.

Independent of #28 (different subsystem, different failure mode); branched directly off main so the two can be reviewed and merged in any order.

🤖 Generated with Claude Code

A run that completed all rounds and wrote its final reply crashed at
shutdown with OSError: [Errno 9] Bad file descriptor raised from
os.close() in _read_json_file: the stored exception of the control
watcher task resurfaced when the CLI awaited it in the finally block,
turning a fully successful run into a traceback and a non-zero exit.

Three layers, all consistent with the tolerant-close style the module
already uses in its walk-cleanup paths:

1. control_bus: the two remaining bare os.close() calls in finally
   blocks (_read_json_file, control-log reader) tolerate OSError - a
   stray double-close elsewhere in the process can recycle the
   descriptor number between our open and close.
2. cli watch_control: one failed poll costs that poll, not the watcher -
   a dead watcher would silently stop honoring stop/abort requests for
   the rest of the run.
3. cli shutdown: an OSError stored in the watcher must not override the
   outcome of the finished run.

tests: _read_json_file returns the parsed payload when its own close
raises EBADF (the walk's directory closes stay real).
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.

Successful run crashes at shutdown: unhandled EBADF from control-bus watcher turns a clean run into a non-zero exit

1 participant