Skip to content

Fix #67: dispatch CircuitFailed when an asynchronous task ends without a result - #91

Open
corgab wants to merge 2 commits into
mainfrom
fix/67-circuit-failed-event
Open

Fix #67: dispatch CircuitFailed when an asynchronous task ends without a result#91
corgab wants to merge 2 commits into
mainfrom
fix/67-circuit-failed-event

Conversation

@corgab

@corgab corgab commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Summary

The asynchronous flow announced success through CircuitCompleted but had no event for the three ways a dispatched task ends without a result: the backend reports FAILED/CANCELLED (TaskFailedException), the polling budget is exhausted (QuantumExecutionException::pollingExhausted), or the task completes without counts (QuantumExecutionException::malformedResponse). In each case PollQuantumTask::handle() threw and the only trace was a row in failed_jobs, which is not an application-level API. Laravel exposes both sides of its own asynchronous primitives (JobFailed next to JobProcessed, NotificationFailed next to NotificationSent); this package now does the same.

Plan

  • Add a readonly CircuitFailed event with the payload the issue asks for and dispatch it from the polling job right before each throw, through the injected Dispatcher like CircuitCompleted. Keep the exceptions so the job still fails and is recorded.
  • Document it next to CircuitCompleted.

One deliberate departure from the issue text: Quantum::fake() does not dispatch CircuitFailed itself. The fake is a device, and neither event belongs to a device: CircuitCompleted is already dispatched only by the job, and a job run against the fake (the documented way to test the async flow) would otherwise announce the failure twice. The fake keeps reporting the stubbed status, and the job dispatches exactly one CircuitFailed; a test covers that count.

Changes

  • src/Events/CircuitFailed.php (new): driver, circuit, taskArn, status (TaskStatus, never null: FAILED/CANCELLED, the last non-terminal status when the budget ran out, or COMPLETED for a task without counts), reason (the exception message).
  • PollQuantumTask: the three failure branches go through a private abandonTask() that persists the state, dispatches CircuitFailed and throws. A listener that throws is reported and swallowed, so the job still fails with the task exception whose message carries the ARN. Class docblock updated.
  • QuantumFake::respondWithTaskStatus() docblock states that the events belong to the polling job.
  • README: events table row, listener example, the failure paragraph in "Asynchronous Execution", the "events never fire on failure" sentence rewritten to distinguish synchronous from asynchronous failures, and the Quantum::fake() paragraph explaining the single dispatch. CLAUDE.md gains one conventions bullet.

No config change, no new dependencies, no change to what the job throws or persists.

Tests

  • PollQuantumTaskTest: CircuitFailed dispatched (driver, ARN, circuit, status, reason) and CircuitCompleted not dispatched for Failed and Cancelled; dispatched with Running when the budget is exhausted; dispatched with Completed and a "no measurement counts" reason on null counts; not dispatched on success.
  • tests/Feature/CircuitFailedEventTest.php (new): the job run against Quantum::fake() dispatches exactly one CircuitFailed for Failed and Cancelled; polling the fake directly dispatches nothing; a throwing listener does not replace the TaskFailedException.
  • All new assertions fail on main (the event class does not exist).

Local: pint passed, pest 800 tests passed.

Closes #67

… result

The asynchronous flow announced success through CircuitCompleted but had
no event for the three ways a dispatched task can end without a result:
the backend reports FAILED or CANCELLED, the polling budget runs out, or
the task completes without counts. Application code could only react to
those by reading failed_jobs.

PollQuantumTask now dispatches Aether\Events\CircuitFailed, carrying the
driver, the circuit, the task ARN, the last status read and the reason,
right before throwing in each of those branches; the exception still
propagates so the job fails as before. Quantum::fake() mirrors it, once per
task, when a stubbed Failed or Cancelled status is polled. The README
events table, the asynchronous section and the fake section document it.

Closes #67
Dispatching CircuitFailed from Quantum::fake() as well as from the polling
job made a job run against the fake announce the failure twice, and the
fake never dispatched CircuitCompleted either; the fake now only reports
the stubbed status, as a real backend would, and both events belong to the
job. The event's status is no longer nullable, since every producer knows
it, and a listener that throws is reported and swallowed so the job still
fails with the task exception whose message carries the ARN. Class
docblocks and docs describe the final behaviour.
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.

[events] Add a CircuitFailed event for asynchronous tasks that end without a result

1 participant