Skip to content

The one-scan config test tolerates a snapshot TTL that elapsed during its own loop - #570

Open
emooreatx wants to merge 3 commits into
mainfrom
fix/graph-config-scan-test-tolerates-the-ttl
Open

The one-scan config test tolerates a snapshot TTL that elapsed during its own loop#570
emooreatx wants to merge 3 commits into
mainfrom
fix/graph-config-scan-test-tolerates-the-ttl

Conversation

@emooreatx

Copy link
Copy Markdown
Contributor

main 55b8c7a failed on macOS and Windows: "150 keyed reads + one list must
cost ONE scan; got 2". On a cold runner the 150 awaits outlived the 2 s
snapshot TTL and the cache expired mid-loop — the cache working, not a scan
per read. The budget is now 1 + elapsed / TTL, derived from the measured
loop time; ubuntu still sees exactly 1.

Main 55b8c7a: clippy + test (macos-latest) and (windows-latest) both red on this test; ubuntu green. Test-only change.

🤖 Generated with Claude Code

…n loop

main 55b8c7a failed on macOS and Windows: "150 keyed reads + one list must
cost ONE scan; got 2". On a cold runner the 150 awaits outlived the 2 s
snapshot TTL and the cache expired mid-loop — the cache working, not a scan
per read. The budget is now 1 + elapsed / TTL, derived from the measured
loop time; ubuntu still sees exactly 1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3de93f1777

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tests/graph_config.rs Outdated
Comment on lines +413 to +416
let budget = 1 + (elapsed.as_millis() / graph_config::CONFIG_SNAPSHOT_TTL.as_millis()) as u64;
let scans = after - before;
assert!(
(1..=budget).contains(&scans),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Isolate scan accounting from concurrent graph-config tests

This elapsed-time budget still leaves the test flaky under the normal parallel test harness: SCANS is process-global, and the graph-config cache is a single process-wide slot, while the other #[tokio::test] cases in this same integration binary create different engines and call config reads/writes. If one of those tests scans or replaces/invalidates the cache between before and after, this test can record multiple scans even when elapsed < CONFIG_SNAPSHOT_TTL, so budget == 1 and the assertion fails for reasons unrelated to TTL expiry. Serialize this test or use per-engine/snapshot-local accounting rather than bounding the shared counter by this test's elapsed time.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in a5782ad, at the root rather than by serialising the test: the cache is now one slot per engine (a bounded map keyed by engine identity, CACHE_SLOTS = 8) and the writers invalidate per engine (invalidate_engine), so a sibling test's engine can no longer evict this test's snapshot or count against it. The test compares its OWN engine's snapshot ordinals before and after the reads (tolerating one rescan per TTL elapsed) and no longer touches the process-wide SCANS. graph_config 3/3, conformance_gate (the keyed-cache gate) green.

…ine, and the one-scan test reads its own engine's ordinal (Codex on #570)

The TTL budget alone left the test flaky under the parallel harness: the
cache was ONE process-wide slot and SCANS a process-wide counter, so a
sibling test's engine reading or writing its own config plane evicted this
test's snapshot and counted a scan against it with no TTL involved.

- `graph_config`: a small map of (engine identity → snapshot), CACHE_SLOTS
  = 8, oldest evicted; `invalidate_engine(&Engine)` drops one engine's slot
  and is what `set_config` and `attest::put` call — a write to one store
  says nothing about another's; `invalidate()` still clears all (compose's
  re-serve). `engine_identity` takes `&Engine` (the value's address, which
  is `Arc::as_ptr` for an Arc) so `attest::put`'s `&Engine` names the same
  slot.
- The test compares ITS engine's snapshot ordinals before and after the
  150 reads, tolerating one rescan per TTL elapsed; the process counter is
  no longer part of the assertion.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@emooreatx

Copy link
Copy Markdown
Contributor Author

Further evidence for the root fix: the same test just failed on ubuntu in PR #569's run (34251718688, a branch without this change), with no slow lane involved — a sibling engine evicted the one shared slot. The per-engine slots in a5782ad are what removes it.

