// The per-run spill directory: where it goes (`--spill-dir` or TMPDIR), what
// counts as one of its run files, and how it is removed again. Claiming and
// reclaiming ownership of a directory lives in spill_owner*.mbt; this file is
// only the path and the purge.
///|
/// One query's spill directory, and what removing it means.
///
/// `base` is the directory the caller named (`--spill-dir`) or TMPDIR;
/// `created_base` records that this process made `base`, so an empty base can
/// be removed again while a caller's own directory is left alone.
priv struct SpillRun {
dir : String
base : String
created_base : Bool
}
///|
/// The spill directory for one query: a subdirectory of `base`, named
/// `moonlake-spill-`.
///
/// The subdirectory is not decoration:
///
/// * two processes sharing one `--spill-dir` used to start at `run0.bin` in
/// the same directory, overwrite each other, and then each delete the
/// other's run files when it finished; a per-process subdirectory makes that
/// collision impossible without a lock;
/// * the purge then only touches a directory this process created, so a
/// caller's own files in `--spill-dir` are never read or deleted;
/// * `create_dir` is the lock: the `path_exists` check is an optimisation, and
/// a racing process loses the create and moves to the next index.
///
/// A directory left behind by a killed process is reclaimed here, but only when
/// it says who owned it and that owner is provably gone: the marker written by
/// `claim_owner` records a pid, a boot id and the pid's start time, and
/// `judge_owner` accepts exactly three proofs of death (another boot, a pid that
/// no longer exists, a pid whose start time changed). A directory with no
/// marker, an unknown marker, or a live owner is left alone — one of those is a
/// running query's run files.
fn spill_run_dir(spill_dir : String?) -> SpillRun raise @types.SqlError {
let base = match spill_dir {
Some(d) => d
None =>
match @env.get_env_var("TMPDIR") {
Some(d) => d
None => "/tmp"
}
}
let created_base = !@fs.path_exists(base)
if created_base {
@fs.create_dir(base) catch {
@fs.IOError(msg) => raise @types.SqlError::Execute("spill dir: \{msg}")
}
}
let mut n = 0
let mut noted = 0
while n < SPILL_RUN_LIMIT {
let dir = "\{base}/moonlake-spill-\{n}"
if @fs.path_exists(dir) {
match inspect_owner(dir) {
Stale(why) =>
// The directory is free after this; go around again rather than burn
// the index, so the numbering stays dense for the next run.
if reclaim_owner(dir) {
if noted < SPILL_NOTE_LIMIT {
noted += 1
eprintln("moonlake: reclaimed \{dir}: \{why}")
}
} else {
n += 1
}
Live => n += 1
Unreadable(why) => {
if noted < SPILL_NOTE_LIMIT {
noted += 1
eprintln("moonlake: warning: kept spill directory \{dir}: \{why}")
}
n += 1
}
Unavailable => n += 1
}
continue
}
@fs.create_dir(dir) catch {
@fs.IOError(_) => {
n += 1
continue
}
}
claim_owner(dir)
return { dir, base, created_base, }
}
raise @types.SqlError::Execute("no free spill directory under \{base}")
}
///|
/// Remove one spill directory: the run files `@spill` names, then the
/// directory. A directory that survives is reported rather than silently
/// ignored — ignoring it is how a leaked temp tree turns into a disk-full
/// failure mid-query.
fn purge_dir(dir : String) -> Unit {
let entries = @fs.read_dir(dir) catch {
@fs.IOError(_) => return // already gone: nothing to purge
}
for name in entries {
if @spill.is_run_file_name(name) || is_owner_file(name) {
@fs.remove_file("\{dir}/\{name}") catch {
@fs.IOError(_) => ()
}
}
}
@fs.remove_dir(dir) catch {
@fs.IOError(msg) =>
eprintln("moonlake: warning: kept spill directory \{dir}: \{msg}")
}
}
///|
/// Remove this query's spill directory, and an empty base this process made.
fn purge_spill_run(run : SpillRun) -> Unit {
purge_dir(run.dir)
if run.created_base {
@fs.remove_dir(run.base) catch {
@fs.IOError(_) => () // not empty: something else lives there now
}
}
}