fix(scanner): fence movement generation publication (#6461)

* feat(scanner): add movement generation fencing

* fix(scanner): prioritize unverified cycle deferral

* feat(ecstore): add scanner publication lease fence

* feat(rpc): add scanner publication lease protocol

* feat(scanner): hold remote leases through usage publish

* test(scanner): cover publication lease fencing

* fix(scanner): fence remote leases across restart and delay

* feat(rpc): fence scanner publication rename writes

* fix(scanner): fence observed cleanup deletes

* fix(proto): qualify lease release test types

* fix(scanner): pin movement notifications

* fix(scanner): clean publication imports

* fix(ecstore): satisfy scanner fence clippy

* refactor(scanner): group wait and publication options

* fix(scanner): satisfy final lint and facade guards

* fix(rpc): resolve facade export conflicts

* fix(ci): remove unused decommission and healing facades

* fix(ci): cfg-gate test-only usage overlay import

* fix(scanner): wake on remote scanner restart
This commit is contained in:
cxymds
2026-08-24 14:17:35 +08:00
committed by GitHub
parent 1c5c28842a
commit eec0e0e056
30 changed files with 2926 additions and 230 deletions
+6 -2
View File
@@ -151,7 +151,8 @@ impl ScannerIOCycle for ECStore {
ScannerCycleResult::new(ScannerCycleStatus::Incomplete, None).with_publication_epoch(publication_epoch)
);
}
let activity_status = scanner_cycle_activity_status(self, distributed, &activity_before).await;
let (activity_status, remote_publication_lease_targets) =
scanner_cycle_activity_status(self, distributed, &activity_before).await;
let dirty_usage_status = dirty_usage_snapshot_status(&dirty_usage_snapshot);
let status = classify_nsscanner_cycle(
true,
@@ -187,6 +188,7 @@ impl ScannerIOCycle for ECStore {
};
return Ok(ScannerCycleResult::new(status, dirty_usage_clear)
.with_publication_epoch(publication_epoch)
.with_remote_publication_lease_targets(remote_publication_lease_targets)
.with_remote_dirty_usage_acknowledgements(remote_dirty_usage_acknowledgements));
}
@@ -400,7 +402,8 @@ impl ScannerIOCycle for ECStore {
let budget_elapsed = budget.budget_elapsed();
let dirty_usage_status = dirty_usage_snapshot_status(&dirty_usage_snapshot);
let dirty_usage_current = dirty_usage_status == DirtyUsageSnapshotStatus::Current;
let activity_status = scanner_cycle_activity_status(self, distributed, &activity_before).await;
let (activity_status, remote_publication_lease_targets) =
scanner_cycle_activity_status(self, distributed, &activity_before).await;
let all_bucket_names = all_buckets.iter().map(|bucket| bucket.name.clone()).collect::<Vec<_>>();
let completed_usage = completed_data_usage_info(
&results,
@@ -460,6 +463,7 @@ impl ScannerIOCycle for ECStore {
};
Ok(ScannerCycleResult::new(cycle_status, dirty_usage_clear)
.with_publication_epoch(publication_epoch)
.with_remote_publication_lease_targets(remote_publication_lease_targets)
.with_remote_dirty_usage_acknowledgements(remote_dirty_usage_acknowledgements)
.with_failed_dirty_usage(!failed_buckets.is_empty())
.with_pending_maintenance_work(pending_maintenance_work)
+50 -6
View File
@@ -766,7 +766,7 @@ fn scanner_cycle_status_requires_a_clean_complete_snapshot() {
DirtyUsageSnapshotStatus::Current,
ScannerCycleActivityStatus::Unverified,
),
ScannerCycleStatus::Incomplete
ScannerCycleStatus::Deferred(ScannerCycleDeferReason::ActivityBaselineUnavailable)
);
for status in [
@@ -815,6 +815,34 @@ fn scanner_cycle_status_requires_a_clean_complete_snapshot() {
}
}
#[test]
fn unverified_activity_defers_partial_and_floor_cycles() {
let expected = ScannerCycleStatus::Deferred(ScannerCycleDeferReason::ActivityBaselineUnavailable);
assert_eq!(
classify_nsscanner_cycle(
true,
false,
false,
ScannerBucketScanStatus::Partial,
DirtyUsageSnapshotStatus::Current,
ScannerCycleActivityStatus::Unverified,
),
expected
);
assert_eq!(
classify_nsscanner_cycle(
false,
false,
false,
ScannerBucketScanStatus::Complete,
DirtyUsageSnapshotStatus::Current,
ScannerCycleActivityStatus::Unverified,
),
expected
);
}
#[tokio::test]
async fn structurally_complete_superseded_cycles_publish_without_claiming_convergence() {
let (updates, mut receiver) = mpsc::channel(2);
@@ -834,6 +862,15 @@ async fn structurally_complete_superseded_cycles_publish_without_claiming_conver
.await
.expect("incomplete snapshot suppression should succeed")
);
assert!(
!publish_usage_snapshot(
&updates,
ScannerCycleStatus::Deferred(ScannerCycleDeferReason::ActivityBaselineUnavailable),
DataUsageInfo::default(),
)
.await
.expect("unverified activity suppression should succeed")
);
assert_eq!(
receiver
@@ -855,11 +892,7 @@ async fn structurally_complete_superseded_cycles_publish_without_claiming_conver
#[test]
fn scanner_cycle_fails_closed_for_namespace_disappearance() {
for activity_status in [
ScannerCycleActivityStatus::Changed,
ScannerCycleActivityStatus::Unchanged,
ScannerCycleActivityStatus::Unverified,
] {
for activity_status in [ScannerCycleActivityStatus::Changed, ScannerCycleActivityStatus::Unchanged] {
assert_eq!(
classify_nsscanner_cycle(
false,
@@ -872,6 +905,17 @@ fn scanner_cycle_fails_closed_for_namespace_disappearance() {
ScannerCycleStatus::Incomplete
);
}
assert_eq!(
classify_nsscanner_cycle(
false,
false,
false,
ScannerBucketScanStatus::NamespaceNotFound,
DirtyUsageSnapshotStatus::Changed,
ScannerCycleActivityStatus::Unverified,
),
ScannerCycleStatus::Deferred(ScannerCycleDeferReason::ActivityBaselineUnavailable)
);
assert_eq!(
classify_nsscanner_cycle(
true,