Skip to content

Track command processing errors on CommandState (errorsCount, error, modifiedAt) - #103

Merged
pditommaso merged 1 commit into
fix/cmd-queue-retry-on-handler-exceptionfrom
feat/cmd-queue-error-tracking
Jul 31, 2026
Merged

Track command processing errors on CommandState (errorsCount, error, modifiedAt)#103
pditommaso merged 1 commit into
fix/cmd-queue-retry-on-handler-exceptionfrom
feat/cmd-queue-error-tracking

Conversation

@pditommaso

Copy link
Copy Markdown
Contributor

Stacked on #102 (which is stacked on #100). Base is fix/cmd-queue-retry-on-handler-exception. Merge order: #100#102 → this.

Recovers the error tracking originally released as 0.6.0 (e54229e, #89), which #100's revert to 0.4.0 dropped. VERSION is deliberately untouched — functionality only; numbering is a separate decision.

Why this belongs with #102

#89's own changelog says it plainly: it exists because of #87. Once a thrown handler is retried instead of terminal-failed, a command can retry indefinitely with nothing recording that it is happening. #102 on its own creates that blind spot; this closes it.

  • errorsCount — consecutive processing errors since the last successful processing
  • modifiedAt — last-write timestamp
  • error — now also carries the message of a transient (non-terminal) processing error. It holds the most recent message, transient or terminal; a terminal failure is identified by status == FAILED, not by error being non-null.

recordError is best-effort: a failed write is logged and never changes control flow, so the command is still kept in the queue and retried. The streak resets on recovery with a single write, and only when there is something to reset, so healthy re-polls stay write-free. Backward-compatible — new fields default to 0/null when older serialized state is read.

One deliberate adaptation from e54229e

Not a verbatim cherry-pick. This tree still has executeWithTimeout, which wraps a handler exception:

throw new RuntimeException("Command execution failed", e);

#89 was written against #84, which had removed that method, so the handler's exception propagated directly and recording e.getMessage() was correct there. On 0.4.x it is not: every transient error on the execute() path would be stamped with the same useless "Command execution failed", defeating the whole purpose of the field.

recordError now records the root cause's message via a small rootMessage() helper, which is also correct for the checkStatus() path where the exception propagates directly.

This was caught by #89's own test, which asserts error == 'Persistent boom' and failed with Command execution failed before the adaptation:

state.error() == 'Persistent boom'
|     Command execution failed
CommandState[... status=SUBMITTED, error=Command execution failed, errorsCount=2, modifiedAt=...]

Note the rest of the mechanism was already correct in that run — errorsCount=2, modifiedAt set, status non-terminal. Only the message needed unwrapping.

Verification

🤖 Generated with Claude Code

Recovers the error tracking originally released as 0.6.0 (e54229e, #89), dropped
by the revert to 0.4.0 in #100. VERSION is deliberately untouched.

Companion to the retry fix in #102: once a thrown handler is retried instead of
terminal-failed, a command can retry indefinitely with nothing recording that it
is happening. These fields make that visible.

- errorsCount: consecutive processing errors since the last successful processing
- modifiedAt: last-write timestamp
- error: now also carries the message of a transient (non-terminal) processing
  error. It holds the most recent message, transient or terminal; a terminal
  failure is identified by status == FAILED, not by error being non-null.

recordError is best-effort — a failed write is logged and never changes control
flow, so the command is still kept in the queue and retried. The streak is reset
on recovery, with a single write and only when there is something to reset, so
healthy re-polls stay write-free. Backward-compatible: the new fields default to
0/null when older serialized state is read.

One deliberate adaptation from e54229e, required by this tree: 0.4.x still has
executeWithTimeout, which wraps a handler exception in a generic
RuntimeException("Command execution failed"). #89 was written against #84, which
had removed that method, so recording e.getMessage() verbatim was correct there
but here would stamp every transient error on the execute() path with the same
useless string. recordError now records the root cause's message via
rootMessage(), which is also correct for the checkStatus() path where the
exception propagates directly.

Caught by #89's own test asserting error == 'Persistent boom'; it failed with
'Command execution failed' before the adaptation.

Tests: the two specs from #89 plus its CommandState serialization coverage.
Module suite 18/18 green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@pditommaso
pditommaso requested a review from jordeu July 31, 2026 09:04
@pditommaso
pditommaso merged commit 90465d4 into fix/cmd-queue-retry-on-handler-exception Jul 31, 2026
3 checks passed
pditommaso added a commit that referenced this pull request Jul 31, 2026
#100 and #102 (which also carried #103) are now on master, so this branch
retargets there instead of stacking on the revert branch.

Resolution:
- VERSION and changelog changes are dropped from this branch entirely. Master
  merged #102/#103 without bumping cmd-queue past 0.4.0 or adding a changelog
  entry, so versioning for the whole 0.4.x line is one decision to make in one
  place, not something this PR should pre-empt.
- CommandServiceImpl and AbstractMessageStream keep this branch's side, which is
  master's tree plus the drain: the #102 catch and #103 recordError/rootMessage
  reached this branch by cherry-pick before they were squashed onto master, so
  both sides carry them and only the drain is genuinely new.

Verified: the diff against master is now the drain and nothing else — 5 files,
no VERSION, no changelog.

Signed-off-by: Paolo Di Tommaso <paolo.ditommaso@gmail.com>
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.

1 participant