Skip to content

Step 11 — error handling with user-decided retry (keyed + keyless) - #7

Merged
rigazilla merged 3 commits into
mainfrom
feat/step-11b-retry
Sep 25, 2026
Merged

rigazilla merged 3 commits into
mainfrom
feat/step-11b-retry

Conversation

@rigazilla

Copy link
Copy Markdown
Member

Summary

Completes Step 11 — error handling with a user-decided retry model, for both keyed operations and keyless ping. Base operations never retry on their own: a failure surfaces as a typed HotRodClientException carrying the facts a caller needs (failure phase, tried nodes, ownersExhausted), and the caller opts into a retry via a bound view — cache.excluding(e) — because replay-safety/idempotency is the caller's call (D2).

Three commits:

  1. Step 11b — user-decided retry for keyed operations

    • Exclusion-aware selectServerForKey over the pure orderKeyCandidates() helper (owners → non-owner fallback, minus an exclusion set), reporting tried nodes and computing ownersExhausted honestly (ServerSelection.h, RetryContext.h).
    • Client-level proxyToNonOwner flag (default true, D5) gates the owner→non-owner fallback; disabling it throws ownersExhausted instead of proxying.
    • Op-level retry threading via a RetryView bound view: base op signatures stay retry-free; excluding(nodes)/excluding(exception) forward to private …Impl(args, const RetryContext&) methods, and every op unions its tried nodes into the thrown exception's triedNodes (unionNodes()).
    • Thread-safe routing/pool state: a single stateMutex_ guards topology + consistent hash + connection pool across the read loop and user/retry threads; the pool holds shared_ptr so an in-flight op's connection survives concurrent topology eviction.
  2. Step 11c — keyless-op retry (ping), mirroring the keyed strategy

    • ping() delegates to pingImpl(const RetryContext&); RetryView::ping() forwards the exclusion set, so cache.excluding(e).ping() works exactly like keyed ops.
    • selectAnyServer(ctx, triedOut) routes via orderKeyCandidates({}, allServers, exclude) (empty owners → all servers in topology order), sweeps to the next reachable server, and falls back to the seed connection when no topology is known yet (ping is often the first op). ownersExhausted is always false for keyless ops; the implementation is generic for future non-key ops.
  3. Docs reconciliation — tick Step 11c / mark Step 11 complete in STATUS, bring the drifted README back in line with STATUS, and change WORKFLOW.md/AGENTS.md to prescribe checking README after every commit.

Divergences from Java (documented in DECISIONS.md)

  • D2 — retry is user-decided (idempotency is the caller's call), not automatic; Java auto-retries up to maxRetries.
  • D5 — proxyToNonOwner gates owner→non-owner fallback and, when off, surfaces ownersExhausted rather than silently proxying.
  • Keyless routing uses deterministic topology order, not Java's round-robin balancer; COMMAND_TIMEOUT (0x86) is treated as transient + outcome-uncertain.

Tests

  • Unit: 204/204 (+ exclusion-aware selection, union accumulation, proxyToNonOwner on/off, keyless ordering).
  • Integration: 84/84 across 17 suites, incl. RetryViewIntegrationTest (keyed) and PingRetryIntegrationTest (keyless ping) — verified against a live 3-node Docker cluster.
  • examples/quickstart/retry.cpp demonstrates getWithRetry/putWithRetry/pingWithRetry; builds warning-clean under -Wall -Wextra -Werror -pedantic and runs end-to-end.

🤖 Generated with Claude Code

rigazilla and others added 3 commits September 25, 2026 16:50
Base ops never retry on their own: a failure surfaces as a typed
HotRodClientException carrying the failure phase, tried nodes, and the
ownersExhausted flag; the caller opts into a retry via a bound view
(cache.excluding(e)), because replay-safety/idempotency is the caller's
call (D2).

- Exclusion-aware selectServerForKey over the pure orderKeyCandidates()
  helper (owners -> non-owner fallback, minus an exclusion set), reporting
  tried nodes and computing ownersExhausted honestly (ServerSelection.h,
  RetryContext.h).
- Client-level proxyToNonOwner flag (default true, D5) gates the
  owner->non-owner fallback; disabling it throws ownersExhausted instead of
  proxying.
- Op-level retry threading via a RetryView bound view: base signatures stay
  retry-free; excluding(nodes)/excluding(exception) forward to private
  …Impl(args, const RetryContext&) methods, and every op unions its tried
  nodes into the thrown exception's triedNodes (unionNodes()).
- Thread-safe routing/pool state: a single stateMutex_ guards topology +
  consistent hash + connection pool across the read loop and user/retry
  threads; pool holds shared_ptr so an in-flight op's connection survives
  concurrent topology eviction. Fixed a concurrent push_back race in the
  test harness (TopologyTestFixture).
- Records two intentional divergences from Java routing/retry (DECISIONS
  D2/D5).
- RetryViewIntegrationTest (3 tests) + examples/quickstart/retry.cpp.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Keyless operations get the same two-tier model as keyed ops: automatic
before-send failover across all servers, user-decided after-send retry (D2).

- ping() delegates to pingImpl(const RetryContext&); RetryView::ping()
  forwards the exclusion set, so cache.excluding(e).ping() works exactly
  like keyed ops.
- selectAnyServer(ctx, triedOut) routes via orderKeyCandidates({},
  allServers, exclude) (empty owners -> all servers in topology order),
  sweeps to the next reachable server, and falls back to the seed connection
  when no topology is known yet (ping is often the first op). ownersExhausted
  is always false for keyless ops; deterministic topology order (not Java's
  round-robin balancer), and after-send retry stays user-decided.
- Tests: 3 keyless-ordering unit tests; PingRetryIntegrationTest (3 tests);
  a keyless pingWithRetry loop added to the retry example.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…mmit

- Tick Step 11c in the STATUS backlog and mark Step 11 complete.
- README had drifted behind STATUS.md (pre-existing): test counts (145/46 ->
  204/84 across 17 suites), Steps 10/11 shown as future though both shipped,
  plus the feature/milestone/headline lists. Bring all user-facing facts in
  line with STATUS.md.
- Process fix so this drift does not recur: WORKFLOW.md §3 and AGENTS.md now
  prescribe checking README after *every* commit and updating it in the same
  commit when a user-facing fact changed (was "only if ... most commits won't
  touch it").

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@rigazilla
rigazilla merged commit 64a05c5 into main Sep 25, 2026
3 checks passed
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