graph_config.rs, attest.rs and node_key.rs were not in the ladder's path
filter, so #570 (the per-engine snapshot cache) would have merged with the
config plane proven only by unit tests. Every node writes config at boot
through graph_config and peers through attest::put; the ladder is what
proved 0.5.201's per-key leaf on real nodes. Now it runs for these files.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@emooreatx

Copy link
Copy Markdown
Contributor Author

915af08 adds src/graph_config.rs, src/attest.rs and src/node_key.rs to the mesh-harness ladder's path filter: this PR changes the plane every node writes at boot and the ladders were not running for it (the filter listed neither file). The traceflow and chat ladders now run on this head before merge; the conformance run's first attempt on a5782ad was an Actions startup failure (no jobs spawned) and passed on rerun.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 915af08e2b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tests/graph_config.rs
Comment on lines +412 to +415
let after = graph_config::snapshot(&engine)
.await
.expect("snapshot")
.scan;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep the post-loop assertion engine-local

When a sibling test scans its engine after this ordinal is captured, the unchanged assertion at lines 434–438 compares the process-global SCANS counter with this engine-local after value and fails even though this engine reused its cache. Fresh evidence beyond the earlier review is that the new ordinal is still fed into that global equality check; compare the returned snapshot's scan instead so concurrent engines cannot affect the result.

Useful? React with 👍 / 👎.

Comment thread src/graph_config.rs
/// How many engines keep a snapshot at once. A node has one; the bound exists
/// so a process that churns engines (a test binary, the embedded fold
/// re-serving) cannot grow the map without limit.
const CACHE_SLOTS: usize = 8;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Prevent active test engines from being evicted

Under the normal parallel harness, this integration binary has ten Tokio tests that each create an engine, but the cache retains only eight engine slots and cache hits do not refresh FIFO order. If enough sibling tests populate slots while fifty_reads_are_one_scan_and_a_write_invalidates is running, slots.remove(0) can evict its still-active snapshot; its next keyed read then rescans before a TTL has elapsed, making rescans == 1 with budget == 0. Fresh evidence beyond the earlier review is the mismatch between this eight-slot bound and the ten engine-owning tests, so the claimed per-engine isolation remains conditional on scheduling.

Useful? React with 👍 / 👎.

Comment thread src/graph_config.rs
Comment on lines +501 to +505
fn engine_identity(engine: &Engine) -> usize {
// The address of the `Engine` value itself — for an `Arc<Engine>` this is
// exactly `Arc::as_ptr`, and it lets a door that holds only `&Engine`
// (`attest::put`) name the same slot.
std::ptr::from_ref(engine) as usize

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid retaining recyclable addresses as engine identities

When engines are created and dropped in one process, an Engine allocation address can be reused while this new multi-slot cache still retains the prior owner's fresh snapshot. For example, after engine A is cached, engine B is cached, A is dropped, and engine C is allocated at A's address, C's first read can match A's slot and return A's configuration without scanning C's store; the former single-slot cache would contain only B in this sequence. Key the entry with an identity whose lifetime is tied to the engine (such as a Weak<Engine> checked by pointer equality) rather than a bare recyclable integer address.

Useful? React with 👍 / 👎.

Comment thread tests/graph_config.rs
Comment on lines +421 to +423
let elapsed = t0.elapsed();
let budget = (elapsed.as_millis() / graph_config::CONFIG_SNAPSHOT_TTL.as_millis()) as u64;
let rescans = after - before;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Start timing before creating the baseline snapshot

If this test task is delayed after first is returned but before t0 is recorded, the baseline snapshot can already be near or beyond its TTL while the measured elapsed remains below one TTL. The first keyed read then legitimately rescans, producing rescans == 1 and budget == 0; even without a long pause there is always a boundary window equal to the unmeasured age. Start the timer before loading first, or otherwise include the baseline snapshot's age when calculating the allowance.

Useful? React with 👍 / 👎.

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