fix(openapi): harden story 2.1 production lifecycle
This commit is contained in:
@@ -4,7 +4,7 @@ use std::{
|
||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use crate::temp_scan::{list_names, valid_temp_name};
|
||||
use crate::temp_scan::{list_names_after, valid_temp_name};
|
||||
use crate::{
|
||||
ArtifactError, ArtifactStore, ReconciliationMutation, ReconciliationNamespace,
|
||||
ReconciliationScan, ReconciliationScanStop,
|
||||
@@ -94,10 +94,33 @@ pub struct StaleTemp {
|
||||
grace: Duration,
|
||||
}
|
||||
|
||||
/// Opaque, inode-bound progress marker for the bounded stale-temp sweeper.
|
||||
///
|
||||
/// It deliberately advances by shard instead of retaining a directory offset:
|
||||
/// directory offsets are invalidated by a concurrent writer, while round-robin
|
||||
/// shard progress prevents a busy low-numbered shard from starving all others.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TempScanCursor {
|
||||
root_dev: u64,
|
||||
root_ino: u64,
|
||||
next_shard: u8,
|
||||
shard_continuation: Option<TempShardContinuation>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct TempShardContinuation {
|
||||
shard: u8,
|
||||
dev: u64,
|
||||
ino: u64,
|
||||
cookie: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub struct TempScan {
|
||||
pub scanned: usize,
|
||||
pub omitted: usize,
|
||||
/// True when this page completed a full round-robin traversal.
|
||||
pub complete: bool,
|
||||
}
|
||||
|
||||
impl ArtifactStore {
|
||||
@@ -324,27 +347,93 @@ impl ArtifactStore {
|
||||
scan_budget: usize,
|
||||
result_limit: usize,
|
||||
) -> Result<(TempScan, Vec<StaleTemp>), ArtifactError> {
|
||||
self.scan_stale_temps_after(grace, None, scan_budget, result_limit)
|
||||
.map(|(report, candidates, _)| (report, candidates))
|
||||
}
|
||||
|
||||
/// Resumes stale-temp cleanup from an opaque round-robin marker. A
|
||||
/// continuation is valid only for this exact pinned root.
|
||||
pub fn scan_stale_temps_after(
|
||||
&self,
|
||||
grace: Duration,
|
||||
continuation: Option<TempScanCursor>,
|
||||
scan_budget: usize,
|
||||
result_limit: usize,
|
||||
) -> Result<(TempScan, Vec<StaleTemp>, TempScanCursor), ArtifactError> {
|
||||
let root = self.root()?;
|
||||
let _lock = RootLock::shared(root)?;
|
||||
ensure_root_unchanged(root)?;
|
||||
let sha = match open_existing_dir(root.fd.as_raw_fd(), b"sha256") {
|
||||
Ok(fd) => fd,
|
||||
Err(ArtifactError::NotFound) => return Ok((TempScan::default(), Vec::new())),
|
||||
Err(ArtifactError::NotFound) => {
|
||||
return Ok((
|
||||
TempScan {
|
||||
complete: true,
|
||||
..TempScan::default()
|
||||
},
|
||||
Vec::new(),
|
||||
TempScanCursor {
|
||||
root_dev: root.dev,
|
||||
root_ino: root.ino,
|
||||
next_shard: 0,
|
||||
shard_continuation: None,
|
||||
},
|
||||
));
|
||||
}
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let (start_shard, mut shard_continuation) = match continuation {
|
||||
Some(cursor) if cursor.root_dev == root.dev && cursor.root_ino == root.ino => {
|
||||
(cursor.next_shard, cursor.shard_continuation)
|
||||
}
|
||||
Some(_) => return Err(ArtifactError::UnsafeRoot),
|
||||
None => (0, None),
|
||||
};
|
||||
let mut report = TempScan::default();
|
||||
let mut remaining = scan_budget;
|
||||
let mut result = Vec::new();
|
||||
for shard in (0_u8..=255).map(|value| format!("{value:02x}")) {
|
||||
if remaining == 0 {
|
||||
return Ok((
|
||||
report,
|
||||
result,
|
||||
TempScanCursor {
|
||||
root_dev: root.dev,
|
||||
root_ino: root.ino,
|
||||
next_shard: start_shard,
|
||||
shard_continuation,
|
||||
},
|
||||
));
|
||||
}
|
||||
let mut next_shard = start_shard;
|
||||
for offset in 0_u16..=255 {
|
||||
if remaining == 0 {
|
||||
break;
|
||||
}
|
||||
let shard_number = start_shard.wrapping_add(offset as u8);
|
||||
next_shard = shard_number.wrapping_add(1);
|
||||
let shard = format!("{shard_number:02x}");
|
||||
let shard_fd = match open_existing_dir(sha.as_raw_fd(), shard.as_bytes()) {
|
||||
Ok(fd) => fd,
|
||||
Err(ArtifactError::NotFound) => continue,
|
||||
Err(ArtifactError::NotFound) => {
|
||||
shard_continuation = None;
|
||||
continue;
|
||||
}
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
for name in list_names(shard_fd.as_raw_fd(), &mut remaining, &mut report)? {
|
||||
let stat = stat_fd(shard_fd.as_raw_fd())?;
|
||||
let resume = shard_continuation.take().and_then(|continuation| {
|
||||
if continuation.shard == shard_number
|
||||
&& continuation.dev == stat.st_dev
|
||||
&& continuation.ino == stat.st_ino
|
||||
{
|
||||
Some(continuation.cookie)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
let page = list_names_after(shard_fd.as_raw_fd(), &mut remaining, &mut report, resume)?;
|
||||
for entry in page.names {
|
||||
let name = entry.name;
|
||||
if !valid_temp_name(&name) {
|
||||
continue;
|
||||
}
|
||||
@@ -370,6 +459,23 @@ impl ArtifactStore {
|
||||
{
|
||||
if result.len() == result_limit {
|
||||
report.omitted += 1;
|
||||
if result_limit > 0 {
|
||||
return Ok((
|
||||
report,
|
||||
result,
|
||||
TempScanCursor {
|
||||
root_dev: root.dev,
|
||||
root_ino: root.ino,
|
||||
next_shard: shard_number,
|
||||
shard_continuation: Some(TempShardContinuation {
|
||||
shard: shard_number,
|
||||
dev: stat.st_dev,
|
||||
ino: stat.st_ino,
|
||||
cookie: entry.cookie_before,
|
||||
}),
|
||||
},
|
||||
));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
result.push(StaleTemp {
|
||||
@@ -385,8 +491,35 @@ impl ArtifactStore {
|
||||
});
|
||||
}
|
||||
}
|
||||
if let Some(cookie) = page.continuation {
|
||||
return Ok((
|
||||
report,
|
||||
result,
|
||||
TempScanCursor {
|
||||
root_dev: root.dev,
|
||||
root_ino: root.ino,
|
||||
next_shard: shard_number,
|
||||
shard_continuation: Some(TempShardContinuation {
|
||||
shard: shard_number,
|
||||
dev: stat.st_dev,
|
||||
ino: stat.st_ino,
|
||||
cookie,
|
||||
}),
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok((report, result))
|
||||
report.complete = true;
|
||||
Ok((
|
||||
report,
|
||||
result,
|
||||
TempScanCursor {
|
||||
root_dev: root.dev,
|
||||
root_ino: root.ino,
|
||||
next_shard,
|
||||
shard_continuation: None,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
/// Deletes a revalidated stale inode under an exclusive lock, then fsyncs.
|
||||
|
||||
Reference in New Issue
Block a user