Skip to content

feat(ethflow-watcher): cap backoff: retries (COW-1083)#59

Closed
brunota20 wants to merge 1 commit into
feat/chain-forward-revert-data-cow-1082from
feat/backoff-retry-cap-cow-1083
Closed

feat(ethflow-watcher): cap backoff: retries (COW-1083)#59
brunota20 wants to merge 1 commit into
feat/chain-forward-revert-data-cow-1082from
feat/backoff-retry-cap-cow-1083

Conversation

@brunota20

Copy link
Copy Markdown

Summary

  • ethflow-watcher's apply_submit_retry previously wrote an empty backoff:{uid} presence flag on every retriable submit failure — including the TryNextBlock fallback for unparseable orderbook envelopes (the case the issue names explicitly). With no payload, the same dead placement would retry indefinitely on every log re-delivery, bounded only by re-delivery frequency. This change persists a per-UID retry count in the marker's value (ASCII u32) and upgrades to dropped: with a Warn log after MAX_BACKOFF_RETRIES = 5 consecutive retries.
  • Backwards-compat: pre-COW-1083 empty b"" markers decode to count 0 so previously-set backoffs get a fresh attempt instead of being prematurely dropped on rollout. Garbage marker values reset to 0 (defensive against manual store edits).
  • No change to the safe-default classification (TryNextBlock on None data stays correct for transient flakiness) — that part is explicitly out of scope per the issue. This is bounded retry semantics on top of the existing classification.

Why this is M4

Surfaced by jeffersonBastos's PR #55 (M3 mirror) review, thread on crates/shepherd-sdk/src/cow/error.rs:82. Latent in normal operation (host forwards parseable envelopes after COW-1075, so classify_api_error returns Drop for permanent rejections), but the gap fires when the orderbook returns a non-JSON 4xx body (e.g. an HTML error page from a CDN) or if a future host change accidentally drops the envelope again.

Tests

  • 19/19 ethflow-watcher tests pass (17 existing + 2 new)
  • New submit_transient_error_at_cap_upgrades_to_dropped_warn — Nth retry of an unparseable rejection upgrades to dropped: + Warn log
  • New submit_transient_error_with_legacy_empty_marker_resets_counter — pre-COW-1083 empty marker bumps to "1", no premature drop
  • cargo clippy -p ethflow-watcher --all-targets -- -D warnings clean

Stacked on

PR #58 (feat/chain-forward-revert-data-cow-1082) → PR #57 → upstream M4 epic (TBD).

AI-assistance disclosure

Authored with Claude (Opus 4.7); reviewed end-to-end and validated against the existing ethflow-watcher strategy tests before push.

…OW-1083)

The strategy's `apply_submit_retry` previously wrote an empty
`backoff:{uid}` marker on every retriable submit failure (including
the `TryNextBlock` fallback for unparseable orderbook envelopes). The
marker was a presence flag with no payload, so on every supervisor
reconnect / engine restart the same dead placement would retry
indefinitely — bounded only by log re-delivery frequency.

This change persists a per-UID retry count in the marker's value
(ASCII `u32`) and upgrades to `dropped:` after `MAX_BACKOFF_RETRIES =
5` consecutive retries. The upgrade emits a Warn-level log line so
the operator sees the structural issue (flaky CDN, indexer hiccup,
poisoned envelope) rather than silently accumulating retries.

## Changes

- `modules/ethflow-watcher/src/strategy.rs`:
  - New const `MAX_BACKOFF_RETRIES = 5`.
  - New helper `read_backoff_count` that reads + parses the marker
    payload; pre-COW-1083 empty markers decode to 0 so previously-set
    backoff: rows still get a fresh attempt (no premature drop on
    rollout).
  - `apply_submit_retry`'s retriable branch now reads the prior
    count, increments, and either writes the new count or upgrades
    to `dropped:` (clearing the stale `backoff:`) at the cap.
  - Cap-upgrade log line carries the retry-count and message: "...
    after 5 retries on transient/unparseable rejection ...".

## Tests

- 19/19 ethflow-watcher tests pass.
- New `submit_transient_error_at_cap_upgrades_to_dropped_warn`:
  seeds `backoff:{uid} = "4"`, triggers a `data: None` rejection
  (the unparseable case the issue names explicitly), asserts:
    * `dropped:{uid}` is now set
    * `backoff:{uid}` is cleared (single outcome marker at rest)
    * exactly one Warn log line containing "ethflow dropped" +
      "retries"
- New `submit_transient_error_with_legacy_empty_marker_resets_counter`:
  backwards-compat — a pre-COW-1083 empty `b""` marker is treated
  as count 0, bumped to "1" on first retry rather than prematurely
  dropping. Protects in-flight backoffs across the rollout.
- Existing `submit_transient_error_writes_backoff_marker_and_returns`
  extended with an assertion that the first retry persists
  `backoff:{uid} = "1"`.
- `cargo clippy -p ethflow-watcher --all-targets -- -D warnings`
  clean.

## Why this is M4

Surfaced by jeffersonBastos's PR #55 (M3 mirror) review, thread on
`crates/shepherd-sdk/src/cow/error.rs:82`. Latent in normal
operation (the host forwards parseable envelopes after COW-1075, so
`classify_api_error` returns `Drop` for permanent rejections), but
the gap fires when the orderbook returns a non-JSON 4xx body
(e.g. an HTML error page from a CDN) or if a future host change
accidentally drops the envelope again. Bounded retry semantics
close the latent risk without changing the safe-default
classification (still `TryNextBlock` on `None` data — that part is
explicitly out of scope per the issue).

AI-assisted authoring with Claude (Opus 4.7); reviewed end-to-end
and validated against the existing ethflow-watcher strategy tests
before push.
@linear-code

linear-code Bot commented Jun 22, 2026

Copy link
Copy Markdown

COW-1083

@brunota20

Copy link
Copy Markdown
Author

Closing as superseded by the observe-not-submit redesign in PR #62 (M2 layer) + PR #63 (BLEU-855 / M3-M4 layer).

This PR caps the backoff: retry counter so a persistent transient orderbook rejection eventually flips to dropped: instead of retrying forever. Useful guard under the original BLEU-833 submit design, but the M3/M4 redesign removes the submit path entirely — there is no backoff: marker to cap and no apply_submit_retry flow to exit. MAX_BACKOFF_RETRIES, read_backoff_count, and the related state machine are deleted in PR #63.

Background on the redesign and the empirical evidence (2026-06-22 Sepolia soak: 3 EthFlow placements dropped by shepherd but fulfilled on the orderbook at the same UIDs) is captured in COW-1076: https://linear.app/bleu-builders/issue/COW-1076

No code from this PR is retained.

@brunota20 brunota20 closed this Jun 23, 2026
jean-neiverth added a commit that referenced this pull request Jul 6, 2026
…delity gaps

Add MockLocalStore::with_namespace() for per-module key isolation,
matching the engine's separate-redb-file-per-module semantics.
Document the remaining fidelity gaps (no transaction semantics, no
concurrent access) as deferred to the MockRuntime refactor (#94).

Closes #59
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