Repository navigation
Fix shared worker result boundaries, falsy outputs, and shutdown classification #701
Description
Parent epic: #699. Independent first batch; applies to released v1 as well as the internal v2 proof.
Current defects
StepTaskExecutor.execute() catches handler execution, successful-handler logging, and completeTask() in one error path. A completion RPC failure therefore calls failTask() as if the handler failed. Its shutdown decision uses only the exception name. PgflowSqlClient.completeTask() serializes output || null, corrupting valid 0, false, and "".
Implementation
Separate the handler outcome from result reporting. Only an actual handler/execution failure belongs in the handler-failure path. Completion/failure RPC or result serialization errors, and logging errors after an outcome exists, must not relabel that outcome or execute the handler again. Preserve existing adapter/platform structure; do not add a generic retry framework.
Once a handler outcome exists, telemetry failure must not prevent its result-reporting attempt. Isolate completion/failure logging from settlement; retain the original outcome and surface telemetry errors separately. A logger failure is neither a business failure nor a reason to retry transport. Pre-handler logging failure must not manufacture a handler-failure settlement. This needs a local boundary, not a logging framework.
Use nullish output handling: undefined and null normalize to JSON null; the valid falsy outputs survive SQL serialization. Check the actual worker shutdown signal, not only error.name. An unrelated handler AbortError with a live shutdown signal is a handler failure. An arbitrary thrown reason with an aborted shutdown signal is interruption, not a new business failure.
If shutdown occurs after the handler returns success, keep that success separate; do not call failTask(). Document whether the reporting attempt proceeds or surfaces as interrupted/unknown acknowledgement. Do not promise recovery of in-memory results after process death.
For v1, surface/log acknowledgement failure without an automatic blind settlement-retry loop: its callbacks have no claim-replay contract. V2's bounded same-result replay belongs to #706, after #704 exists.
Tests and gate
- Handler success plus completion RPC rejection never calls
failTask(); handler invocation count remains one. Failure-RPC rejection does not cause a second failure report or handler execution. - A throwing completion logger still attempts
completeTask()once with the original result. A throwing failure logger still attemptsfailTask()once with the original handler error when the shutdown signal is live. Pre-handler logging errors do not manufacture a handler failure. - Post-success logging/serialization/reporting failure never enters the business failure path. Preserve an observable error through the existing worker error boundary without suppressing the settlement attempt because telemetry failed.
- Already-aborted execution does not start a handler or report a handler failure. Actual shutdown with a non-
AbortErrorreason does not report failure; unrelatedAbortErrordoes. - Shutdown after a successful handler never reports business failure. A real failure without shutdown calls
failTask()once with the original failure. - Actual SQL storage preserves
0,false,"", JSON null, and undefined→null; mocks alone do not prove SQL serialization.
Add unit checks using observable calls, not private methods, and focused SQL-client integration tests. Run current core unit/types and edge-worker unit/types plus the affected integration target. Preserve JSR Edge and npm Node/Bun runtime paths; add no new dependency.
Depends on: none. Not blocked by protocol design, #696, #697, or #694 diagnosis. Applicable final evidence still belongs to #706.
Activity
chore: add worker result boundary patch changeset
fix(worker): preserve outcomes across result reporting failures (#708)