Skip to content

fix(sdk): bound each DAPI request attempt, including the response body - #4973

Merged
lklimek merged 5 commits into
v4.2-devfrom
fix/dapi-client-request-deadline
Sep 28, 2026
Merged

lklimek merged 5 commits into
v4.2-devfrom
fix/dapi-client-request-deadline

Conversation

@llbartekll

@llbartekll llbartekll commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Issue being fixed or feature implemented

RequestSettings::timeout is only sent as the grpc-timeout header. tonic enforces that header on the client only until the response headers arrive (GrpcTimeout wraps the service call, not the body read), and the tonic channels have no HTTP/2 keepalive. An attempt whose response body stalls — typically a half-open connection after a mobile network blip — therefore never returns: no error, no retry, no ban.

Seen in a Dash Wallet iOS diagnostic export (build on fe90c2f674):

  • two shielded-notes chunk fetches failed only after 123 s, with h2 protocol error: error reading a body from connection when the node finally reset the streams;
  • 7 of 16 chunk fetches never returned at all;
  • from 07:52:46 on, no Platform response of any kind arrived for 6.5 minutes, although SPV traffic had recovered by 07:53.

One of the hung requests was a DPNS name refresh that the wallet's network switch awaited, so the switch never finished and the app had to be killed. A fresh process worked immediately.

What was done?

  • Per-attempt deadline (dapi_client.rs, native targets only): the whole attempt — connect, headers, body, trailers — is bounded by timeout + connect_timeout, the bound RequestSettings::timeout already documents. A miss becomes Status::deadline_exceeded, which is already retryable, so the existing loop bans the node and retries on another one. A zero timeout still means "no limit". New helper: AppliedRequestSettings::attempt_deadline().
  • Evict the pooled connection after a miss (ConnectionPool::remove_uri). Otherwise the dead channel stays pooled and the next request to that node stalls again once its ban expires.
  • HTTP/2 keepalive while a request is in flight (tonic_channel.rs): 15 s interval, 10 s timeout, keep_alive_while_idle(false), so idle pooled connections are never pinged.
  • 5 minute attempt timeout for megabyte-sized responses (LARGE_RESPONSE_TIMEOUT): GetShieldedEncryptedNotes (chunks of thousands of notes, fetched in parallel) and GetRecentCompactedAddressBalanceChanges (~9 MB per the existing comment). Without it the new deadline would fail these on slow links and ban healthy nodes.
  • Docs on RequestSettings::timeout / AppliedRequestSettings::timeout describe the new bound.

Unchanged: wasm (it only sends the header, as before), streaming methods (their future resolves at the response headers, so their streams are not bounded by this), the retry/ban policy itself.

Behaviour change worth a look: callers that pass a short explicit timeout now bound the response body too. For example, the rs-sdk-ffi mobile address-sync settings (timeout 5 s + connect_timeout 3 s) now cap the whole attempt at 8 s, and the Dash Wallet iOS app doesn't call that path. I did not change those call sites.

How Has This Been Tested?

  • cargo test -p rs-dapi-client: all green, including the new tests:
    • tests/request_deadline.rs (fake transport, tokio paused clock):
      • a node that never answers is cut at the deadline, banned, and the request succeeds on the other node;
      • when every node stalls, the result is NoAvailableAddressesToRetry(DeadlineExceeded);
      • a response that would complete only after timeout + connect_timeout is cut;
      • timeout == 0 does not cut a slow response.
    • Unit tests for attempt_deadline (sum, zero, saturation) and remove_uri (all prefixes and settings for the URI; a longer URI sharing the prefix is kept).
    • Mutation check: with attempt_deadline() forced to None, the body-deadline test fails.
  • cargo clippy -p rs-dapi-client --all-targets -- -D warnings, cargo fmt --check, cargo check -p rs-dapi-client --target wasm32-unknown-unknown, cargo check -p dash-sdk -p platform-wallet-ffi: all clean.
  • Built into the iOS xcframework and exercised on a simulator with fix(dashpay): finish a network switch without waiting on the identity recovery dashwallet-ios#1149. Switches with a healthy network complete normally. With the Mac's Wi-Fi turned off while DPNS/Platform requests were in flight:
    07:56:43 🔀 NETSWITCH [88D4A429] start mainnet → testnet
    07:56:48 🪪 IDENT-RECOVERY [f8a425ed] start network=testnet
    07:56:48 🔀 NETSWITCH [88D4A429] ready in 4817ms platform=running(1)
             (Mac Wi-Fi turned off here)
    07:56:58 grpc: Deadline expired before operation could complete, "no complete response within 10s" (x5, nodes banned)
    07:57:08 grpc: tcp connect error / Timeout expired on the retries
    07:57:13 grpc: no available addresses
    07:57:13 🪪 IDENT-RECOVERY [f8a425ed] complete in 25719ms identities=1 persisted=true
    07:57:45 🔀 NETSWITCH [3568001B] start testnet → mainnet   (still offline; Wi-Fi back ~07:57:55)
    07:58:05 🔀 NETSWITCH [3568001B] ready in 20567ms platform=running(0)
    
    The in-flight requests are cut by the new deadline after 10 s and banned. The retries fail fast while offline, and Platform responses resume once the network is back, without restarting the app.

