Repository navigation
feat(browser): report the serving peer and GET attempts per downloaded chunk - #249
Conversation
d9fa721 to
66e3148
Compare
MaidSafe-Hermes
left a comment
There was a problem hiding this comment.
REQUEST_CHANGES — 2/2 seats reviewed (sol-1 gpt-6.1-sol xhigh; glm-1 z-ai/glm-5.3). The seats differed: sol-1 identified the retry-accounting defect; glm-1 approved but also noted deferred rounds reset the count. I verified the defect in the frozen source.
ant-core/src/data/client/chunk.rs:1186-1194reports only the successfulchunk_getcall's local count. The browser download path retries failed records in later deferred rounds (ant-core/src/client_engine/files.rs:396-434), and each retry callsfetch_data_record_with_sourceagain, which starts a freshchunk_getcount. Thus if one or more GETs fail and a later retry succeeds, progress reports only the final round's attempts, not all GETs spent fetching that chunk. This contradicts the documented “GETs it cost” claim and can hide costly retries.
Please carry the attempt total across deferred rounds (and preserve it through failed fetch results) and add a browser-WASM test where an initial GET fails and a later round succeeds, asserting the reported total. This is a source-confirmed defect, not a CI-only concern.
CI at review time also has failures (Clippy, Documentation, Security Audit, Unit Tests on Ubuntu, E2E on Ubuntu, Build on Ubuntu, and Merkle E2E on Ubuntu); several other jobs remain in progress. The run is still active, so its failed-job logs are not yet available. These checks are separate from the code finding and need diagnosis before merge.
…d chunk
A browser read investigation could not tell which node served a chunk or how
many GETs the chunk cost: the complete-download path reported only
`Downloaded chunk n/total`, and the one line naming the peer is a `debug!` that
never reaches a headless driver's stderr. That left the PROD-WDL-01 slowdown
(V2-1795) with no way to attribute a slow download to a peer.
`chunk_get` now reports where its chunk came from. `ChunkSource` carries the
serving `PeerId` — `None` for a chunk cache hit and for the kad lookup's own
early return — and the number of peer GETs the call dispatched, counted with an
atomic captured by the per-peer `get` closure so the native `data_download`
futures stay `Send`. A deferred retry round is a separate `chunk_get` call with
its own count. Single-flight joiners receive the leader's source: the same GET
served them.
`chunk_get_from_closest_peers_with_diagnostics` and the single-flight map carry
the pair; `chunk_get_from_closest_peers`, `chunk_get_observed` and
`chunk_get_observed_from_closest_peers` keep their signatures by stripping the
source, so the native download paths are untouched. The browser's complete
download resolves each chunk's source through a new
`chunk_get_observed_with_source` and reports it:
Downloaded chunk 3/147 from <64-char peer hex> after 1 attempts
Downloaded chunk 3/147 from cache after 0 attempts
The `Downloaded chunk n/total` prefix is unchanged, so the startup probes that
match on it still work.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
66e3148 to
ebcf49b
Compare
Review of WithAutonomi#249 found that the reported attempt count covered only the `chunk_get` call that succeeded. `read_chunks` retries a record that was not found in later deferred rounds, and each round is a fresh `chunk_get` with its own count, so a chunk whose first round found nothing reported just its last round's GETs — hiding exactly the costly retries the line exists to surface. The rounds are now added up. `chunk_get_observed_with_source` takes a `dispatched` counter that the fetch raises as it sends each peer GET, whatever the call goes on to return, so a round that ends in an error — and therefore carries no `ChunkSource` — still reports what it spent. `data_download_indexed_chunks` keeps a per-record running total and reports it, taking a successful round's count from the returned source (which a single-flight joiner also receives) and a failed round's from the counter. The new wasm test withholds one data record for the first two GETs, so the record can only be found in a later round, and asserts the retried chunk reports every GET the mock served for it. Observed: 3 GETs for that record and `[1, 1, 3]` reported, where the succeeding round sent one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Confirmed and fixed in 1b64f1f — the finding was right. To be explicit about scope: counting per Carrying the total across rounds. The awkward part is that a round which finds nothing .await;
// Report the GETs before the error path returns: a round that found
// nothing still cost them, and a retrying caller needs them counted.
let attempts = attempts.load(Ordering::Relaxed);
if let Some(dispatched) = dispatched {
dispatched.fetch_add(attempts, Ordering::Relaxed);
}
let result = outcome?;
let round = match &fetched {
Ok((_, source)) => source.attempts,
Err(_) => dispatched.load(Ordering::Relaxed),
};The split matters for single-flight: a joiner sends no GET of its own, so its Test. Run locally against the built bindings, instrumented to show the numbers: Three GETs for the withheld record, reported as 3. The succeeding round sent one of them, so the On the CI failures you saw — they were all one cause, now fixed in Security Audit is not from this branch — it fails on #248 as well, so it is a new advisory against 🤖 Generated with Claude Code |
MaidSafe-Hermes
left a comment
There was a problem hiding this comment.
REQUEST_CHANGES — 2/2 seats reviewed (sol-1 gpt-6.1-sol xhigh; glm-1 z-ai/glm-5.3). Both seats confirm the original retry-round omission is fixed. They differ on a remaining concurrency case; I verified sol-1's finding against the frozen source.
ant-core/src/data/client/chunk.rs:549-563returns a shared fetch result to joiners, but only the leader'sdispatchedcounter is updated (:541-547passes that counter into the fetch closure). When the shared fetch fails, the result carries noChunkSource, so a joiner's counter stays zero. Thenant-core/src/data/client/data.rs:518-525uses that zero as the failed round's cost. With two concurrent browser downloads of the same uncached chunk, a failed shared GET round is therefore counted for the leader but omitted from the follower's later progress total; the successful retry count is included. This still underreports “every GET it cost” under supported single-flight sharing.
Please propagate failed-round attempt metadata through the shared unsuccessful result as well, and add a test with two concurrent downloads sharing a failed round before success; assert that both progress callbacks report the same cumulative count. The new single-download retry test does not exercise this joiner path.
CI: the browser-WASM tests, Clippy, unit tests, documentation and Ubuntu build have passed. The Security Audit fails on crypto-bigint 0.5.5 (RUSTSEC-2026-0387); the base and PR have identical Cargo.lock hashes, so this appears inherited, but the required check still needs maintainer disposition. E2E, Merkle E2E and real Chromium checks were still running at this review snapshot.
Second review of WithAutonomi#249: a joiner was charged nothing for a shared round that found nothing. The GET count was raised through a counter the leader alone passed into the fetch, and a failed round returns an error carrying no chunk to report with, so with two concurrent downloads of the same uncached chunk the failed round was counted for the leader and silently free for the follower. The count now rides in the shared result itself. `SharedChunkGet` pairs the outcome with the GETs the fetch dispatched, the single-flight map holds that pair, and `chunk_get_shared` adds the count to whichever caller it is serving — leader or joiner — before handing back the outcome. The same GETs served them both, so they are charged alike whatever the outcome. This also tidies the split that made the bug possible. `chunk_get` now reports only the serving peer (`SourcedChunk` is a chunk and an `Option<PeerId>`) and leaves the counting to the counter, so the download path accumulates one number from one source instead of taking a successful round's count from the chunk and a failed round's from the counter. `ChunkSource` is now built where it is reported, in `data_download_indexed_chunks`, which lets it be browser-only and drops the `allow(dead_code)` the native build needed. The new wasm test runs two concurrent downloads of a file whose record is withheld for the first two GETs, with responses held open so the second download joins the first's fetch. Observed over five runs: 3 GETs for that record and both downloads reporting `[1, 1, 3]`. Before this change the joiner reported `[1, 1, 1]`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Confirmed and fixed in 369ddba. sol-1 is right: I had documented the joiner's zero as correct The fix. The count now rides in the shared result rather than in a counter only the leader /// What one shared `chunk_get` resolves to: its outcome, and the number of
/// peer GETs the fetch dispatched. Both reach a joiner, so sharing a fetch
/// costs a caller the same as leading it — including a round that found
/// nothing, whose outcome is an error and so carries no chunk to report with.
pub(crate) type SharedChunkGet = (Result<Option<SourcedChunk>>, usize);The single-flight map holds that pair, and let (outcome, sent) = match flown {
Flown::Led { value, observe } => { /* observe value.0 */ value }
Flown::Joined(value) => value,
};
if let Some(dispatched) = dispatched {
dispatched.fetch_add(sent, Ordering::Relaxed);
}
outcomeIt also removes the split that made the bug possible. Previously the download path took a let total = { *spent.entry(address).or_default() += dispatched.load(Relaxed); ... };
let (bytes, peer) = fetched?;
sources.borrow_mut().insert(address, ChunkSource { peer, attempts: total });A side benefit: Test — "downloads sharing a failed round are both charged for it". Two concurrent Three GETs for the withheld record, both downloads reporting all three. Before this change the One note on the suggested assertion: "both report the same cumulative count" holds only when the Full Security Audit: agreed it is inherited — 🤖 Generated with Claude Code |
MaidSafe-Hermes
left a comment
There was a problem hiding this comment.
APPROVE — 2/2 seats reviewed (sol-1 gpt-6.1-sol xhigh; glm-1 z-ai/glm-5.3). The original retry-round accounting issue and the shared-fetch joiner case are fixed in this head. The new single-download and concurrent-download retry tests passed in the Browser WASM CI job; the real Chromium/local-node job also passed.
GLM-1 returned APPROVE. Sol-1 returned UNCERTAIN because it could not confirm test determinism from the supplied evidence. I checked the mock's synchronous response ordering and the current-head Browser WASM result; I find no unresolved code objection.
CI is not all green: E2E and Merkle E2E jobs remain in progress. Security Audit fails on crypto-bigint 0.5.5 (RUSTSEC-2026-0387); base and PR have the same Cargo.lock object. Per the WithAutonomi organization owner's direction, I treat these unrelated/inherited checks as non-blocking for this browser-only review. This approval does not authorize merge or release.
A browser download reports per chunk the GETs it cost (main's WithAutonomi#249). The count was raised when a GET's future started, before the GET waited for its turn under the browser's read budget. A GET still waiting there is dropped, unsent, when a linked reader sends the record meanwhile, and the chunk then reported a GET that never left; a download that joined the fetch was charged the same. A GET now counts once it is sent, or once it ends without being sent (it failed first, which main counts as a try that cost the chunk), and not when it is dropped while it still waits. A unit test covers the three cases.
Linear issue
Closes V2-1872
Risk tier
The visible change is progress text, but it reaches it by threading a new
return value through the client's chunk-fetch path, so it is more than a pure
output change. No request, response or stored byte changes.
Compatibility
ant-corepublic signature changes —chunk_get,chunk_get_from_closest_peers,chunk_get_observedandchunk_get_observed_from_closest_peerskeep their current types. Thebrowser's
downloadPublicFile/downloadPrivateFileprogress strings gain asuffix; the
Downloaded chunk n/totalprefix is unchanged, so the startupprobes (
browser-tests/read-startup.mjs,browser-tests/sdk-startup.mjs)that match on it still work.
Semver impact
Test evidence
Gates run locally:
cargo fmt --all -- --check— pass.cargo check -p ant-core --target wasm32-unknown-unknown --no-default-features --features browser-wasm— pass.cargo clippy -p ant-core --target wasm32-unknown-unknown --no-default-features --features browser-wasm -- -D warnings— pass.cargo clippy --all-targets --all-features -- -D warnings— left to CI (thefull-feature build had not finished locally when this was opened); the
lintjob covers it.New and updated tests, run by the
browser-wasmjob:ant-core/wasm-tests/large-download.test.mjs— the in-memory download testnow asserts each delivered chunk reports a 64-character lowercase peer hex
belonging to one of the mock nodes and
after 1 attempts, with the firstline still the bare
Downloaded chunk 0/3.from cache after 0 attemptsfor every chunk, and that it re-requested noneof the file's data records.
chunk_get_outcome's unit test covers the generic signature, including thechunk-plus-source pair the browser path carries.
New dependency
none
ADR
n/a (Tier 1).
docs/adr/ADR-0004-direct-browser-read-client.mdis updatedwhere it documents the progress line.
Mitigation / rollback
Revert the commit. The change is additive text on one progress line plus an
internal return value; nothing persists it and no peer sees it.
🤖 Generated with Claude Code