* Store: unsettle a salvaged-away row so the next observation rewrites it R8 row 3 (F1). When a batch loses a row to a database failure the ledger kept the binding it made before the commit, so the next walk resolved the same file Unchanged and the row stayed missing for as long as its size and mtime held. commit now takes the ledger and unsettles each row it could not land: the uuid keeps its path, the size and mtime evidence is cleared, and the next observation resolves Changed and writes the whole row under the identity the arena already holds. Un-ignores a_repeat_observation_repairs_a_failed_write. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Snapshot: move an unreadable artifact aside instead of deleting it R8 row 10 (F2). load_snapshot_impl removed a snapshot it could not read, decode, or accept the version of, so the artifact was gone before anyone could inspect it. It now moves to <name>.corrupt-<unix seconds> beside the slot with a WARN naming both paths; the slot is clear for the next save and no launch parses it again. Un-ignores an_invalid_snapshot_is_retained_for_diagnosis, which now asserts the bytes survive at the aside path. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Watcher: register a root only after the OS accepts, report and retry refusals R8 row 11 (F3). watch_root registered the root with the volume index before subscribing at the OS, so a refused subscription left the root in watched_paths and is_watched and core.index_status reported a watch that did not exist. The OS subscription now comes first; a refusal is recorded on the volume index with its reason, surfaces as IndexStatus::refused_watches (and in sd index status), and is retried every watch_retry_interval (30 s) while the service runs. A retry the OS accepts moves the root to watched_paths. Un-ignores a_refused_watch_is_not_reported_active and extends it with the status report and the reconciling retry. Regenerates ts-client types. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Search: answer a file under nested sources once from the stores R8 row 25 (F5). With an outer and an inner source both walked, the file under the inner root sits in both stores, and after a restart with no snapshot search_every_index queried each store in turn and returned it twice. The store loop now reads sources innermost first and drops a hit whose local path an earlier partition (an inner store or a drive's arena) already answered, before it is counted, so total_found stays the number of distinct files. Un-ignores a_file_under_nested_sources_is_one_hit_from_the_stores. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Docs: R8 rows 3, 10, 11 and 25 fixed; row 17 remains the R4 brief Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Review: one retained corrupt snapshot, full-arena dedupe seed, reset clears refusals A recurring load failure no longer retains an artifact per launch: when a .corrupt-* sibling exists the slot is removed instead, keeping the oldest copy as the mismatched-root path does. The store de-duplication seed is collected from each arena's full match set before the page window narrows it, so total_found stays exact beyond the page. clear_all forgets refused watches along with the watched paths. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com>
14 KiB
Source runtime acceptance matrix
The R8 table in
the source runtime reliability plan
lists 28 scenarios. This file maps each one to the automated test that proves
it, states whether that test passes on the CI runner (Blacksmith Ubuntu
24.04, core_tests.yml), and records the observed behavior where it does not.
Nothing here depends on a personal machine or a NAS.
Status values:
passing: the named test runs in CI and passes at the commit that landed this file.failing: the named test exists, is marked#[ignore = "R8: <row> fails: <reason>"], and fails when run with--ignored. Each one is a fix brief.fixed: the row failed at the commit that landed this file and a later commit made its test pass; the row's section below records the root cause and the fix.not automatable: no Linux test can prove the row; the reason says why.
How the suites run:
- Colocated tests (
core/src/...) run in the--libsuite ofcargo xtask test-core --unit. - The other three run as
cargo xtask test-core --acceptance, theacceptancejob ofcore_tests.yml:crates/store/tests("Store crate tests");core/tests/source_runtime_acceptance_test.rs, oneCoreover temporary directories (the single-daemon rows);core/tests/source_replication_test.rs, two daemons in separate processes paired over loopback (the peer rows).
Run an ignored row by name, for example:
cargo test -p sd-core --lib repeated_clear_and_refill_keeps_allocation_bounded -- --ignored
Matrix
| # | Scenario | Test | Status |
|---|---|---|---|
| 1 | Child before parent across batches | core/src/ops/indexing/store.rs a_child_arriving_before_its_parent_batch_gets_real_ancestry, a_batch_builds_the_tree_whatever_order_it_arrives_in; crates/store/tests/files.rs a_child_ahead_of_its_parent_still_commits |
passing |
| 2 | Shallow browse inside an empty retained source | core/src/ops/indexing/store.rs a_shallow_browse_in_an_empty_store_persists_its_ancestry |
passing |
| 3 | One failed row and successful siblings | core/src/ops/indexing/store.rs one_failed_row_keeps_its_siblings_and_fails_the_flush (siblings retained, flush refuses and counts the loss); a_repeat_observation_repairs_a_failed_write (the R1 proof that a repeat observation repairs the row) |
passing / fixed (see F1) |
| 4 | Writer dies or pending query fails | core/src/ops/indexing/store.rs a_dead_writer_is_a_typed_failure_not_an_empty_queue, a_failed_commit_surfaces_at_the_flush_barrier |
passing |
| 5 | Restart between identity assignment and commit | core/src/ops/indexing/store.rs a_restart_after_assignment_keeps_the_identity_and_lands_the_write |
passing |
| 6 | Interrupted or partially denied scan | crates/store/tests/files.rs an_interrupted_sweep_does_not_delete_the_source, a_subtree_the_walk_could_not_read_survives_the_sweep, a_sweep_that_saw_almost_nothing_is_refused; core/src/ops/indexing/store.rs a_locked_folder_costs_its_subtree_not_the_walk; core/src/ops/indexing/job.rs only_a_whole_unfiltered_walk_may_sweep, a_browse_may_not_sweep, rules_bar_a_sweep, a_depth_limit_bars_a_sweep |
passing |
| 7 | Old schema with origin offline | crates/store/tests/files.rs an_index_that_predates_parent_addressing_is_refused_intact (the compatibility-status branch: the old table and its assertions stay intact; no data-preserving migration exists yet) |
passing |
| 8 | Same volume, nested roots, reversed registration order | core/src/ops/indexing/sources.rs a_nested_source_never_redefines_its_volume_root, reversed_registration_order_keeps_one_volume_boundary; core/src/ops/indexing/volume_index.rs a_nested_source_shares_the_drive_it_sits_on, nested_sources_registered_inner_first_share_one_map_and_identity |
passing |
| 9 | Remount and APFS alias changes | Remount: core/src/ops/indexing/sources.rs a_remount_keeps_the_source, folders_on_one_drive_are_distinct_sources, a_different_drive_at_the_same_mount_point_is_a_different_source; core/src/ops/indexing/volume_index.rs a_root_mismatched_snapshot_moves_aside_instead_of_deleting. APFS alias: none |
passing (remount) / not automatable (APFS: firmlink spellings such as /System/Volumes/Data/Users/x only exist on macOS; VolumeManager::locate_path resolves them through the live volume list, which a Linux runner cannot produce) |
| 10 | Missing or invalid restart snapshot | core/src/ops/indexing/volume_index.rs an_invalid_snapshot_leaves_the_source_visible_and_its_store_readable, test_snapshot_roundtrip_and_detached_restore (missing); an_invalid_snapshot_is_retained_for_diagnosis |
passing / fixed (see F2) |
| 11 | Failed watcher subscription | core/tests/source_runtime_acceptance_test.rs a_refused_watch_is_not_reported_active (refusal reported with its reason on core.index_status, then a retry flips the root to active) |
fixed (see F3) |
| 12 | Offline client restart | core/src/service/mounts/peer.rs a_cold_restore_rebuilds_the_inventory_without_the_owner, published_facts_outlive_the_owners_connection, a_manifest_without_facts_still_restores |
passing |
| 13 | One failing source among nine | core/src/service/mounts/peer.rs one_failing_source_among_nine_stays_listed_as_unavailable |
passing |
| 14 | Listing/fetch generation race | core/src/service/mounts/peer.rs the_generation_recorded_names_the_delivered_bytes, a_corrupt_delivery_never_replaces_a_good_artifact |
passing |
| 15 | Unchanged owner for ten intervals | core/tests/source_replication_test.rs test_source_replication (Bob's ten passes after convergence: same generation, same sync time, same loaded arena); core/src/service/mounts/peer.rs dirtiness_alone_paces_while_a_moved_generation_transfers, an_unchanged_store_exports_identical_bytes; crates/store/tests/revision.rs reopening_an_unchanged_store_rewrites_no_triggers |
passing |
| 16 | Continuous real writes | core/tests/source_replication_test.rs test_source_replication (Alice writes and re-walks; Bob lands a newer generation holding every record); crates/store/tests/revision.rs file_changes_move_the_revision_and_nothing_else_does |
passing |
| 17 | Repeated subtree clear and refill | core/src/ops/indexing/arena.rs repeated_clear_and_refill_keeps_allocation_bounded |
failing (see F4) |
| 18 | Cold search across 100 stores | crates/store/tests/scale.rs a_hundred_cold_stores_answer_without_writers (CI size: 100 stores x 200 records); a_cold_fan_out_across_many_stores (the measurement, ignored, sized by SD_SCALE_*) |
passing |
| 19 | Same capture read through arena and SQLite | core/src/ops/search/arena_search.rs the_store_backend_matches_the_arena_for_the_same_capture (identities, matching, filters, scores); core/src/ops/search/pipeline.rs every_sort_field_orders_and_reverses, equal_scores_tiebreak_deterministically, a_page_is_a_window_over_the_sorted_whole (ordering and pagination shared by both backends) |
passing |
| 20 | Suitable loaded arena returns no matches | core/tests/source_runtime_acceptance_test.rs an_empty_answer_from_a_suitable_arena_is_final |
passing |
| 21 | Loaded arena has insufficient coverage or query support | core/tests/source_runtime_acceptance_test.rs an_unwalked_source_answers_from_its_store |
passing |
| 22 | Five suitable arenas and 95 stores | core/tests/source_runtime_acceptance_test.rs five_arenas_and_ninety_five_stores_page_the_same |
passing |
| 23 | Arena candidates need store-only filter or sort fields | core/tests/source_runtime_acceptance_test.rs a_store_only_filter_narrows_before_pagination (tags are the store-only field) |
passing |
| 24 | Search with requested limit five | core/tests/source_runtime_acceptance_test.rs a_limit_of_five_returns_five_with_an_honest_total |
passing |
| 25 | Same record in multiple representations | core/tests/source_runtime_acceptance_test.rs a_file_under_nested_sources_is_one_hit_from_the_arena (loaded); a_file_under_nested_sources_is_one_hit_from_the_stores (store-backed) |
passing / fixed (see F5) |
| 26 | Replica replacement with local assertions | none | not automatable today: replicas open read-only and carry no receiver-owned assertions (R6 results, "receiver-owned assertions on replica databases" is a registered follow-on gated on FD2), so there is no local assertion to preserve. crates/store/tests/files.rs a_moved_file_carries_its_assertions_with_it and a_removal_takes_the_facet_and_leaves_the_assertion cover the owner-side contract the replica path will have to reuse |
| 27 | Mapped volume with no source | core/src/ops/indexing/volume_index.rs a_tracked_drive_maps_without_appearing_as_a_source (browse and snapshot), a_mapped_drive_without_a_source_is_watchable (watcher); core/src/ops/indexing/store.rs a_partition_with_no_store_still_browses |
passing |
| 28 | Status query and resource event | core/tests/source_runtime_acceptance_test.rs listing_status_and_store_agree_on_a_sources_count (sources.list, core.index_status and the store report one count and one observation time for one source) |
passing |
Totals: 25 rows fully passing (21 at the commit that landed this file, plus rows 3, 10, 11 and 25 fixed since); 1 row failing (17); row 9 passing except its APFS half, which no Linux runner can produce; row 26 not automatable because the feature it names does not exist yet. One ignored test remains (F4).
Failing and fixed rows
F4 is an ignored test that fails; run it with --ignored to see the
behavior. F1, F2, F3 and F5 record what failed at the commit that landed
this file and what fixed it.
F1. Row 3: a repeat observation did not repair a failed write (fixed)
core/src/ops/indexing/store.rs a_repeat_observation_repairs_a_failed_write.
After a batch lost one row to a database failure, the ledger kept the
binding it made before the commit. The next walk observed the same file
unchanged, the ledger answered Unchanged, and nothing was written: the row
was missing for as long as the file's size and mtime held. Observed: the
store held ["docs"] where ["docs", "docs/bad.txt"] was expected.
Fix: commit (store.rs) now takes the ledger and calls
Ledger::unsettle(uuid) for every row the salvage could not land.
unsettle keeps the uuid at its path but clears the size and mtime
evidence, so the next observation resolves Changed and rewrites the whole
row under the identity the arena already holds. Dropping the binding
instead would have minted a second identity for the same file.
F2. Row 10: an unreadable snapshot was deleted on load (fixed)
core/src/ops/indexing/volume_index.rs an_invalid_snapshot_is_retained_for_diagnosis.
snapshot.rs removed the file when it could not decode or parse it, so the
artifact was gone before anyone could inspect it. The source stayed visible
and its store still answered (the passing half of the row); only retention
failed.
Fix: load_snapshot_impl moves an artifact it cannot read, decode, or
accept the version of to <name>.corrupt-<unix seconds> beside the slot
and logs a WARN naming both paths. The slot is clear for the next save and
no launch parses the artifact again; the test asserts the bytes survive at
the aside path. One retained copy is enough: when a .corrupt-* sibling
already exists, a later unreadable artifact in the slot is removed instead,
so a recurring failure cannot fill the disk.
F3. Row 11: a refused watch was reported active (fixed)
core/tests/source_runtime_acceptance_test.rs a_refused_watch_is_not_reported_active.
FsWatcherService::watch_root called VolumeIndex::register_for_watching
before watcher.watch_path. When the OS refused the subscription (the
directory was removed), the error propagated but the volume index kept the
root in watched_paths, so is_watched and core.index_status reported
an active watch that did not exist.
Fix: watch_root subscribes at the OS first and registers only after the
OS accepts; a refusal is recorded on the volume index with its reason
(VolumeIndex::record_watch_refusal) and surfaces as
IndexStatus::refused_watches, which sd index status prints beside the
watched roots. The service retries every refused root on
FsWatcherServiceConfig::watch_retry_interval (30 s) while it runs
(retry_refused_watches), and a retry the OS accepts moves the root from
the refused list to watched_paths. The test covers both halves: the
refusal with its reason, then the directory returning and one retry arming
the watch.
F4. Row 17: arena allocation grows with history
core/src/ops/indexing/arena.rs repeated_clear_and_refill_keeps_allocation_bounded.
One hundred clear-and-refill cycles over a 50-file branch leave 54 live
paths and 5,154 allocated slots. NodeArena::vacate keeps the slot, nothing
compacts, and the snapshot serializes every slot. This is R4, which has not
landed.
F5. Row 25: store-backed search returned one hit per nested store (fixed)
core/tests/source_runtime_acceptance_test.rs a_file_under_nested_sources_is_one_hit_from_the_stores.
With an outer and an inner source both walked, the file under the inner root
is committed to both stores. After a restart with no snapshot, every source
answered from its store and search_every_index queried each store in turn,
so the file surfaced twice (total_found 2 where 1 was expected). The
loaded case was one hit because both registrations share one arena.
Fix: the store loop in search_every_index (arena_search.rs) reads sources
innermost first and keeps a set of local paths already answered, seeded
with every arena's full match set (collected before the page window
narrows it), so a file an inner store (its owner) returned is dropped from
the outer store's partition before it is counted. The total stays exact
because the de-duplication runs on each store's full match set ahead of the
page window.
Not in the plan's table but covered on the way
- Unchanged refresh:
an_unchanged_generation_still_refreshes_the_owner_facts(peer.rs). - Resumable replica transfer:
an_interrupted_transfer_resumes_from_the_part_length,a_part_from_another_generation_starts_over(peer.rs). - Database replica restore:
a_database_artifact_restores_into_a_browsable_replica(peer.rs).