Breaking Changes

None in the API: AppliedRequestSettings::attempt_deadline and ConnectionPool::remove_uri are additions. The behaviour change for callers with short explicit timeouts is described above.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed
  • If I added or changed GroveDB structure, I described it in the area's structure.rs, regenerated grovedb-structure.json, and checked the structure viewer link posted on this pull request

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

🤖 Generated with Claude Code

PR Hygiene · 8442d58

  • Bots — coderabbitai ✓ · thepastaclaw ✓
  • Self-review — posted; again after any push
  • Within your 5 open PRs
  • Build green
  • Approvals
    • files with no dedicated owner (Cargo.lock, packages/rs-dapi-client/Cargo.toml, packages/rs-dapi-client/src/connection_pool.rs and 7 more) — QuantumExplorer or shumkov

When every box is checked the PR Hygiene check passes and this can merge.

Summary by CodeRabbit

  • Reliability
    • On native platforms, request attempts are bounded by the configured timeout plus connection timeout. For unary requests, this includes receiving the response body and trailers; for streaming requests, it ends when response headers arrive.
    • Timed-out attempts can be retried on another node, without removing a newer connection to that node.
    • Setting the timeout to zero disables the client-side deadline and omits the server timeout header.
    • Connections send keep-alive signals during active requests to help maintain long-running connections.
  • Large Responses
    • Two requests that can return large responses use a five-minute timeout.

The request timeout was only sent as the `grpc-timeout` header, and tonic
enforces that header on the client only until the response headers arrive.
Reading the body had no limit, and channels had no HTTP/2 keepalive, so an
attempt over a half-open connection (a network path that died mid-response,
e.g. a mobile handover) never returned: no error, no retry, no ban. A mobile
wallet observed requests pending for 8+ minutes while other traffic had
recovered, and one stalled stream took 123 s to fail.

- Bound the whole attempt, from dispatch to the last byte of the response,
  by `timeout + connect_timeout` — the bound `RequestSettings::timeout`
  already documents. A miss fails with the retryable `DeadlineExceeded`, so
  the existing loop bans the node and retries elsewhere. Zero still means
  "no limit". Native targets only; wasm is unchanged.
- Evict the pooled connection after a miss so the next request to that node
  dials a fresh one instead of reusing the dead channel.
- Ping with HTTP/2 keepalive while a request is in flight (15 s interval,
  10 s timeout; idle connections are not pinged).
- Give the megabyte-sized responses (shielded encrypted notes chunks,
  compacted address balance changes) a 5 minute attempt timeout, so slow
  links do not fail them.

Callers that pass their own short timeout (e.g. the rs-sdk-ffi mobile
address-sync settings, 5 s + 3 s connect) now bound the response body too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: dashpay/platform/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: eefe7e0d-0625-4b61-aa38-797dfe5f9fed

📥 Commits

Reviewing files that changed from the base of the PR and between 939431e and 8442d58.

📒 Files selected for processing (4)
  • packages/rs-dapi-client/src/connection_pool.rs
  • packages/rs-dapi-client/src/dapi_client.rs
  • packages/rs-dapi-client/src/transport.rs
  • packages/rs-dapi-client/src/transport/grpc.rs

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The DAPI client adds native attempt deadlines that include response-body reading for unary calls. On timeout, it returns DeadlineExceeded and removes eligible pooled connections for the attempted address. Two unary methods use a five-minute timeout. The tonic channel sends HTTP/2 keep-alive PINGs during active requests.

Changes

DAPI client timing

Layer / File(s) Summary
Attempt deadline settings and tests
packages/rs-dapi-client/src/request_settings.rs, packages/rs-dapi-client/tests/request_deadline.rs, packages/rs-dapi-client/Cargo.toml
AppliedRequestSettings::attempt_deadline returns no deadline for a zero timeout. Otherwise, it adds the optional connection timeout with saturating addition. Tests cover deadline calculation and timeout behavior.
Generation-bounded pool eviction
packages/rs-dapi-client/src/connection_pool.rs, packages/rs-dapi-client/src/transport.rs, packages/rs-dapi-client/src/transport/grpc.rs
The pool tags inserted connections with generations. remove_uri matches exact URIs and removes only connections at or before the supplied generation. Transport clients can return the generation associated with the acquired connection.
Native attempt deadlines and retries
packages/rs-dapi-client/src/dapi_client.rs, packages/rs-dapi-client/tests/request_deadline.rs, packages/rs-dapi-client/tests/stalled_response_body.rs
On non-WASM targets, DapiClient applies the deadline to the full transport attempt. On timeout, it removes connections up to the generation associated with the used connection and returns DeadlineExceeded. Tests cover stalled response bodies, retries, and connection retention.
Large unary response timeouts
packages/rs-dapi-client/src/transport/grpc.rs
GetShieldedEncryptedNotes and GetRecentCompactedAddressBalanceChanges use a five-minute timeout and retain their other default request settings.
HTTP/2 keep-alive settings
packages/rs-dapi-client/src/transport/tonic_channel.rs
The channel sends keep-alive PINGs every 15 seconds during active requests, allows 10 seconds for acknowledgement, and does not ping idle connections.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant DapiClient
  participant TransportClient
  participant ConnectionPool
  participant RetryLogic
  DapiClient->>TransportClient: Build client and return its connection generation
  DapiClient->>DapiClient: Bound transport attempt by deadline
  DapiClient->>ConnectionPool: Remove matching URI entries up to that generation
  DapiClient->>RetryLogic: Return DeadlineExceeded
  RetryLogic->>DapiClient: Select next address for retry
