Skip to content

fix(test): stop test_download_policies hanging on a double-counted download - #20

Merged
jonaswre merged 1 commit into
mainfrom
fix/download-policies-double-count
Aug 1, 2026
Merged

fix(test): stop test_download_policies hanging on a double-counted download#20
jonaswre merged 1 commit into
mainfrom
fix/download-policies-double-count

Conversation

@jonaswre

@jonaswre jonaswre commented Aug 1, 2026

Copy link
Copy Markdown

test_download_policies has been timing out at its own 120s TIMEOUT on i686, Windows and Ubuntu, blocking #16, #17 and #19. It is not architecture-specific and never was — it is a race that any sufficiently loaded machine loses.

The bug

The event loop breaks only on exact equality of counters that never decrease:

if synced_a == EXPECTED_A_SYNCED
    && downloaded_a.len() == EXPECTED_A_DOWNLOADED
    ...

and downloaded_* was pushed from two event arms that are not mutually exclusive:

Event Source
ContentReady { hash } emitted by on_download_ready when a download completes (engine/live.rs:686)
InsertRemote { content_status } status from content_status_cb(...), evaluated when the event is converted (engine.rs:348)

Under scheduling pressure the download finishes and emits ContentReady before its InsertRemote reaches the subscriber. By the time that event converts, the content is present — so its status reads Complete and the same key is recorded twice.

downloaded_b.len() reaches 4 against an expected 3, the equality can never hold again, and the loop spins to the deadline. That is why the timing is always binary: ~0.2s or exactly 120s, never between.

Evidence

Reproduced locally by saturating all 8 cores: 40 consecutive passes when idle, then failures under load.

Instrumenting every push site proved the mechanism directly rather than by inference:

iter 3: DUPE b via InsertRemote{Complete}
iter 8: DUPE b via InsertRemote{Complete}
=== duplicate detected in 2/14 runs; 2 timed out ===

The counts match exactly. This also corrected my initial reading of the ordering: ContentReady arrives first, and InsertRemote is the duplicate.

Why this is a test bug, not a product bug

The two events mean different things — "download finished" versus "entry synced, content already present" — and a real consumer must handle both. Only the test assumed they were exclusive. Recording each key once is what it actually wanted; the assertions already compare exact contents.

The change

  1. Dedup each key on record.
  2. ==>=. The dedup fixes this instance, but an exact comparison against a monotonic counter is a hang waiting to happen — any future overshoot would spin for 120s with no diagnosis. With >= the loop exits and the existing assertions report the discrepancy legibly.

Verification

  • 0 failures in 25 runs under full CPU load, against a pre-fix baseline of 2 in 14 under identical load
  • Full krikos-docs suite green (99 passed)

🤖 Generated with Claude Code

…wnload

This test has been timing out at its own 120s TIMEOUT on i686, Windows
and Ubuntu, blocking three PRs. It is not architecture-specific and never
was; it is a race that any sufficiently loaded machine loses.

The event loop breaks only on exact equality of counters that never
decrease:

    if synced_a == EXPECTED_A_SYNCED
        && downloaded_a.len() == EXPECTED_A_DOWNLOADED
        ...

and `downloaded_*` was pushed from two event arms that are not mutually
exclusive:

  - ContentReady { hash } -- emitted by `on_download_ready` when a
    download completes (engine/live.rs);
  - InsertRemote { content_status } -- where the status comes from
    `content_status_cb(entry.content_hash())`, evaluated when the event
    is CONVERTED (engine.rs), not when the entry synced.

Under scheduling pressure the download finishes and emits ContentReady
before its InsertRemote reaches the subscriber. By the time that event
converts, the content is present, so its status reads Complete and the
same key is recorded twice. downloaded_b.len() reaches 4 against an
expected 3, the equality can never hold again, and the loop spins until
the deadline.

Reproduced locally by saturating all 8 cores: 40 consecutive passes when
idle, then failures under load. Instrumenting every push site proved the
mechanism directly rather than by inference -- 2 of 14 loaded runs
printed "DUPE b via InsertRemote{Complete}" and exactly 2 timed out, the
counts matching. That also corrected the ordering: ContentReady arrives
first, and InsertRemote is the duplicate.

This is a test bug, not a product bug. The two events mean different
things -- "download finished" versus "entry synced, content already
present" -- and a real consumer must handle both. Only the test assumed
they were exclusive, so recording each key once is what it actually
wanted; the assertions already compare exact contents.

Also relax `==` to `>=`. The dedup fixes this instance, but an exact
comparison against a monotonic counter is a hang waiting to happen: any
future overshoot would spin for 120s with no diagnosis. With `>=` the
loop exits and the existing assertions report the discrepancy legibly.

Verified: 0 failures in 25 runs under full CPU load, against a pre-fix
baseline of 2 in 14 under the same load. Full krikos-docs suite green
(99 passed).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

Documentation for this PR has been generated and is available at: https://holon-technologies.github.io/iroh/pr/20/docs/krikos/

Last updated: 2026-08-01T20:11:53Z

