Repository navigation
Failure hooks: report every failed invocation to the handler SDK, with reason and source - #190
Draft
shawnburke wants to merge 3 commits into
Draft
shawnburke wants to merge 3 commits into
shawnburke wants to merge 3 commits into
Conversation
The agent now reports failures it sees on its own (queue full, expired while queued, no report before the timeout) on a new DispatchEvents stream, and re-queues a failed invocation on RetryInvocation. The queue behind the stream is unbounded so no failure is dropped, and it only exists for clients that set DispatchRequest.failure_events, so older SDKs are unaffected. The Go SDK adds WithPreDispatchHook and WithPostDispatchHook. The post hook runs for every invocation, plus every agent failure event, with a reason (HANDLER_ERROR, TIMEOUT, NO_RESOURCES) and a source (APPLICATION or RUNTIME), and can ask for a retry. A timeout while a Cortex API call is in flight is classed RUNTIME. invokeHandler no longer lets a timed-out handler write to the report while it is being sent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e up on Python gets pre_dispatch and post_dispatch on AxonClient, matching Go. Failure events are read on their own thread and run on their own pool, so they arrive even while every handler slot is busy. At a handler's timeout the SDK now reports the timeout itself and drops whatever the handler does afterwards, since a Python thread can't be stopped. The agent now sends each invocation an absolute deadline: when it stops waiting and reports the failure itself. A saturated client used to read invocations out of the stream buffer long after the agent had reported them as failed, and run them anyway, so a failure hook replaying them would have processed them twice. Both SDKs now skip an invocation past its deadline and cap a handler's timeout at it, classing that timeout as RUNTIME. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collaborator
Author
What this means for handler authorsThe problemWhen a webhook message failed to process, it disappeared without a trace. A message could fail because:
Your code got no signal. The only way to find out was to notice later that data hadn't updated. Even when you did notice, you couldn't tell whether your handler or Axon was to blame. The solutionYou can now register a failure hook with the Axon SDK. Axon calls it for every message that fails, including the ones that never reached your handler. It passes:
What the hook does is up to you: save the message to a topic, a table or a file, raise an alert, or ask Axon to try it again. Axon doesn't store or replay anything itself; your hook decides what happens. Examplefrom cortex_axon import AxonClient, FailureAction, FailureSource
from cortex_axon.handler import cortex_webhook
# A normal webhook handler. If it raises, the failure hook is called.
@cortex_webhook(id="employee-sync")
def employee_sync(context):
update_records(context.args["body"])
# Called for every message that fails.
def on_failure(info, failure):
# Keep the failed message so it can be replayed later.
dead_letter_topic.publish({
"invocation_id": info.invocation_id,
"handler": info.handler_name,
"attempt": info.attempt,
"reason": failure.reason.name, # HANDLER_ERROR, TIMEOUT or NO_RESOURCES
"source": failure.source.name, # APPLICATION or RUNTIME
"message": info.args, # the original webhook: body, content-type, url
})
# Optionally, have Axon try Axon-side failures again (up to 5 attempts).
if failure.source == FailureSource.RUNTIME and info.attempt < 3:
return FailureAction.RETRY
AxonClient(scope=globals(), on_failure=on_failure).run()Good to know
|
The post-dispatch hook ran on success too, so every hook began by checking for a failure, and the pre-dispatch hook only made sense paired with that success call. Neither was part of the ask. Python now takes on_failure and Go WithFailureHook; both run only for failures and return FailureAction (IGNORE / RETRY). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Summary
Adds a failure hook to the Go and Python handler SDKs (
WithFailureHook/on_failure) that fires for every failed invocation. The hook receives the original message and a reason that separates handler failures from Axon failures. A customer can then route failed webhooks to a store of their own choosing and replay them later. Before this change, those messages failed silently. Tracks COR2-1098.The three failure cases asked for, and what the hook receives for each:
reasonsourceHANDLER_ERRORAPPLICATIONTIMEOUTAPPLICATION, orRUNTIMEif it was waiting on a Cortex API call or for a free handlerTIMEOUTRUNTIMENO_RESOURCESRUNTIME,caller_rejected=trueNO_RESOURCESRUNTIMEEach failure is reported once, by whichever side saw it.
Python usage
Design
DispatchEventsserver stream andRetryInvocationRPC.FailureReason/FailureSourceenums andInvocationFailure/DispatchEventmessages.DispatchRequest.failure_events.DispatchHandlerInvoke.attemptand.deadline.Dispatchwhile all its handler slots are busy, which is exactly when the agent has failures to report. Events are read on a separate thread and run on a separate pool.axon_handler_failure_events_pendingshows a consumer that has fallen behind. The agent only queues events for clients that setfailure_events, so older SDKs see no change and the queue can't grow for them. An event whose send fails is put back at the front of the queue.completeInvocationnow checks and removes the outstanding request atomically, so two reports can't both win.attempt+1and a hardcoded cap of 5 attempts. The hook seesattemptand decides when to stop.caller_rejected=true, so a replay system can avoid double-delivering a message the sender will also retry.invokeHandlerused to let a timed-out handler keep writing to the report while it was being sent, a race that was already on main. The outcome now comes back over a channel, and logs go through a lock.Compatibility
DispatchEvents/RetryInvocationreturnUnimplemented: the SDK logs once and stops asking; it does not redial in a loop.Known limits
TIMEOUT/RUNTIME("did not report") rather thanNO_RESOURCES. The source is still right.Test plan
TIMEOUT→ retry → attempt 2; a client-reported failure is not repeated; retry rejected at the cap;caller_rejected.go test ./...passes.go test -race ./...):Unimplementedagent is not redialed;failure_eventsflag.ruffclean):APPLICATIONand its late result is dropped;RUNTIME;DRYRUN) with the Python SDK and the HTTP webhook endpoint:HANDLER_ERROR/APPLICATION, then a successful retry as attempt 2.TIMEOUT/APPLICATION, and the late result was dropped.🤖 Generated with Claude Code