Skip to content

fix(runtime): do not report a poison disposition that never committed (redux) - #73

Open
Thom Chiovoloni (thomcc-work) wants to merge 6 commits into
mainfrom
waldemort/fix-47-poison-disposition-reporting
Open

Thom Chiovoloni (thomcc-work) wants to merge 6 commits into
mainfrom
waldemort/fix-47-poison-disposition-reporting

Conversation

@thomcc-work

@thomcc-work Thom Chiovoloni (thomcc-work) commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Taking over from #49.

Correct orchestration poison reporting so that an acknowledgement error is not counted as a successful poisoning. Previously, poison accounting could happen before acknowledgement or regardless of its result.

The shared acknowledgement path now records the final result after retry handling, covering:

  • Orchestrations exceeding their attempt limit.
  • Poison handling for corrupted history.
  • Poison failures propagated from activities and child orchestrations.
Final acknowledgement result Reporting
Success Increment duroxide_orchestration_poison_total
Error Increment duroxide_orchestration_poison_failed_total and log at ERROR with the instance, poison attempt count, configured maximum, and provider error

Each final acknowledgement result increments one counter, not once per retry. A transient failure followed by a successful retry counts only as confirmed. A failed poison acknowledgement followed by a successful infrastructure-failure fallback does not count as confirmed poisoning.

The diagnostic deliberately says termination was not confirmed, rather than asserting that it did not occur. A write may have committed before its response was lost, or another dispatcher may have completed the work after lease expiry. These counters describe observed acknowledgement outcomes, not an exact ledger of durable terminations.

The public MetricsSnapshot shape remains unchanged. The new failed count is available separately through MetricsProvider::orchestration_poison_failed() on the existing observability handle.

Tests and reliability

The new poison-disposition suite contains 18 tests, covering:

  • Successful and rejected acknowledgements for both normal and corrupted-history poison handling.
  • Retryable failures followed by success, exhausted retries, and recovery on redelivery.
  • Lost responses after committed writes.
  • Activity and child-orchestration poison propagation, including infrastructure fallback.
  • Exact counters, structured error logs, exported metrics, and downstream MetricsSnapshot struct-literal compatibility.

Async poison tests run in single-threaded 1×1 mode. They retain the production five-second lease and use bounded, condition-based waits of up to 30 seconds, avoiding unrelated lease-expiry failures under load.

The reliability tests also make duplicate-event batching deterministic. The crash-recovery case explicitly fetches a batch and leaves its lease unacknowledged before restarting, verifies that fetch did not append history, then checks that recovery records both arrivals and exactly one completion.

Both metrics references and the changelog are updated.

Checklist

  • Tests added/updated
  • Regular tests pass locally (cargo nt)
  • Doc tests pass locally (cargo test --doc --all-features)
  • Docs updated
    • Existing docs updated where behavior changed
    • New doc added under docs/ if introducing a new area
    • Linked from docs/README.md

Links / Design notes

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Poison failures propagated through orchestration execution are still counted before their acknowledgement succeeds, and compatibility/documentation issues remain.

3 open findings
What changed in this PR

Corrects orchestration poison metrics so acknowledgement failures are not reported as confirmed poison dispositions.

Changes:

  • Records confirmed and failed poison acknowledgements separately.
  • Adds structured error logging and regression coverage.
  • Documents the new metrics and acknowledgement uncertainty.
File Description
src/​runtime/​dispatchers/​orchestration.rs Records poison metrics from acknowledgement results.
src/​runtime/​observability.rs Adds the failed-poison counter.
src/​runtime/​mod.rs Exposes failed-poison metric recording.
tests/​poison_disposition_tests.rs Tests success, failure, recovery, logs, and exports.
tests/​common/​fault_injection.rs Adds acknowledgement fault injection.
tests/​reliability_tests.rs Makes duplicate-event batching deterministic.
docs/​observability-guide.md Documents poison acknowledgement counters.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/runtime/observability.rs Resolved
Comment thread docs/observability-guide.md Resolved

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

A modified reliability test no longer exercises the crash window its name and surrounding documentation claim to cover.

1 open finding
3 resolved since last review

🧠 Review effort: Balanced

Comment thread tests/reliability_tests.rs Resolved

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