@jonaswre
jonaswre merged commit ef851de into main Aug 1, 2026
47 checks passed
@jonaswre
jonaswre deleted the fix/download-policies-double-count branch August 1, 2026 20:32
jonaswre added a commit that referenced this pull request Aug 2, 2026
* fix(test): un-ignore four flaky tests; fix the content_status race

Four of the five tests carrying #[ignore = "flaky"] are now enabled.
Three of them were not flaky at all any more, and the fourth had a real
race with a measurable cause.

Measured first, before changing anything -- 5 runs of all five ignored
tests:

  connect_via_relay_becomes_direct_and_sends_direct   5/5 pass
  sync_restart_node                                   5/5 pass
  sync_big                                            5/5 pass
  sync_full_basic                                     4/5 FAIL
  test_roundtrip_bytes_small                          5/5 FAIL

The three that pass were left ignored long after whatever made them
flaky stopped happening. Nothing re-checks an #[ignore], so they simply
stayed off.

sync_full_basic passes in isolation and fails only alongside other
tests. The failure is not a timeout:

  Event didn't match any matcher:
    InsertRemote { ..., content_status: Incomplete }

Its matchers required `content_status: ContentStatus::Missing`. But
content_status is evaluated when the event is CONVERTED, not when the
entry synced -- the same timing-dependent field behind the double-count
fixed in #20 -- so whether a download has started by then is a race.

Measured which values actually occur, over 20 runs under full CPU load,
by accepting any status and logging it:

  peer0/from-peer1    Missing 20/20
  peer2/hash0         Incomplete 12, Missing 8
  peer2/hash1         Missing 13, Incomplete 7
  Complete            never observed

So "not yet complete" is a real assertion that holds, while the
Missing/Incomplete split is pure timing. Match both. Applied to all six
sites, including sync_restart_node's three -- those pass today but carry
the identical latent race.

Verified: the four run together 12 times under full CPU saturation with
0 failures, in the configuration that previously failed sync_full_basic
4 times in 5. Full suites green with nothing skipped -- krikos-docs
102/102, krikos 155/155.

test_roundtrip_bytes_small stays ignored and needs a decision, not a
fix: it fails 5/5, so it is broken rather than flaky and has never given
signal. It asserts `expected.addr() == actual.addr()`, i.e. that
get_bytes hands back the same allocation add_bytes was given. That is an
implementation detail the store does not guarantee, which the ignore
reason itself concedes ("I need a reliable way to keep the handle
alive").

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

* fix(test): enable test_roundtrip_bytes_small by dropping an unfounded assertion

Last of the five #[ignore = "flaky"] tests. It was not flaky: it failed
5/5, so it had never given signal, and the "flaky" label hid that.

It asserted

    assert_eq!(&expected.addr(), &actual.addr(), ...)

i.e. that `get_bytes` hands back the same allocation `add_bytes` was
given. The store makes no such promise:

  - reads go through `export_bao(..).data_to_bytes()`;
  - an entry may be `MemOrFile::Mem` or `MemOrFile::File`, and from disk
    a copy is unavoidable;
  - zero-copy is documented only as an internal property of the
    in-memory variant (store/fs/bao_file.rs), not as API behaviour;
  - it additionally requires a live handle, which the test had no
    reliable way to hold -- the ignore reason said exactly this.

So the assertion was true only by coincidence. Drop it and keep what the
API does promise: the data round-trips, the hash matches, and the entry
reaches completion.

The alternative -- making zero-copy a public guarantee so the assertion
becomes honest -- was rejected deliberately. It would foreclose
encryption at rest, compression and checksum-on-read for the sake of one
assertion. Read-path allocation behaviour belongs in a benchmark, which
says "this should stay fast" without freezing how. The comment in the
test records that reasoning so the assertion is not reinstated blindly.

Nothing is lost by removing it: the test has been disabled and failing,
so it was protecting nothing.

Also refresh scripts/determinism-boundaries.txt. Removing one #[ignore]
line from krikos/src/endpoint/tests.rs shifted every boundary below it.
Verified this is pure line drift before refreshing: 1078 entries before
and after, 0 with any change to kind/file/code, exactly 22 line numbers
moved by -1 with identical code text.

No #[ignore = "flaky"] remains in the tree. check-flaky-sweep-scope.sh
now reports 0 flaky tests watched, which is the intended end state: a
red nightly sweep from here means a genuinely new flake.

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

* fix(test): drop the import orphaned by removing the addr assertion

`SliceInfoExt` provides `.addr()`. Removing that assertion in the
previous commit left the import unused, which under CI's `-Dwarnings` is
a hard error -- it failed clippy, MSRV and every test job on every
platform.

My local check did not catch it because `cargo nextest run` does not
compile with warnings-as-errors: 115/115 passed while the code did not
build the way CI builds it. Running the tests answers a different
question from "does this compile cleanly", and I reported the first as
if it settled the second.

Verified this time by exit code rather than absence of output:
  cargo check --workspace --all-targets --all-features (-Dwarnings)  0
  cargo clippy --workspace --all-targets --all-features -D warnings  0
  cargo fmt --all -- --check                                         0
plus krikos-blobs 115/115 and krikos-docs 102/102.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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