Repository navigation
Don't record FAILURE when on_success raises after SUCCESS was stored - #10759
Conversation
Task.on_success runs after mark_as_done. If it raised, the exception reached the tracer's outer handler, which overwrote the stored SUCCESS with FAILURE, fired the errbacks, and skipped the task_success signal and after_return. Log the exception instead, the same way signal receivers' exceptions are handled, and finish the success path. Refs celery#10724
MehrazRumman
left a comment
There was a problem hiding this comment.
Thanks @gyanu2507, this is the right fix for the on_success half of #10724. I compared main and this branch by running the tracer with eager=False and a task whose on_success raises:
main |
this PR | |
|---|---|---|
mark_as_failure called (with call_errbacks=True) |
yes | no |
task_failure / task_internal_error signals |
both sent | not sent |
task_success signal |
skipped | sent |
after_return |
skipped | called with SUCCESS |
| tracer return value | ExceptionInfo |
4 |
Catching the exception next to the hook, logging it, and continuing down the success path matches how signal receiver errors are already handled.
1. Eager mode now swallows the error
On main, the outer handler does if eager: raise, so task.apply() (and task_always_eager) propagates an exception from on_success to the caller:
main: add.apply((2, 2)) -> raises RuntimeError('hook failed')
this PR: add.apply((2, 2)) -> EagerResult SUCCESS 4, error only logged
Many projects run their test suites in eager mode, so a broken on_success hook that fails a test today would pass silently after this change. The reasoning in the code comment ("SUCCESS is already stored") also applies less in eager mode, where nothing is stored by default. Could you keep the old behaviour there, e.g. if eager: raise inside the new except? If swallowing is intended in eager mode as well, please say so in the changelog.
2. after_return has the same problem
task_after_return(...) runs a few lines later, also after mark_as_done, and it is still unprotected. With an after_return that raises, both main and this branch call mark_as_failure with errbacks and send task_failure for a task that succeeded. For the failure states it's arguably worse: on_error has already recorded the failure, and the outer handler records it a second time. It's the same class of bug, so it would fit in this PR, or in a follow-up linked from #10724 so the issue isn't closed with that path still open.
3. Description and test accuracy
- The description says the stored SUCCESS is overwritten with FAILURE. On key-value backends it isn't:
KeyValueStoreBackend._store_resultreturns early when the current status is SUCCESS, so in my run the stored state stays SUCCESS onmaintoo. The real effects there are the errbacks, the chord part counted as a failure, and the failure signals. The database backend has no such guard, so the overwrite does apply there. Worth making precise. - For the same reason,
assert ...get_state('id-1') == states.SUCCESSin the new test also passes onmain(the test app uses the cache backend). The assertions that actually pin the fix arehandle_failure.assert_not_called(), thetask_successreceiver andafter_return. That's fine, but a short comment would stop someone from trimming the test down to the state check. - The test doesn't check that the error is logged. Patching
celery.app.trace.logger(as other tests in this file do) and assertinglogger.exceptionwas called would cover the new branch fully.
4. Smaller points
Rejectis a subclass ofException, soraise Reject(...)fromon_successis now swallowed. Onmainit reached the outerexcept Reject: raise. Rejecting a task whose SUCCESS is stored and whose callbacks were dispatched is questionable anyway, but please make the choice explicit (eitherexcept Reject: raisebefore the new handler, or a line in the comment).- Errors in
on_successno longer producetask_failureortask_internal_error, only an ERROR log record. Anyone alerting on those signals will stop seeing broken hooks. That deserves a changelog line and a sentence in theon_successsection ofdocs/userguide/tasks.rst.
LGTM once the eager behaviour is decided. The after_return case can be a follow-up if you prefer to keep this PR small.
…turn - Re-raise from on_success in eager mode so apply() and task_always_eager still surface a broken hook. - Let Reject from the hook reach the outer handler as before. - Handle after_return the same way: it also runs after the outcome is recorded, and a raise there recorded a failure for a successful task, or a second failure for a failed one. - Assert the log call in the test and note which assertions pin the fix. - Document the behaviour in the task handler docs and the changelog.
|
Thanks @MehrazRumman, this was a really useful review. Pushed 45a2f34 on top of the merge from
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #10759 +/- ##
=======================================
Coverage 90.72% 90.72%
=======================================
Files 153 153
Lines 21533 21549 +16
Branches 2651 2653 +2
=======================================
+ Hits 19535 19551 +16
Misses 1685 1685
Partials 313 313
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. |
Resolve Changelog.rst: keep Unreleased entry for celery#10724 above 5.7.0rc1.
|
@MehrazRumman thank you for the detailed review. The follow-up in
I just merged current |
|
@ChristianMurphy can you check this one too? |
|
i think the unit test coverage can be improved as well @gyanu2507 |
|
I tested this with a prefork worker, against Redis and SQLite ( Before merging:
Also, the |
… quiet Restore the RST section underline under 5.7.0b1 that a main merge dropped. Extend the on_success-raises regression test so task_failure and task_internal_error are asserted not to fire.
|
@auvipy Restored the Also tightened the existing unit coverage: when |
is this already covered? the codcov is reporting some missing coverage. not specific though, can you cross check? |
|
I checked 34c68d3. The changelog heading renders again, and the test now checks that These are still open from my last comment.
except (Reject, MemoryError):
raiseThe No test raises Also, the merge adds an extra blank line above |
Keep MemoryError (and Reject) propagating from on_success/after_return like main. Add after_return Reject and on_success MemoryError tests. Restore Unreleased changelog entries after merging main.
|
Addressed the remaining notes from @ChristianMurphy:
|
Description
Refs #10724 (the
on_successpath). #10752 covers the other path in that issue, a pool child lost after the store.Task.on_successandTask.after_returnrun after the task's outcome is recorded. If either hook raised in a worker, the exception reached the tracer's outerexcept Exception, which callson_erroragain. For a task that succeeded, that meant:task_failureandtask_internal_errorwere sent, andtask_successandafter_returnwere skipped (for anon_successraise)KeyValueStoreBackend._store_resultreturns early when the stored status is SUCCESS.For a task that failed, a raising
after_returnrecorded the failure a second time.With this change, a worker logs an exception from either hook at
ERRORlevel, with the task name and id, and the recorded outcome stands. That matches how exceptions from signal receivers are already handled. Specifically:apply(),task_always_eager) the exception still propagates to the caller, as onmain, so test suites keep seeing broken hooks.Rejectraised from a hook still reaches the outerexcept Reject: raise, as onmain.Because broken hooks no longer send
task_failureortask_internal_errorin a worker, I added a changelog entry and a sentence to theon_successandafter_returnsections ofdocs/userguide/tasks.rst.Tests
In
t/unit/tasks/test_trace.py:test_trace_on_success_raises_after_success_was_stored: non-eager trace with a raisingon_success.handle_failureisn't called,task_successandafter_returnrun, and the error is logged. A comment notes that the state check alone also passes onmainwith the cache backend.test_trace_on_success_raises_eager_propagatesandtest_trace_after_return_raises_eager_propagates: eager mode still raises.test_trace_on_success_raises_reject_is_not_swallowed:Rejectstill propagates.test_trace_after_return_raises[False/True]: a raisingafter_returnrecords no failure for a successful task, and exactly one for a failed task.t/unit/tasksandt/unit/workerpass locally on the branch after the merge frommain(1806 passed). flake8 and isort are clean.