use super::*;
pub(crate) const SPAWN_WATCHDOG_TICK_SECS: u64 = 30;
pub(crate) const DEFAULT_SPAWN_WATCHDOG_SECS: u64 = 600;
pub(crate) const SPAWN_WATCHDOG_ENV: &str = "OXO_FLOW_SPAWN_WATCHDOG_SECS";
pub(crate) const WATCHDOG_REQUEUE_FIRES: u32 = 6;
pub(crate) const WATCHDOG_REQUEUE_FIRES_ENV: &str = "OXO_FLOW_SPAWN_WATCHDOG_REQUEUE_FIRES";
pub(crate) fn watchdog_requeue_fires() -> u32 {
parse_watchdog_requeue_fires(std::env::var(WATCHDOG_REQUEUE_FIRES_ENV).ok().as_deref())
}
pub(crate) fn parse_watchdog_requeue_fires(raw: Option<&str>) -> u32 {
raw.and_then(|v| v.parse::<u32>().ok())
.filter(|&n| n > 0)
.unwrap_or(WATCHDOG_REQUEUE_FIRES)
}
pub(crate) const DISK_PRESSURE_TICK_SECS: u64 = 60;
pub(crate) const DISK_HOLD_ABORT_ROUNDS: u32 = 2;
pub(crate) const DISK_TICK_ENV: &str = "OXO_FLOW_DISK_TICK_SECS";
pub(crate) const DISK_HOLD_ROUNDS_ENV: &str = "OXO_FLOW_DISK_HOLD_ROUNDS";
pub(crate) fn disk_tick_secs() -> u64 {
parse_disk_tick_secs(std::env::var(DISK_TICK_ENV).ok().as_deref())
}
pub(crate) fn parse_disk_tick_secs(raw: Option<&str>) -> u64 {
raw.and_then(|v| v.parse::<u64>().ok())
.filter(|&s| s > 0)
.unwrap_or(DISK_PRESSURE_TICK_SECS)
}
pub(crate) fn disk_hold_abort_rounds() -> u32 {
parse_disk_hold_abort_rounds(std::env::var(DISK_HOLD_ROUNDS_ENV).ok().as_deref())
}
pub(crate) fn parse_disk_hold_abort_rounds(raw: Option<&str>) -> u32 {
raw.and_then(|v| v.parse::<u32>().ok())
.filter(|&s| s > 0)
.unwrap_or(DISK_HOLD_ABORT_ROUNDS)
}
pub(crate) fn spawn_watchdog_threshold() -> std::time::Duration {
parse_spawn_watchdog_threshold(std::env::var(SPAWN_WATCHDOG_ENV).ok().as_deref())
}
pub(crate) fn parse_spawn_watchdog_threshold(raw: Option<&str>) -> std::time::Duration {
raw.and_then(|v| v.parse::<u64>().ok())
.filter(|&s| s > 0)
.map(std::time::Duration::from_secs)
.unwrap_or(std::time::Duration::from_secs(DEFAULT_SPAWN_WATCHDOG_SECS))
}
pub(crate) fn endofrun_prune_decision(
free_mb: Option<u64>,
thresholds: Option<(u64, u64)>,
) -> EndOfRunPruneDecision {
let Some((_, reclaim_mb)) = thresholds else {
return EndOfRunPruneDecision::Sweep;
};
match free_mb {
Some(free) if free >= reclaim_mb => EndOfRunPruneDecision::Skip {
free_mb: free,
reclaim_mb,
},
Some(_) => EndOfRunPruneDecision::Sweep,
None => EndOfRunPruneDecision::KeepOnUnknown,
}
}
#[derive(Debug)]
pub(crate) enum EndOfRunPruneDecision {
Sweep,
Skip { free_mb: u64, reclaim_mb: u64 },
KeepOnUnknown,
}
pub(crate) fn absolutize(path: &Path) -> Result<PathBuf> {
if path.is_absolute() {
Ok(path.to_path_buf())
} else {
Ok(std::env::current_dir()?.join(path))
}
}
pub(crate) fn tmp_dir_resolved(
configured: Option<String>,
workflow_dir: &Path,
) -> Result<Option<PathBuf>> {
if let Ok(raw) = std::env::var("OXO_FLOW_TMPDIR") {
let raw = raw.trim().to_string();
if !raw.is_empty() {
let resolved = absolutize(Path::new(&raw))?;
std::fs::create_dir_all(&resolved)
.with_context(|| format!("creating OXO_FLOW_TMPDIR dir {}", resolved.display()))?;
return Ok(Some(resolved));
}
tracing::warn!("OXO_FLOW_TMPDIR is empty, ignoring it in favor of [engine] tmp_dir");
}
let Some(raw) = configured else {
return Ok(None);
};
let trimmed = raw.trim();
if trimmed.is_empty() {
tracing::warn!("[engine] tmp_dir is empty, ignoring it");
return Ok(None);
}
let raw_path = Path::new(trimmed);
let anchored = if raw_path.is_absolute() {
raw_path.to_path_buf()
} else {
workflow_dir.join(raw_path)
};
std::fs::create_dir_all(&anchored)
.with_context(|| format!("creating [engine] tmp_dir {}", anchored.display()))?;
Ok(Some(anchored))
}
pub(crate) fn normalize_dots(path: PathBuf) -> PathBuf {
let mut out = PathBuf::with_capacity(path.as_os_str().len());
for comp in path.components() {
match comp {
std::path::Component::CurDir => {}
std::path::Component::ParentDir => {
out.push(comp);
}
other => out.push(other),
}
}
if out.as_os_str().is_empty() {
PathBuf::from(".")
} else {
out
}
}
#[cfg(test)]
mod tmp_dir_tests {
use super::tmp_dir_resolved;
#[test]
fn unset_everywhere_is_none() {
if std::env::var("OXO_FLOW_TMPDIR").is_ok() {
return;
}
let resolved = tmp_dir_resolved(None, std::path::Path::new("/wf")).unwrap();
assert!(resolved.is_none(), "no config + no env = None");
}
#[test]
fn relative_config_anchors_to_workflow_dir() {
let dir = std::env::temp_dir().join(format!("oxo-tmpdircfg-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let workflow_dir = dir.join("wf");
std::fs::create_dir_all(&workflow_dir).unwrap();
let resolved = tmp_dir_resolved(Some("scratch".to_string()), &workflow_dir).unwrap();
assert_eq!(
resolved.as_deref(),
Some(workflow_dir.join("scratch").as_path()),
"relative tmp_dir must anchor to the workflow directory"
);
assert!(workflow_dir.join("scratch").is_dir());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn empty_config_value_is_ignored() {
if std::env::var("OXO_FLOW_TMPDIR").is_ok() {
return;
}
let resolved =
tmp_dir_resolved(Some(" ".to_string()), std::path::Path::new("/wf")).unwrap();
assert!(resolved.is_none(), "empty/whitespace tmp_dir = None");
}
}