Skip to content

Commit fefce79

Browse files
authored
Merge pull request #10537 from Extra-Chill/fix-artifacts-retention-p0s
fix(observation): hoist retention store, reap artifact-root scratch (#10284, #10285)
2 parents aab757d + 3f87ea1 commit fefce79

8 files changed

Lines changed: 1068 additions & 47 deletions

File tree

crates/homeboy-cli/src/commands/cleanup.rs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ use homeboy::core::defaults;
1111
use homeboy::core::engine;
1212
use homeboy::core::engine::shell::quote_arg;
1313
use homeboy::core::observation::runs_service::{
14-
self, PersistedArtifactCleanupOptions, RunnerDownloadCleanupOptions,
14+
self, OrphanedArtifactBytesCleanupOptions, PersistedArtifactCleanupOptions,
15+
RunnerDownloadCleanupOptions,
1516
};
1617
use homeboy::core::resource_cleanup_intent::ResourceCleanupIntent;
1718
use homeboy::core::worktree::{self, WorktreeCleanupOptions, WorktreeCleanupOutput};
@@ -69,6 +70,7 @@ pub enum CleanupCategoryArg {
6970
WorktreeProviders,
7071
TerminalRuns,
7172
PersistedRunArtifacts,
73+
OrphanedArtifactBytes,
7274
RunnerDownloads,
7375
RemoteLabWorkspaces,
7476
RuntimeTmp,
@@ -639,6 +641,19 @@ const TERMINAL_RUNS_METADATA: CleanupInventoryCategoryMetadata = CleanupInventor
639641
apply_command: "homeboy runs retention --apply",
640642
};
641643

644+
/// Crash residue under the artifact root that no database row can describe.
645+
/// This is the only artifact-root cleanup that is not row-driven, so it is
646+
/// scoped to the two name families a single private constructor owns rather
647+
/// than to "anything without a row" — see
648+
/// `runs_service::orphaned_artifact_bytes` for why a row join is unsafe here.
649+
const ORPHANED_ARTIFACT_BYTES_METADATA: CleanupInventoryCategoryMetadata =
650+
CleanupInventoryCategoryMetadata {
651+
category: "orphaned_artifact_bytes",
652+
include_arg: "orphaned-artifact-bytes",
653+
dry_run_command: "homeboy cleanup --include orphaned-artifact-bytes",
654+
apply_command: "homeboy cleanup --include orphaned-artifact-bytes --apply",
655+
};
656+
642657
pub(crate) const RUNNER_DOWNLOADS_METADATA: CleanupInventoryCategoryMetadata =
643658
CleanupInventoryCategoryMetadata {
644659
category: "runner_downloads",
@@ -795,6 +810,24 @@ fn cleanup_inventory(args: CleanupArgs) -> homeboy::core::Result<Value> {
795810
categories.push(persisted_artifacts_category(persisted, resources, apply)?);
796811
}
797812

813+
if selected.includes(CleanupCategoryArg::OrphanedArtifactBytes) {
814+
let output =
815+
runs_service::cleanup_orphaned_artifact_bytes(OrphanedArtifactBytesCleanupOptions {
816+
apply,
817+
limit: usize::try_from(limit).unwrap_or(usize::MAX),
818+
})?;
819+
categories.push(category_from_output(
820+
ORPHANED_ARTIFACT_BYTES_METADATA,
821+
apply,
822+
output.planned_count,
823+
output.removed_count,
824+
output.skipped_count,
825+
output.planned_size_bytes,
826+
output.removed_size_bytes,
827+
output,
828+
)?);
829+
}
830+
798831
if selected.includes(CleanupCategoryArg::RunnerDownloads) {
799832
let output = runs_service::cleanup_runner_downloads(RunnerDownloadCleanupOptions {
800833
apply,
@@ -2041,6 +2074,13 @@ mod tests {
20412074
"homeboy runs artifact cleanup-persisted",
20422075
"homeboy runs artifact cleanup-persisted --apply",
20432076
),
2077+
(
2078+
ORPHANED_ARTIFACT_BYTES_METADATA,
2079+
"orphaned_artifact_bytes",
2080+
"orphaned-artifact-bytes",
2081+
"homeboy cleanup --include orphaned-artifact-bytes",
2082+
"homeboy cleanup --include orphaned-artifact-bytes --apply",
2083+
),
20442084
(
20452085
RUNNER_DOWNLOADS_METADATA,
20462086
"runner_downloads",

crates/homeboy-core/src/observation/runs_service/mod.rs

Lines changed: 48 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -173,13 +173,15 @@ pub enum ArtifactStorage {
173173

174174
mod artifact_links;
175175
mod artifact_resolve;
176+
mod orphaned_artifact_bytes;
176177
mod persisted_cleanup;
177178
mod run_lookup;
178179
mod runner_downloads;
179180
pub mod runner_evidence;
180181

181182
pub use artifact_links::*;
182183
pub use artifact_resolve::*;
184+
pub use orphaned_artifact_bytes::*;
183185
pub use persisted_cleanup::*;
184186
pub use run_lookup::*;
185187
pub use runner_downloads::*;
@@ -204,24 +206,22 @@ pub fn retain_terminal_runs(
204206
));
205207
}
206208
let finished_before = (Utc::now() - Duration::days(options.older_than_days)).to_rfc3339();
209+
// The store and artifact root are resolved once for the whole sweep. Both
210+
// used to be re-derived inside each per-run plan call, which meant one
211+
// connection open plus a full migration ladder per candidate run.
207212
let mut store = ObservationStore::open_initialized()?;
213+
let artifact_root = crate::artifacts::root()?;
208214
let candidate_run_ids = store.terminal_run_ids_before(&finished_before, options.limit)?;
209215
let mut artifact_cleanup = Vec::new();
210216
let mut lifecycle_directories = Vec::new();
211217
let mut removable_run_ids = Vec::new();
212218
let mut skipped_run_ids = Vec::new();
213219
for run_id in &candidate_run_ids {
214-
let artifacts = cleanup_persisted_artifacts(PersistedArtifactCleanupOptions {
215-
apply: false,
216-
older_than_days: 0,
217-
run_id: Some(run_id.clone()),
218-
kind: None,
219-
artifact_type: None,
220-
run_kind: None,
221-
component_id: None,
222-
limit: 10_000,
223-
terminal_only: true,
224-
})?;
220+
let artifacts = cleanup_persisted_artifacts_with_store(
221+
&store,
222+
&artifact_root,
223+
terminal_run_artifact_plan_options(run_id),
224+
)?;
225225
let lifecycle_directory = terminal_run_lifecycle_directory(&store, run_id)?;
226226
let blocked = artifacts
227227
.rows
@@ -237,21 +237,25 @@ pub fn retain_terminal_runs(
237237
}
238238
}
239239
}
240+
let mut removed_run_count = 0;
240241
if options.apply {
241242
// Revalidate and remove artifact bytes before deleting any provenance.
242243
// A blocked resource leaves the terminal run record and lifecycle root intact.
244+
//
245+
// This second plan pass is *not* redundant with the loop above: that
246+
// loop walks every candidate before any deletion happens, so the
247+
// earliest plans are already stale by the time apply begins. Re-reading
248+
// each run's classification immediately before deleting its bytes is
249+
// the only thing that catches a run whose artifacts became unsafe (or
250+
// whose owning run left a terminal state) during the planning window.
251+
// What was redundant, and is now gone, is reopening the store and
252+
// re-resolving the artifact root on every one of these calls.
243253
for run_id in &removable_run_ids {
244-
let artifacts = cleanup_persisted_artifacts(PersistedArtifactCleanupOptions {
245-
apply: false,
246-
older_than_days: 0,
247-
run_id: Some(run_id.clone()),
248-
kind: None,
249-
artifact_type: None,
250-
run_kind: None,
251-
component_id: None,
252-
limit: 10_000,
253-
terminal_only: true,
254-
})?;
254+
let artifacts = cleanup_persisted_artifacts_with_store(
255+
&store,
256+
&artifact_root,
257+
terminal_run_artifact_plan_options(run_id),
258+
)?;
255259
if artifacts
256260
.rows
257261
.iter()
@@ -262,7 +266,6 @@ pub fn retain_terminal_runs(
262266
}
263267
let records = store.list_artifacts(run_id)?;
264268
let run = store.get_run(run_id)?;
265-
let artifact_root = crate::artifacts::root()?;
266269
for row in artifacts.rows.iter().filter(|row| row.action == "remove") {
267270
let artifact = records
268271
.iter()
@@ -310,24 +313,39 @@ pub fn retain_terminal_runs(
310313
}
311314
}
312315
store.delete_terminal_runs(&deleted)?;
316+
// Count what was actually deleted. Subtracting `skipped_run_ids` from
317+
// `removable_run_ids` under-reported by every run the planning loop had
318+
// already blocked, because those runs are in `skipped_run_ids` but were
319+
// never in `removable_run_ids` to begin with.
320+
removed_run_count = deleted.len();
313321
}
314322
Ok(TerminalRunRetentionOutcome {
315323
dry_run: !options.apply,
316324
older_than_days: options.older_than_days,
317-
removed_run_count: if options.apply {
318-
removable_run_ids
319-
.len()
320-
.saturating_sub(skipped_run_ids.len())
321-
} else {
322-
0
323-
},
325+
removed_run_count,
324326
candidate_run_ids,
325327
artifact_cleanup,
326328
lifecycle_directories,
327329
skipped_run_ids,
328330
})
329331
}
330332

333+
/// Per-run artifact plan used by terminal retention. Always a dry-run plan:
334+
/// retention removes bytes itself and releases the rows with the owning run.
335+
fn terminal_run_artifact_plan_options(run_id: &str) -> PersistedArtifactCleanupOptions {
336+
PersistedArtifactCleanupOptions {
337+
apply: false,
338+
older_than_days: 0,
339+
run_id: Some(run_id.to_string()),
340+
kind: None,
341+
artifact_type: None,
342+
run_kind: None,
343+
component_id: None,
344+
limit: 10_000,
345+
terminal_only: true,
346+
}
347+
}
348+
331349
fn terminal_run_lifecycle_directory(
332350
store: &ObservationStore,
333351
run_id: &str,

0 commit comments

Comments
 (0)