Loading

Suggested reviewers: quantumexplorer

Merge Risk: 🔵 Low · up to 8442d

Built-in DAPI requests use the corrected generation handling. Custom pool-backed transports may still evict a newer connection after a timeout, causing an unnecessary reconnect; this is bounded follow-up risk rather than a merge blocker.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 8442d

The built-in transports preserve the identity of the connection used by each attempt, and eviction is limited to the attempted node. A narrower availability risk remains for custom transports that use the new default behavior: a timed-out request could discard a connection created by another request. Use of that extension path outside this repository is not established.

Retained concerns

  • Low · reliability · inferred: A custom transport using the new default generation method can record a generation assigned to another request's newer connection. If its attempt then times out, URI-wide cleanup can evict that newer pooled connection, weakening concurrent-request failure containment. The built-in Core and Platform transports override the default; downstream exposure is unverified.
Security review details

Security Blast Radius

  • inferred — A DAPI endpoint that stalls a response can cause its attempt to reach the native deadline. The resulting cleanup is bounded to that endpoint's exact URI in the client's shared pool, though it can discard entries for multiple transport types or trust settings at that URI. No broader tenant, data-store, credential, or environment exposure is established.

Security Findings and Attack Paths

  • inferred — If a custom transport uses the new default method while another request inserts a connection for the same URI, a stalled custom attempt can acquire a cutoff covering that newer entry. Timeout cleanup can then discard the entry despite its separate ownership. No visible built-in transport takes this path, and no downstream occurrence is verified.

Trust Boundaries and Controls

  • observed — Connection reuse preserves transport-type and certificate identity through its pool key. The new generation-aware methods do not themselves introduce a route to construct or retrieve a transport across that boundary: pool entry types remain inside the private module, while built-in constructors choose their own Core or Platform prefix.

Resilience and Maintainability Implications

  • observed — Timeout cleanup occurs when the native timeout returns an expiry. An externally interrupted attempt does not reach that branch; the available evidence does not establish cancellation cleanup as part of this change's guarantee.

Hardening Proposals

  • proposed — Require a transport that permits timeout eviction to return the generation of the connection it actually acquired, or avoid evicting on behalf of implementations without that ownership information. This would remove the default method's concurrent-insertion ambiguity.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.55% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 66 functions across 8 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: bounding each DAPI request attempt through response-body processing.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@thepastaclaw

thepastaclaw commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

✅ Final review complete — no blockers (commit 8442d58) · triage: normal

@llbartekll
llbartekll marked this pull request as ready for review September 25, 2026 08:06
@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Bots are done — your move: post /self-reviewed.
Full checklist in the description.

@github-actions github-actions Bot added waiting-self-review Waiting for the author to post /self-reviewed bot-review-skipped A required review bot did not report; it was skipped by the window or by a person. labels Sep 25, 2026
@llbartekll

Copy link
Copy Markdown
Contributor Author

/self-reviewed

@github-actions github-actions Bot removed the waiting-self-review Waiting for the author to post /self-reviewed label Sep 25, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Bots are done — your move: post /self-reviewed.
Full checklist in the description.

@github-actions github-actions Bot added the waiting-self-review Waiting for the author to post /self-reviewed label Sep 25, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Bots are done — your move: post /self-reviewed.
Full checklist in the description.

romchornyi
romchornyi previously approved these changes Sep 25, 2026

@romchornyi romchornyi 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.

