Repository navigation
Log a task failure in the daemon loop instead of stopping the daemon - #6
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
The new documentation incorrectly claims ignored failures are not logged, contradicting the implementation.
2 open findings
What changed in this PR
Keeps the scheduler daemon running after task failures while preserving throwing behavior outside run().
Changes:
- Tracks daemon-loop execution and logs task failures instead of rethrowing them.
- Documents daemon failure handling.
- Adds sequential and forked-mode regression tests.
| File | Description |
|---|---|
src/Scheduler.php |
Implements daemon failure tolerance. |
src/Event/FailureEvent.php |
Documents daemon-specific behavior. |
tests/SchedulerTest.php |
Adds failure-handling tests. |
README.md |
Documents operational behavior. |
CHANGELOG.md |
Records the fix. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A child failing between the end of the loop and the shutdown of the others was thrown at whoever stopped the daemon. The flag is now dropped by terminateChildren(), which also leaves the end of run() as it is on master. The note on the daemon moves from the FailureEvent class docblock to ignore(), and the new tests further down, so the pull request no longer conflicts with #4 and #9. The comments of the work command said its signal handler catches a task failure, which it no longer does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
KalimeroMK
approved these changes
Oct 12, 2026
| $this->stopChildren(); | ||
| } finally { | ||
| $this->terminating = false; | ||
| $this->looping = false; |
Contributor
There was a problem hiding this comment.
nit: this reset has no test: removing it keeps the suite green, and a later tick() would then swallow failures. A forked-mode test that stops run() while a child is still running and then expects a later tick() to throw would pin it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Problem
One failing task stops the whole
schedule:workdaemon and sendsSIGTERMto every other task that is running.An unignored failure was thrown out of
Scheduler::run(), fromtick()in sequential mode and fromreapChildren()in forked mode, and thefinallyofrun()then terminated the remaining children. TheSIGCHLDhandler ofWorkCommanddid the same throughstop(). No listener ignores a failure by default, so any task exiting with an error brought the schedule down. This contradicted thereapChildren()docblock ("neither kills its siblings").How to reproduce
Save as
repro.phpin the repository root and runphp repro.php. It uses the test doubles fromtests/Support.repro.php
On
master:With this change:
The script uses the sequential loop so that it needs no
ext-pcntl; the forked loop ends the same way onmaster, with "The task process exited with code 1."Fix
While
run()is driving the ticks, a task failure that no listener ignored is logged as an error instead of thrown. The failure event and the schedule's failure listeners are still notified as usual.Outside of
run()nothing changes:tick()andreapChildren()still throw, soschedule:runstill exits with a failure.Why this way
WorkCommandand start the loop again. Leavingrun()already terminates every running child, so the damage is done before the command sees the error; and it would fix the command only, notrun()for anyone calling it directly.tick()would stop throwing as well, andschedule:runwould exit with 0 after a failed task. A cron entry relies on that exit code.run()to choose between the two. A daemon that dies on the first failing task is not something to opt out of; there is no use for the other value.FailureEventlisteners only. An ignored failure was already logged as an error, so this is the same record.Things to know
FailureEvent::ignore(), not on the class docblock, which Keep a failing task from blocking the rest of the schedule #4 rewrites.FailureEventlistener, a failure in the daemon is now silent rather than fatal. The README says so.Tests
tick()still throws afterrun()has returned.The first two fail on
master.🤖 Generated with Claude Code