Skip to content

Commit 0dd1fd7

Browse files
fix(doctor): isolate registry diff conflicts (#440)
1 parent 974d423 commit 0dd1fd7

2 files changed

Lines changed: 105 additions & 14 deletions

File tree

src/doctor/tests.rs

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,14 @@ async fn orphan_reporting_uses_complete_registry_rows_not_token_accounting() {
4141
.unwrap();
4242
let profile_root = dir.path().join("profile");
4343
let eligible_root = dir.path().join("eligible-repo");
44+
let conflicting_root = dir.path().join("conflicting-repo");
45+
let conflicting_registered_root = dir.path().join("registered-elsewhere");
4446
let blocked_root = dir.path().join("blocked-repo");
4547
std::fs::create_dir_all(&eligible_root).unwrap();
48+
std::fs::create_dir_all(&conflicting_root).unwrap();
49+
std::fs::create_dir_all(&conflicting_registered_root).unwrap();
4650
std::fs::create_dir_all(&blocked_root).unwrap();
47-
for root in [&eligible_root, &blocked_root] {
51+
for root in [&eligible_root, &conflicting_root, &blocked_root] {
4852
let status = std::process::Command::new("git")
4953
.args(["init", "--quiet"])
5054
.current_dir(root)
@@ -61,8 +65,18 @@ async fn orphan_reporting_uses_complete_registry_rows_not_token_accounting() {
6165
)
6266
.unwrap();
6367
write_repository_identity_marker(&eligible_root, "proj_eligible").unwrap();
68+
write_enrollment_marker(
69+
&conflicting_root,
70+
&EnrollmentMarker {
71+
project_id: "proj_conflict".to_string(),
72+
storage_mode: StorageMode::ProfileSharded,
73+
},
74+
)
75+
.unwrap();
76+
write_repository_identity_marker(&conflicting_root, "proj_conflict").unwrap();
6477
for (project_id, project_root) in [
6578
("proj_eligible", &eligible_root),
79+
("proj_conflict", &conflicting_root),
6680
("proj_blocked", &blocked_root),
6781
] {
6882
let data_root = profile_root.join("projects").join(project_id);
@@ -88,9 +102,24 @@ async fn orphan_reporting_uses_complete_registry_rows_not_token_accounting() {
88102
let db = crate::global_db::GlobalDb::open_at(&dir.path().join("global.db"))
89103
.await
90104
.unwrap();
105+
db.upsert_code_project(
106+
"proj_conflict",
107+
&conflicting_registered_root,
108+
None,
109+
None,
110+
Some("main"),
111+
)
112+
.await
113+
.unwrap();
91114
let (count, warnings) = orphan_store_manifest_report(&db, &profile_root).await;
92115

93116
assert_eq!(count, 1, "{warnings:?}");
117+
assert!(
118+
warnings
119+
.iter()
120+
.any(|warning| warning.contains("proj_conflict")),
121+
"{warnings:?}"
122+
);
94123

95124
let scan = crate::migrate::registry::scan_profile_store_manifests(&profile_root, 1_800_000_000);
96125
let eligible = crate::migrate::registry::RegistryReconstructionReport {
@@ -99,10 +128,42 @@ async fn orphan_reporting_uses_complete_registry_rows_not_token_accounting() {
99128
.into_iter()
100129
.filter(|plan| {
101130
plan.status == crate::migrate::registry::RegistryReconstructionStatus::Eligible
131+
&& plan.project.project_id == "proj_eligible"
102132
})
103133
.collect(),
104134
issues: Vec::new(),
105135
};
136+
let mut batch_left = eligible.plans[0].clone();
137+
batch_left.project.aliases = vec![dir.path().join("shared-alias")];
138+
let mut batch_right = batch_left.clone();
139+
batch_right.project.project_id = "proj_batch_other".to_string();
140+
batch_right.project.project_root = conflicting_root.clone();
141+
batch_right.store.project_id = batch_right.project.project_id.clone();
142+
batch_right.store.store_id = "store:proj_batch_other:profile_sharded".to_string();
143+
batch_right.store.store_relpath = "projects/proj_batch_other".to_string();
144+
batch_right.store.manifest_relpath =
145+
Some("projects/proj_batch_other/store_manifest.json".to_string());
146+
batch_right.graph_scopes.clear();
147+
batch_right.artifacts.clear();
148+
batch_left.graph_scopes.clear();
149+
batch_left.artifacts.clear();
150+
let batch_diff = crate::migrate::registry::diff_registry_reconstruction_report(
151+
&db,
152+
&crate::migrate::registry::RegistryReconstructionReport {
153+
plans: vec![batch_left, batch_right],
154+
issues: Vec::new(),
155+
},
156+
)
157+
.await;
158+
assert_eq!(batch_diff.missing_plans, 0);
159+
assert!(
160+
batch_diff
161+
.issues
162+
.iter()
163+
.any(|issue| issue.contains("shared-alias")),
164+
"{:?}",
165+
batch_diff.issues
166+
);
106167
let applied = crate::migrate::registry::apply_registry_reconstruction_report(&db, &eligible)
107168
.await
108169
.unwrap();

src/migrate/registry.rs

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -79,24 +79,54 @@ pub async fn diff_registry_reconstruction_report(
7979
db: &GlobalDb,
8080
report: &RegistryReconstructionReport,
8181
) -> RegistryReconstructionDiffReport {
82-
let eligible = RegistryReconstructionReport {
83-
plans: report
84-
.plans
85-
.iter()
86-
.filter(|plan| plan.status == RegistryReconstructionStatus::Eligible)
87-
.cloned()
88-
.collect(),
89-
issues: Vec::new(),
90-
};
9182
let mut diff = RegistryReconstructionDiffReport {
92-
issues: preflight_registry_reconstruction(db.conn(), &eligible).await,
83+
issues: report.issues.clone(),
9384
..RegistryReconstructionDiffReport::default()
9485
};
95-
if !diff.issues.is_empty() {
96-
return diff;
86+
let mut eligible = Vec::new();
87+
88+
for plan in report
89+
.plans
90+
.iter()
91+
.filter(|plan| plan.status == RegistryReconstructionStatus::Eligible)
92+
{
93+
let single = RegistryReconstructionReport {
94+
plans: vec![plan.clone()],
95+
issues: Vec::new(),
96+
};
97+
let issues = preflight_registry_reconstruction(db.conn(), &single).await;
98+
if !issues.is_empty() {
99+
diff.issues.extend(issues);
100+
continue;
101+
}
102+
eligible.push(plan);
97103
}
98104

99-
for plan in &eligible.plans {
105+
let mut conflicts = vec![false; eligible.len()];
106+
for left in 0..eligible.len() {
107+
for right in (left + 1)..eligible.len() {
108+
let pair = RegistryReconstructionReport {
109+
plans: vec![eligible[left].clone(), eligible[right].clone()],
110+
issues: Vec::new(),
111+
};
112+
let issues = preflight_registry_reconstruction(db.conn(), &pair).await;
113+
if issues.is_empty() {
114+
continue;
115+
}
116+
conflicts[left] = true;
117+
conflicts[right] = true;
118+
for issue in issues {
119+
if !diff.issues.contains(&issue) {
120+
diff.issues.push(issue);
121+
}
122+
}
123+
}
124+
}
125+
126+
for (index, plan) in eligible.into_iter().enumerate() {
127+
if conflicts[index] {
128+
continue;
129+
}
100130
match registry_plan_has_missing_rows(db.conn(), plan).await {
101131
Ok(true) => diff.missing_plans += 1,
102132
Ok(false) => {}

0 commit comments

Comments
 (0)