Outcome accounting is consistent, compatibility is preserved, and focused regression coverage supports the change; tests were not executed during this review.

0 open findings

1 resolved since last review

🧠 Review effort: Balanced

Waldemort Agent and others added 5 commits November 2, 2026 18:38
When a message exceeds max_attempts the runtime marks it as poison. The
poison marking, the backoff, and the attempt-count increment all commit
through a single ack keyed on the message's current lock token. If that
lease has expired the ack fails and all three are lost together, so the
message is never terminated -- it becomes visible again and is
redelivered for as long as the provider keeps handing out dead leases.

The runtime reported that outcome as a success:

- The ack result was discarded with `let _ = ...`, so a failed
  disposition was invisible. Operators saw an 'exceeded max attempts,
  marking as poison' warning followed by a separate, transient-looking
  'Invalid lock token' warning, with nothing connecting them or saying
  the message was still circulating.

- record_orchestration_poison() was then called unconditionally, so
  duroxide_orchestration_poison_total counted poison *attempts* rather
  than messages actually taken out of circulation. In the incident
  behind #46 that meant tens of thousands of increments while zero
  messages were ever poisoned.

Both poison paths (corrupted-history and normal) now route their ack
result through record_poison_disposition, which records the poison
metric only when the ack committed and otherwise logs at ERROR --
naming the instance, the attempt count, the underlying error, the fact
that the orchestration was NOT terminated, and where to look (a
provider handing out leases that are expired on arrival).

A new duroxide_orchestration_poison_failed_total counter, surfaced as
MetricsSnapshot::orch_poison_failed, makes the condition alertable.

The runtime cannot force a terminal write without a valid lease: both
ack_orchestration_item and abandon_orchestration_item require one by
contract. Closing that gap would need a lease-independent provider
operation, which is an API decision for the Provider trait and is left
out of this change deliberately.

tests/poison_disposition_tests.rs covers both directions: a disposition
that cannot commit must not be counted as a poisoning, and one that does
commit must be counted exactly once and report no failure. The second
test exists so the first cannot be satisfied by simply never recording
the metric. PoisonInjectingProvider gains expire_orchestration_lease(),
which makes every orchestration ack fail with a non-retryable 'Invalid
lock token' -- the condition a provider produces when the lease it
handed out was already dead.

Verified with the repository's own two-pass suite (./run-tests.sh):
1116 tests run, 1116 passed, 0 failed. With the fix reverted, the
failed-disposition test fails as expected.

Refs #47, #46

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@thomcc-work
Thom Chiovoloni (thomcc-work) force-pushed the waldemort/fix-47-poison-disposition-reporting branch from 6750323 to d3081e7 Compare November 2, 2026 18:46
Use 5s lease, with bounded 30s wait.

@tjgreen42 Todd J. Green (tjgreen42) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review by GPT-6.1 Sol.

No actionable findings in this revision. Reviewed poison-disposition accounting across direct and propagated poison, acknowledgement retries, lost acknowledgement responses, fallback acknowledgement, and redelivery. The current revision addresses the earlier compatibility, accounting, and reliability-test concerns.

Scope: static review of the diff and relevant runtime/provider/observability code and regression tests at ed64587. I did not run tests locally.

@tjgreen42

Copy link
Copy Markdown
Contributor

Review note from GPT-6.1 Sol: the PR description accurately explains the core acknowledgement-outcome reporting, but it does not fully reflect the current diff.

  • The new poison-disposition test file now contains 18 tests, rather than the nine stated in the description.
  • Reporting also covers poison failures propagated from activities and child orchestrations, not just exceeded-attempt and corrupted-history handling.
  • Coverage now includes retryable acknowledgement failures followed by success, exhausted retries, and public MetricsSnapshot struct-literal compatibility. The failed count is exposed separately through MetricsProvider::orchestration_poison_failed().
  • The diff also includes reliability-test changes that make duplicate-event batching deterministic and explicitly simulate a crash after fetching a batch but before acknowledging it.

The description’s statements about single-threaded 1×1 execution and bounded condition-based waits remain accurate. The latest commit retains the production lease duration and extends the bounded wait to 30 seconds to avoid unrelated acknowledgement failures under load.

@tjgreen42 Todd J. Green (tjgreen42) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

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.

3 participants