Approving — no blockers found. The per-attempt deadline, pool eviction and keepalive are the right shape, and the new request_deadline.rs tests pin the executor behavior well. Everything below is a non-blocking recommendation — none of it needs to happen before merge.

  1. LARGE_RESPONSE_TIMEOUT is silently defeated by execute-level settings. execute()-level settings override R::SETTINGS_OVERRIDES (dapi_client.rs:730-733), so the 300 s carve-out for GetRecentCompactedAddressBalanceChanges never applies where a caller passes an explicit short timeout — concretely the rs-sdk-ffi mobile address-sync settings (5 s + 3 s → an 8 s cap on a ~9 MB response, plus a healthy-node ban per miss). The PR body already flags the 8 s cap, and I verified AddressSyncService / dash_sdk_sync_addresses_batch_with_result currently has no callers in dashwallet-ios or anywhere org-wide, so this is latent — but the first caller of that path gets a ban machine instead of the protection this diff adds for exactly that request. A comment at the ffi mobile_settings (or a larger timeout there for the catch-up phase) would keep the carve-out honest.

  2. Broadcast + deadline widens the ambiguous-broadcast class. A broadcast_state_transition attempt cut by the new deadline is retried on another node although the first node may have already relayed the ST — a hang becomes a possible double-submit / false-failure. This is the class already tracked in #4762 / dashwallet-ios#1132; worth a cross-reference there rather than a change here.

  3. Keepalive tuning on saturated links. On a slow bufferbloated link the server's PING ack can queue behind megabytes of in-flight response body; an ack delayed past the 10 s HTTP2_KEEP_ALIVE_TIMEOUT kills a healthy connection mid-transfer and fails every in-flight chunk fetch on it — precisely during the parallel shielded-notes fetches the 300 s carve-out targets. Since the attempt deadline now bounds true hangs anyway, a more generous ack timeout would reduce false kills.

  4. Wedged-but-alive node now stalls large requests 300 s. Pre-PR, a node that never sent response headers was cut by tonic at the 10 s grpc-timeout; for the two large-response requests it is now 300 s per attempt (keepalive won't fire — the h2 layer is alive). A separate header-arrival bound, or a progress-based deadline, would keep the 300 s for the body only.

  5. Smaller notes: (a) with DEFAULT_CONNECT_TIMEOUT = None the documented timeout + connect_timeout bound reserves nothing for a fresh dial, so TCP+TLS on a high-RTT link is charged against the 10 s response budget; (b) deadline misses go through the ban ladder, so a ~15 s local outage bans every node with an in-flight request (the PR's own test log shows 5 nodes banned) and a quick reconnect can still hit NoAvailableAddresses; (c) remove_uri evicts by URI, so a second straggler miss also evicts a freshly dialed healthy replacement channel; (d) the executor→pool eviction wiring (remove_uri on a miss in dapi_client.rs) has no test through the executor — FakeClient never touches the pool, so a mis-keyed or deleted call would pass the suite.

🤖 Reviewed with Claude Code

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Final validation — Phase 1 + Phase 2

Verified the supplied findings against head b23cdb1, the surrounding transport code, and the PR description. No blocking issue was confirmed; three non-blocking suggestions remain concerning streaming timeout documentation and regression coverage of stalled response bodies and connection eviction. Source inspection and git diff --check completed successfully; reviewer-reported test runs were not independently repeated.

🟡 3 suggestion(s)

Review provenance

Source: reviewer 1: gemini-3.8-flash-high (agent: phase1-reviewer, role: general); reviewer 2: gemini-3.8-flash-high (agent: phase1-reviewer, role: architecture-layering); reviewer 3: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 4: muse-spark-1.3-contributor (agent: phase1-reviewer, role: security-auditor); reviewer 5: gpt-6-astra (agent: phase2-reviewer, role: general); reviewer 6: gpt-6-astra (agent: phase2-reviewer, role: architecture-layering); reviewer 7: gpt-6-astra (agent: phase2-reviewer, role: rust-quality); reviewer 8: gpt-6-astra (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6-astra (agent: astra-verifier, role: final-verifier)

  • Triage: normal by gpt-6-astra (effort low) — This is a substantial but well-tested change to DAPI request deadlines, connection pooling, transport keepalive, and retry behavior, but it does not modify consensus, funds movement, cryptography, key handling, peer-facing deserialization, or storage migrations.
  • Phase 1 reviewers: gemini-3.8-flash-high — general (completed, effort high); agent phase1-reviewer, gemini-3.8-flash-high — architecture-layering (completed, effort high); agent phase1-reviewer, muse-spark-1.3-contributor — rust-quality (completed, effort xhigh); agent phase1-reviewer, muse-spark-1.3-contributor — security-auditor (completed, effort xhigh); agent phase1-reviewer
  • Phase 1 model: muse-spark-1.3-contributor — not quota-gated; passed over gemini-3.8-flash-high (lane failed), glm-5.3-flash (not used above high effort; tier asks max)
  • Fresh verifier: gpt-6-astra — final-verifier; agent astra-verifier
  • Phase 2 reviewers: gpt-6-astra — general (completed, effort high); agent phase2-reviewer, gpt-6-astra — architecture-layering (completed, effort high); agent phase2-reviewer, gpt-6-astra — rust-quality (completed, effort high); agent phase2-reviewer, gpt-6-astra — security-auditor (completed, effort high); agent phase2-reviewer
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-dapi-client/src/request_settings.rs`:
- [SUGGESTION] packages/rs-dapi-client/src/request_settings.rs:25-30: Scope the response-body timeout guarantee to unary RPCs
  The new documentation promises a client-side bound through the last response byte without distinguishing unary and streaming responses. However, the streaming requests in transport/grpc.rs return tonic::Streaming when headers arrive, so execute_transport completes and the executor's timeout ends before subsequent stream consumption. The PR description correctly identifies this limitation, but callers reading the public settings documentation will not see it. Document that unary responses are bounded through body and trailers, while streaming responses are bounded only until headers arrive; apply the same qualification to AppliedRequestSettings::attempt_deadline.

In `packages/rs-dapi-client/tests/request_deadline.rs`:
- [SUGGESTION] packages/rs-dapi-client/tests/request_deadline.rs:121-133: Cover connection-pool eviction through the deadline executor
  The deadline tests exercise retries and banning, but FakeClient::with_uri_and_settings ignores the supplied ConnectionPool, leaving the executor's pool empty. The remove_uri unit test verifies removal directly, so deleting self.pool.remove_uri(address.uri()) from the timeout branch would leave both sets of tests passing. Since eviction is an explicit part of this fix, add an executor-level test with a pool-aware fake transport or an internal test that seeds lazy channels, triggers the deadline, and verifies that the timed-out URI is removed while another URI remains cached. This can run without network access.
- [SUGGESTION] packages/rs-dapi-client/tests/request_deadline.rs:158-161: Exercise a headers-complete, body-stalled response through tonic
  DelayedRequest::execute_transport only sleeps before returning FakeResponse; it never produces response headers or exercises tonic's body/trailer reader. This verifies the executor's deadline around a delayed future, but does not demonstrate the comment's distinction between a headers-only timeout and a complete-response timeout—the specific failure this PR addresses. Add a deterministic in-memory transport regression test through tonic that supplies headers promptly and then stalls the unary body or trailers, asserting DeadlineExceeded and failover. Keep the existing fake-transport tests for focused retry-policy coverage.

Comment thread packages/rs-dapi-client/src/request_settings.rs Outdated
Comment thread packages/rs-dapi-client/tests/request_deadline.rs
Comment thread packages/rs-dapi-client/tests/request_deadline.rs
@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Your move: thepastaclaw left review threads unresolved; resolve them.
Full checklist in the description.

@github-actions github-actions Bot removed the bot-review-skipped A required review bot did not report; it was skipped by the window or by a person. label Sep 25, 2026
Review follow-ups for the per-attempt DAPI deadline:
- Document that the attempt bound covers the response body and trailers
  for unary RPCs only; for streaming RPCs it ends when the response headers
  arrive (`RequestSettings::timeout`, `AppliedRequestSettings::attempt_deadline`).
- Executor-level test that the pooled connection of a node whose attempt
  missed its deadline is evicted while another node's stays pooled, using a
  fake client that takes its channel from the executor's pool.
- Regression through tonic itself: an in-memory HTTP/2 server answers with
  response headers and never sends the body. The tests show tonic alone is
  still pending 2 s after a 200 ms `grpc-timeout`; that the executor cuts the
  same call with `DeadlineExceeded`; and that it fails over to another node.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added waiting-bots Waiting for the review bots to report on this head and removed waiting-self-review Waiting for the author to post /self-reviewed labels Sep 25, 2026

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-review — Final validation — Phase 1 + Phase 2

The PR correctly bounds native unary attempts through response-body consumption, evicts timed-out pooled connections, configures HTTP/2 keepalive, and adds regression coverage for stalled bodies and pool eviction. The prior three findings are fixed at this head. One in-scope suggestion remains: the new URI eviction matcher can remove a different pooled URI when one URI is a textual prefix of another at a colon boundary.

🟡 1 suggestion(s)

Review provenance

Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: muse-spark-1.3-contributor (agent: phase1-reviewer, role: architecture-layering); reviewer 3: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 4: gpt-6-astra (agent: phase2-reviewer, role: general); reviewer 5: gpt-6-astra (agent: phase2-reviewer, role: architecture-layering); reviewer 6: gpt-6-astra (agent: phase2-reviewer, role: rust-quality); reviewer 7: gpt-6-astra (agent: phase2-reviewer, role: general); reviewer 8: gpt-6-astra (agent: phase2-reviewer, role: architecture-layering); reviewer 9: gpt-6-astra (agent: phase2-reviewer, role: rust-quality); final verifier: gpt-6-astra (agent: astra-verifier, role: final-verifier)

  • Triage: normal by gpt-6-astra (effort low) — The change coordinates request deadlines, retry-triggered connection eviction, keepalive, and large-response timeout settings across the SDK transport, but does not itself alter consensus, funds movement, cryptography, peer-facing deserialization, or storage migrations.
  • Phase 1 reviewers: muse-spark-1.3-contributor — general (completed, effort xhigh); agent phase1-reviewer, muse-spark-1.3-contributor — architecture-layering (completed, effort xhigh); agent phase1-reviewer, muse-spark-1.3-contributor — rust-quality (completed, effort xhigh); agent phase1-reviewer
  • Phase 1 model: muse-spark-1.3-contributor — not quota-gated; passed over gemini-3.8-flash-high (antigravity below 15% reserve: weekly 35% left, 5h 0% left), glm-5.3-flash (not used above high effort; tier asks max)
  • Fresh final gate: an independent Phase-2 review ran after iterative findings were reconciled
  • Fresh verifier: gpt-6-astra — final-verifier; agent astra-verifier
  • Phase 2 reviewers: gpt-6-astra — general (completed, effort high); agent phase2-reviewer, gpt-6-astra — architecture-layering (completed, effort high); agent phase2-reviewer, gpt-6-astra — rust-quality (completed, effort high); agent phase2-reviewer, gpt-6-astra — general (completed, effort high); agent phase2-reviewer, gpt-6-astra — architecture-layering (completed, effort high); agent phase2-reviewer, gpt-6-astra — rust-quality (completed, effort high); agent phase2-reviewer
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-dapi-client/src/connection_pool.rs`:
- [SUGGESTION] packages/rs-dapi-client/src/connection_pool.rs:102-113: Avoid prefix collisions when evicting pooled URIs
  `remove_uri` identifies entries with `starts_with(format!("{}:{}:", prefix, uri))`, but pool keys are also built as `prefix:uri:settings`. This is not an exact URI match when the target URI ends before a colon: removing `http://node` constructs the prefix `Platform:http://node:`, which also matches the distinct pooled key for `http://node:443`. A timeout for the shorter URI can therefore evict the healthy longer URI's connection, defeating targeted eviction and causing an unnecessary reconnect. Use a structured pool key or otherwise compare the URI component exactly, and add a regression test for `http://node` versus `http://node:443`; the existing `3000` versus `30001` test does not expose this collision because the next character is not the delimiter.

Comment thread packages/rs-dapi-client/src/connection_pool.rs Outdated
@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Your move: thepastaclaw left review threads unresolved; resolve them.
Full checklist in the description.

@github-actions github-actions Bot added waiting-self-review Waiting for the author to post /self-reviewed and removed waiting-bots Waiting for the review bots to report on this head labels Sep 25, 2026
`remove_uri` matched pooled entries by a `prefix:uri:` string prefix. A URI
whose path contains a colon extends a shorter one past that delimiter, so
evicting `http://node/grpc` also dropped the healthy connection to
`http://node/grpc:8080`. Pool keys are now a struct (client type, URI,
connection settings) and eviction compares the URI exactly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added waiting-bots Waiting for the review bots to report on this head and removed waiting-self-review Waiting for the author to post /self-reviewed labels Sep 25, 2026

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-review — Final validation — Phase 1 + Phase 2

Independently reviewed the complete diff at 1ac23c6 and confirmed that all four prior findings are fixed. One non-blocking concurrency issue remains: a late timeout can evict a newer healthy connection cached for the same URI. Validation passed for 152 unit/integration tests, 5 doctests, the WASM compilation check, and the review-range diff check; the working tree remains unchanged.

🟡 1 suggestion(s)

Review provenance

Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: muse-spark-1.3-contributor (agent: phase1-reviewer, role: architecture-layering); reviewer 3: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 4: muse-spark-1.3-contributor (agent: phase1-reviewer, role: security-auditor); reviewer 5: gpt-6-astra (agent: phase2-reviewer, role: general); reviewer 6: gpt-6-astra (agent: phase2-reviewer, role: architecture-layering); reviewer 7: gpt-6-astra (agent: phase2-reviewer, role: rust-quality); reviewer 8: gpt-6-astra (agent: phase2-reviewer, role: security-auditor); reviewer 9: gpt-6-astra (agent: phase2-reviewer, role: general); reviewer 10: gpt-6-astra (agent: phase2-reviewer, role: architecture-layering); reviewer 11: gpt-6-astra (agent: phase2-reviewer, role: rust-quality); reviewer 12: gpt-6-astra (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6-astra (agent: astra-verifier, role: final-verifier)

  • Triage: normal by gpt-6-astra (effort low) — This is a substantial but well-tested SDK networking change spanning request deadlines, connection pooling, transport keepalive, retries, and response-body handling, without modifying consensus, funds, cryptography, storage, or other critical surfaces.
  • Phase 1 reviewers: muse-spark-1.3-contributor — general (completed, effort xhigh); agent phase1-reviewer, muse-spark-1.3-contributor — architecture-layering (completed, effort xhigh); agent phase1-reviewer, muse-spark-1.3-contributor — rust-quality (completed, effort xhigh); agent phase1-reviewer, muse-spark-1.3-contributor — security-auditor (completed, effort xhigh); agent phase1-reviewer
  • Phase 1 model: muse-spark-1.3-contributor — not quota-gated; passed over gemini-3.8-flash-high (antigravity below 15% reserve: weekly 35% left, 5h 0% left), glm-5.3-flash (not used above high effort; tier asks max)
  • Fresh final gate: an independent Phase-2 review ran after iterative findings were reconciled
  • Fresh verifier: gpt-6-astra — final-verifier; agent astra-verifier
  • Phase 2 reviewers: gpt-6-astra — general (completed, effort high); agent phase2-reviewer, gpt-6-astra — architecture-layering (completed, effort high); agent phase2-reviewer, gpt-6-astra — rust-quality (completed, effort high); agent phase2-reviewer, gpt-6-astra — security-auditor (completed, effort high); agent phase2-reviewer, gpt-6-astra — general (completed, effort high); agent phase2-reviewer, gpt-6-astra — architecture-layering (completed, effort high); agent phase2-reviewer, gpt-6-astra — rust-quality (completed, effort high); agent phase2-reviewer, gpt-6-astra — security-auditor (completed, effort high); agent phase2-reviewer
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-dapi-client/src/connection_pool.rs`:
- [SUGGESTION] packages/rs-dapi-client/src/connection_pool.rs:111-122: Avoid evicting a newer pooled connection after a concurrent timeout
  The executor calls this method after an attempt expires, but the pool entry at that point need not be the connection used by that attempt. For example, with `ban_failed_address: false`, two concurrent requests can share a stalled channel; the first times out and evicts it, then its retry creates a fresh channel and succeeds. When the second original request reaches its later deadline, this URI-wide removal also evicts the fresh channel. Different request timeouts deliberately share a pool key, and the shared pool has no generation check to prevent this sequence. Existing users of the fresh channel retain their clones, but subsequent requests unnecessarily reconnect, and staggered timeouts can repeat that churn during recovery. Capture the connection generation used by each attempt and make deadline eviction conditional on that generation still being cached; add a regression test where a replacement is inserted before an older attempt expires.

Comment thread packages/rs-dapi-client/src/connection_pool.rs Outdated
Deadline eviction dropped every pooled connection to the node. Two
requests can share a stalled channel: the first times out, evicts it and
reconnects on retry, and when the second reaches its later deadline it
also evicted that fresh connection, so the next request dialed again.

The pool now numbers each connection it stores. The executor notes the
pool's generation once the attempt has its connection, and a deadline
eviction drops only connections to that node pooled at or before it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added waiting-bots Waiting for the review bots to report on this head and removed waiting-self-review Waiting for the author to post /self-reviewed labels Sep 25, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Your move: thepastaclaw left review threads unresolved; resolve them.
Full checklist in the description.

@github-actions github-actions Bot added waiting-self-review Waiting for the author to post /self-reviewed and removed waiting-bots Waiting for the review bots to report on this head labels Sep 25, 2026

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-review — Final validation — Phase 1 + Phase 2

Reviewed the complete diff at 939431e. The DAPI client tests, native clippy checks, and WASM compilation check pass; four prior findings are fixed, but replacement-safe eviction still has a confirmed acquisition/snapshot race. A temporary deterministic regression reproduced the unnecessary eviction, and the worktree was restored clean.

🟡 1 suggestion(s)

Review provenance

Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: muse-spark-1.3-contributor (agent: phase1-reviewer, role: architecture-layering); reviewer 3: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 4: gpt-6-astra (agent: phase2-reviewer, role: general); reviewer 5: gpt-6-astra (agent: phase2-reviewer, role: architecture-layering); reviewer 6: gpt-6-astra (agent: phase2-reviewer, role: rust-quality); reviewer 7: gpt-6-astra (agent: phase2-reviewer, role: general); reviewer 8: gpt-6-astra (agent: phase2-reviewer, role: architecture-layering); reviewer 9: gpt-6-astra (agent: phase2-reviewer, role: rust-quality); final verifier: gpt-6-astra (agent: astra-verifier, role: final-verifier)

  • Triage: normal by gpt-6-astra (effort low) — The changes introduce nontrivial SDK request deadlines, connection-pool eviction, keepalive behavior, and regression tests, but do not alter consensus, funds movement, cryptography, peer-facing network deserialization, or storage migrations.
  • Phase 1 reviewers: muse-spark-1.3-contributor — general (completed, effort xhigh); agent phase1-reviewer, muse-spark-1.3-contributor — architecture-layering (completed, effort xhigh); agent phase1-reviewer, muse-spark-1.3-contributor — rust-quality (completed, effort xhigh); agent phase1-reviewer
  • Phase 1 model: muse-spark-1.3-contributor — not quota-gated; passed over gemini-3.8-flash-high (antigravity below 15% reserve: weekly 20% left, 5h 14% left), glm-5.3-flash (not used above high effort; tier asks max)
  • Fresh final gate: an independent Phase-2 review ran after iterative findings were reconciled
  • Fresh verifier: gpt-6-astra — final-verifier; agent astra-verifier
  • Phase 2 reviewers: gpt-6-astra — general (completed, effort high); agent phase2-reviewer, gpt-6-astra — architecture-layering (completed, effort high); agent phase2-reviewer, gpt-6-astra — rust-quality (completed, effort high); agent phase2-reviewer, gpt-6-astra — general (completed, effort high); agent phase2-reviewer, gpt-6-astra — architecture-layering (completed, effort high); agent phase2-reviewer, gpt-6-astra — rust-quality (completed, effort high); agent phase2-reviewer
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-dapi-client/src/dapi_client.rs`:
- [SUGGESTION] packages/rs-dapi-client/src/dapi_client.rs:1045-1050: Avoid evicting a newer pooled connection after a concurrent timeout
  The generation guard protects replacements inserted after this snapshot, but connection acquisition and generation capture still take separate pool locks. Attempt A can clone connection N, another worker can replace it with N+1, and A can then sample N+1 here while retaining N. If A times out, `remove_uri(..., N+1)` removes the replacement even though A never used it. Existing channel clones remain usable, but subsequent requests unnecessarily reconnect during recovery. Your new executor regression inserts its replacement inside `execute_transport`, after the snapshot; I independently reproduced the uncovered ordering by retaining the acquired client and inserting a replacement on another thread before the client constructor returned, which fails the replacement-preservation assertion. Capture the acquired entry's generation together with its client under the same pool lock, propagate that token through transport-client construction, and add coverage for replacement between acquisition and constructor return.

Comment thread packages/rs-dapi-client/src/dapi_client.rs Outdated
@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Your move: thepastaclaw left review threads unresolved; resolve them.
Full checklist in the description.

The executor read the pool generation after the transport client was
built, under a separate lock. If another worker replaced the node's
connection in between, the attempt kept the old connection but recorded
the replacement's generation, and a deadline eviction then dropped the
replacement.

`ConnectionPool::get_or_create_with_generation` returns the connection
and its generation from the same lock. `TransportClient` gains the
provided method `with_uri_and_settings_and_generation`; the gRPC clients
override it with that generation, and the executor evicts by it. The
default still reads the pool generation after building the client.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added waiting-bots Waiting for the review bots to report on this head and removed waiting-self-review Waiting for the author to post /self-reviewed labels Sep 25, 2026
@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Your move: thepastaclaw left review threads unresolved; resolve them.
Full checklist in the description.

@github-actions github-actions Bot added waiting-self-review Waiting for the author to post /self-reviewed waiting-bots Waiting for the review bots to report on this head and removed waiting-bots Waiting for the review bots to report on this head waiting-self-review Waiting for the author to post /self-reviewed labels Sep 25, 2026

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-review — Final validation — Phase 1 + Phase 2

Independently reviewed the complete diff at 8442d58 and verified that all six prior findings are fixed, with no remaining actionable in-scope findings. The native deadline covers unary response consumption, and both production gRPC clients propagate the acquired connection's generation atomically so timeout eviction preserves newer replacements. Default-feature and all-feature rs-dapi-client tests, the WebAssembly package check, and diff whitespace checks passed; the worktree remains unchanged.

🔴 0 blocking | 🟡 0 suggestion(s) | 💬 0 nitpick(s)

Review provenance

Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: muse-spark-1.3-contributor (agent: phase1-reviewer, role: architecture-layering); reviewer 3: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 4: gpt-6-astra (agent: phase2-reviewer, role: general); reviewer 5: gpt-6-astra (agent: phase2-reviewer, role: architecture-layering); reviewer 6: gpt-6-astra (agent: phase2-reviewer, role: rust-quality); reviewer 7: gpt-6-astra (agent: phase2-reviewer, role: general); reviewer 8: gpt-6-astra (agent: phase2-reviewer, role: architecture-layering); reviewer 9: gpt-6-astra (agent: phase2-reviewer, role: rust-quality); final verifier: gpt-6-astra (agent: astra-verifier, role: final-verifier)

  • Triage: normal by gpt-6-astra (effort low) — The change involves nontrivial asynchronous deadline enforcement, retry behavior, and generation-aware connection-pool eviction, but does not itself alter consensus, funds movement, cryptography, peer-facing network deserialization, or storage migrations.
  • Phase 1 reviewers: muse-spark-1.3-contributor — general (completed, effort xhigh); agent phase1-reviewer, muse-spark-1.3-contributor — architecture-layering (completed, effort xhigh); agent phase1-reviewer, muse-spark-1.3-contributor — rust-quality (completed, effort xhigh); agent phase1-reviewer
  • Phase 1 model: muse-spark-1.3-contributor — not quota-gated; passed over gemini-3.8-flash-high (antigravity below 15% reserve: weekly 13% left, 5h 52% left), glm-5.3-flash (not used above high effort; tier asks max)
  • Fresh final gate: an independent Phase-2 review ran after iterative findings were reconciled
  • Fresh verifier: gpt-6-astra — final-verifier; agent astra-verifier
  • Phase 2 reviewers: gpt-6-astra — general (completed, effort high); agent phase2-reviewer, gpt-6-astra — architecture-layering (completed, effort high); agent phase2-reviewer, gpt-6-astra — rust-quality (completed, effort high); agent phase2-reviewer, gpt-6-astra — general (completed, effort high); agent phase2-reviewer, gpt-6-astra — architecture-layering (completed, effort high); agent phase2-reviewer, gpt-6-astra — rust-quality (completed, effort high); agent phase2-reviewer
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify the current code and confirm that no unresolved issues remain.

No unresolved findings remain from the prior review on this head.

@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Bots are done — your move: post /self-reviewed.
Full checklist in the description.

@github-actions github-actions Bot added waiting-self-review Waiting for the author to post /self-reviewed and removed waiting-bots Waiting for the review bots to report on this head labels Sep 25, 2026
@llbartekll

Copy link
Copy Markdown
Contributor Author

/self-reviewed

@github-actions

Copy link
Copy Markdown
Contributor

Ready for review — needs QuantumExplorer or shumkov.
Full checklist in the description.

@github-actions github-actions Bot added ready-for-human Bots have reported, the author has self-reviewed, and the build is green: this needs a human. and removed waiting-self-review Waiting for the author to post /self-reviewed labels Sep 28, 2026

@romchornyi romchornyi 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.

re-approving

@lklimek
lklimek merged commit e776f95 into v4.2-dev Sep 28, 2026
28 of 29 checks passed
@lklimek
lklimek deleted the fix/dapi-client-request-deadline branch September 28, 2026 13:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Bots have reported, the author has self-reviewed, and the build is green: this needs a human.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants