Skip to main content

git_policy_cli/landing/
work_item.rs

1//! What:
2//!  The durable work-item record: a small file in the Git directory that says "this landed object needs this
3//!  asynchronous work", written by the wrapper and drained by a separate resident consumer.
4//! Why:
5//!  A wrapper process exits when its command ends, and it may start no program, so work that must not delay a landing
6//!  (the standards review, later auto-push and maintenance) is left as a record instead. The record is generic: a kind,
7//!  the landed object and an id derived from both, so a retry cannot duplicate it. The design, its failure cases and
8//!  the consumer contract are in `doc/planning/cli-git-standards-review-port.md`. Nothing calls this yet.
9//!
10//! In TS you'd write (pseudocode):
11//! ```ts
12//! await enqueue(workDir, { kind: 'review', oid, ... }); const item = await claimNext(workDir); await finish(workDir, item);
13//! ```
14
15use crate::claim_time::ClaimTime;
16use crate::private_storage::{create_private_directory_all, write_private_file_unsynced};
17use crate::random_id::random_uuid;
18use crate::service_timing::log;
19pub use crate::work_item_recovery::{Recovered, next_claim_expiry, requeue_expired, requeue_expired_reporting};
20use serde_json::{Map, Value};
21use std::path::{Path, PathBuf};
22use std::time::{Duration, SystemTime};
23
24/// The record format version.
25pub const VERSION: u64 = 1;
26
27/// Directory under the worktree's own Git directory (`WorkItem::git_dir`) that holds the state directories.
28pub const WORK_DIRECTORY: &str = "cli-git/work";
29
30/// The state directories, in the order an item moves through them.
31pub const PENDING: &str = "pending";
32/// Held by one consumer, with a lease.
33pub const CLAIMED: &str = "claimed";
34/// Held by one instance that is returning an expired claim to `pending`; see `requeue_with`.
35pub const RECOVERING: &str = "recovering";
36/// Finished.
37pub const DONE: &str = "done";
38/// Given up after too many tries.
39pub const FAILED: &str = "failed";
40
41/// What:
42///  The kind of work an item asks for.
43/// Why:
44///   One record format carries every asynchronous job; the kind picks the consumer's handler.
45///
46/// In TS you'd write (pseudocode):
47/// ```ts
48/// type WorkKind = 'review' | 'push' | 'maintenance';
49/// ```
50#[derive(Clone, Copy, Debug, Eq, PartialEq)]
51pub enum WorkKind {
52    /// The standards-review shadow run.
53    Review,
54    /// An auto-push of the branch.
55    Push,
56    /// Repository maintenance.
57    Maintenance,
58    /// Emptying a directory of left-out files that a snapshot-made worktree set aside.
59    Trash,
60}
61
62/// Methods over a kind.
63impl WorkKind {
64    /// The kind's name, which is also the start of an item's id.
65    pub fn name(self) -> &'static str {
66        return match self {
67            WorkKind::Review => "review",
68            WorkKind::Push => "push",
69            WorkKind::Maintenance => "maintenance",
70            WorkKind::Trash => "trash",
71        };
72    }
73
74    /// The kind a name spells.
75    pub fn from_name(name: &str) -> Option<WorkKind> {
76        return [WorkKind::Review, WorkKind::Push, WorkKind::Maintenance, WorkKind::Trash].into_iter().find(|kind| return kind.name() == name);
77    }
78}
79
80/// What:
81///  One work item: what to do, to which landed object, and where.
82/// Why:
83///   The consumer needs only the object name and the places to find the repository; it reads the patch itself.
84///
85/// In TS you'd write (pseudocode):
86/// ```ts
87/// type WorkItem = { kind; repository: string; gitDir: string; ref: string; oid: string; parent: string; created: string; tries: number };
88/// ```
89#[derive(Clone, Debug, Eq, PartialEq)]
90pub struct WorkItem {
91    /// What to do.
92    pub kind: WorkKind,
93    /// The worktree root.
94    pub repository: String,
95    /// The Git directory of the worktree that made the commit and whose service instance runs the item: the common directory
96    ///  for the main worktree, `<common>/worktrees/<name>` for a linked one. The work records are under it, and
97    ///  `is_acceptable` refuses an item whose value is not the directory it is read from.
98    pub git_dir: String,
99    /// The branch reference that received the commit.
100    pub branch_ref: String,
101    /// The landed commit.
102    pub oid: String,
103    /// Its first parent, or empty for a root commit.
104    pub parent: String,
105    /// `toISOString` text of the enqueue time.
106    pub created: String,
107    /// Failed or abandoned attempts so far.
108    pub tries: u32,
109}
110
111/// Methods over an item.
112impl WorkItem {
113    /// The id and file name: the kind and the landed object, so the same landing always yields the same id.
114    pub fn id(&self) -> String {
115        return format!("{}-{}", self.kind.name(), self.oid);
116    }
117
118    /// The record's one JSON line.
119    pub fn encode(&self) -> String {
120        let mut fields: Map<String, Value> = Map::new();
121        fields.insert(String::from("v"), Value::from(VERSION));
122        fields.insert(String::from("tries"), Value::from(self.tries));
123        for (name, text) in [
124            ("kind", self.kind.name()),
125            ("id", self.id().as_str()),
126            ("repository", self.repository.as_str()),
127            ("gitDir", self.git_dir.as_str()),
128            ("ref", self.branch_ref.as_str()),
129            ("oid", self.oid.as_str()),
130            ("parent", self.parent.as_str()),
131            ("created", self.created.as_str()),
132        ] {
133            fields.insert(String::from(name), Value::from(text));
134        }
135        return format!("{}\n", Value::Object(fields));
136    }
137
138    /// Read a record; any other version, an unknown kind, a missing field or an id that is not derived from the kind and
139    /// object is refused with a message.
140    pub fn decode(text: &str) -> Result<WorkItem, String> {
141        let value: Value = serde_json::from_str(text).map_err(|error| return format!("the record is not JSON: {error}"))?;
142        let field = |name: &str| -> Result<String, String> {
143            return value.get(name).and_then(Value::as_str).map(String::from).ok_or_else(|| return format!("the record has no text field {name}"));
144        };
145        if value.get("v").and_then(Value::as_u64) != Some(VERSION) {
146            return Err(String::from("the record has an unsupported version"));
147        }
148        let kind: WorkKind = WorkKind::from_name(field("kind")?.as_str()).ok_or_else(|| return String::from("the record has an unknown kind"))?;
149        let tries: u32 = value.get("tries").and_then(Value::as_u64).and_then(|count| return u32::try_from(count).ok()).ok_or_else(|| return String::from("the record has no tries"))?;
150        let item: WorkItem = WorkItem {
151            kind,
152            repository: field("repository")?,
153            git_dir: field("gitDir")?,
154            branch_ref: field("ref")?,
155            oid: field("oid")?,
156            parent: field("parent")?,
157            created: field("created")?,
158            tries,
159        };
160        if field("id")? != item.id() {
161            return Err(String::from("the record's id is not derived from its kind and object"));
162        }
163        return Ok(item);
164    }
165}
166
167/// What:
168///  Whether an enqueue added the item or found it already known.
169/// Why:
170///   A landing that is replayed, or a daemon that restarts, enqueues again; the second time must change nothing.
171///
172/// In TS you'd write (pseudocode):
173/// ```ts
174/// type Enqueued = 'added' | 'known';
175/// ```
176#[derive(Clone, Copy, Debug, Eq, PartialEq)]
177pub enum Enqueued {
178    /// A new pending item was written.
179    Added,
180    /// The id already exists in some state directory.
181    Known,
182}
183
184/// The directory of one state.
185pub fn state_directory(work_dir: &Path, state: &str) -> PathBuf {
186    return work_dir.join(state);
187}
188
189/// What:
190///  Write the item as a pending record, whole or absent, unless its id is already in any state.
191///  The producer never waits for a consumer and never reads anything back.
192/// Why:
193///   The id is the idempotency key; the exclusive publish makes two racing producers agree on one winner.
194///
195/// In TS you'd write (pseudocode):
196/// ```ts
197/// await writeExclusive(join(workDir, 'pending', item.id), item.encode())
198/// ```
199pub fn enqueue(work_dir: &Path, item: &WorkItem) -> Result<Enqueued, String> {
200    for state in [PENDING, CLAIMED, DONE, FAILED] {
201        create_private_directory_all(state_directory(work_dir, state).as_path()).map_err(|error| return format!("creating the {state} directory failed: {error}"))?;
202    }
203    let id: String = item.id();
204    // The states are looked at in the order an item moves through them, so an item that moves on while they are looked at is
205    //  found in the next one: looking at `done` first could miss an item that settled after that look and left `claimed`
206    //  before the look at it.
207    if [CLAIMED, RECOVERING].iter().any(|state| { return entries_oldest_first(work_dir, state).iter().any(|(name, _)| { return id_of_entry(name.as_str()) == id; }); }) {
208        return Ok(Enqueued::Known);
209    }
210    if state_directory(work_dir, DONE).join(id.as_str()).exists() || state_directory(work_dir, FAILED).join(id.as_str()).exists() {
211        return Ok(Enqueued::Known);
212    }
213    return match write_private_file_unsynced(state_directory(work_dir, PENDING).join(id.as_str()).as_path(), item.encode().as_bytes()) {
214        Ok(()) => Ok(Enqueued::Added),
215        Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => Ok(Enqueued::Known),
216        Err(error) => Err(format!("writing the work item {id} failed: {error}")),
217    };
218}
219
220/// What:
221///  A held item: the decoded record, its id, and the name of the file that holds it.
222/// Why:
223///   Finishing needs the claim's file name, not the id: the name carries the generation of the claim, so a settlement or a
224///  renewal reaches only the claim it was made for. The consumer needs the item.
225///
226/// In TS you'd write (pseudocode):
227/// ```ts
228/// type Claimed = { id: string; name: string; item: WorkItem };
229/// ```
230#[derive(Clone, Debug, Eq, PartialEq)]
231pub struct Claimed {
232    /// File name in every state directory but `claimed` and `recovering`, where the file name is `name`.
233    pub id: String,
234    /// File name in `claimed` as of the claim or the last renewal: the id, the claim's generation and the clock reading.
235    pub name: String,
236    /// The decoded record.
237    pub item: WorkItem,
238}
239
240/// Separates the parts of a claim's file name; an id is a kind and a hexadecimal object name and holds none.
241const NAME_SEPARATOR: char = '~';
242
243/// The most characters an object name has: 64 hexadecimal digits of a SHA-256 name. With the longest kind, a generation, a reading that
244/// fills every field and the suffix of a recovery's temporary file, such an id still makes a name of at most 223 bytes, under
245/// the 255 a file name may have; a longer id could make a claim's name fit and the name of its temporary file not.
246pub(crate) const MAX_OBJECT_NAME_LENGTH: usize = 64;
247
248/// Whether `object_name` is what follows the kind in an id: 1 to `MAX_OBJECT_NAME_LENGTH` ASCII letters and digits, so it
249/// holds no separator of a claim's name and no character a file name treats specially.
250fn is_object_name(object_name: &str) -> bool {
251    return !object_name.is_empty() && object_name.len() <= MAX_OBJECT_NAME_LENGTH && object_name.bytes().all(|byte| { return byte.is_ascii_alphanumeric(); });
252}
253
254/// What:
255///  Set the modification time of the file at `path` to now.
256/// Why:
257///   A release that cannot read the reading in a claim's name ages the claim by the file's modification time, which a rename
258///  keeps. Claims and renewals are renames, so without this such a reader sees the time the item was queued, not the last
259///  renewal,
260/// and takes a live holder's claim.
261/// A failure is returned,
262/// because a claim or renewal that went on without the stamp
263///  would be one that such a reader takes (a file another user owns cannot be stamped).
264///
265/// In TS you'd write (pseudocode):
266/// ```ts
267/// await utimes(path, new Date(), new Date())
268/// ```
269fn stamp_modification_time(path: &Path) -> std::io::Result<()> {
270    return std::fs::File::open(path)?.set_modified(SystemTime::now());
271}
272
273/// How a claim or renewal stamps the file it holds.
274type Stamp<'a> = &'a dyn Fn(&Path) -> std::io::Result<()>;
275
276/// Whether `id` is a kind and an object name,
277/// which makes every name built from it fit in a file name.
278pub(crate) fn is_valid_id(id: &str) -> bool {
279    return [WorkKind::Review, WorkKind::Push, WorkKind::Maintenance, WorkKind::Trash]
280        .iter()
281        .any(|kind| { return id.strip_prefix(format!("{}-", kind.name()).as_str()).is_some_and(is_object_name); });
282}
283
284/// What:
285///  The id an entry of `claimed` or `recovering` is the claim of: the part of its name before the generation.
286/// Why:
287///   A claim by an earlier release is named by the id alone, so a name with no separator is its own id.
288///
289/// In TS you'd write (pseudocode):
290/// ```ts
291/// name.split('~')[0]
292/// ```
293pub fn id_of_entry(name: &str) -> &str {
294    return name.split_once(NAME_SEPARATOR).map_or(name, |(id, _)| { return id; });
295}
296
297/// What:
298///  The name of the file that holds the claim of `id` of the generation `token` as of the reading `stamp`.
299/// Why:
300///   The generation is unique to one acquisition and the reading says when the holder last proved it was alive, so the name
301///  alone tells every other instance whose claim this is and whether it ran out, and a rename of exactly that name is the only
302///  way to take it: a holder that renews changes the name, so a take made from an older look finds nothing to take.
303///
304/// In TS you'd write (pseudocode):
305/// ```ts
306/// `${id}~${token}~${stamp.encode()}`
307/// ```
308pub fn claim_file_name(id: &str, token: &str, stamp: &ClaimTime) -> String {
309    return format!("{id}{NAME_SEPARATOR}{token}{NAME_SEPARATOR}{}", stamp.encode());
310}
311
312/// The generation and the reading in a claim's file name, or nothing for a name by an earlier release.
313pub(crate) fn parse_claim_name(name: &str) -> Option<(&str, ClaimTime)> {
314    let (_, rest) = name.split_once(NAME_SEPARATOR)?;
315    let (token, stamp) = rest.split_once(NAME_SEPARATOR)?;
316    if token.is_empty() || !token.bytes().all(|byte| { return byte.is_ascii_hexdigit(); }) {
317        return None;
318    }
319    return Some((token, ClaimTime::decode(stamp)?));
320}
321
322/// A fresh generation: 32 random hexadecimal digits.
323pub(crate) fn new_generation() -> Result<String, String> {
324    return random_uuid()
325        .map(|uuid| { return uuid.replace('-', ""); })
326        .map_err(|error| { return format!("making the generation of a claim failed: {}", error.0); });
327}
328
329/// The names in a state directory with their modification times, oldest first.
330pub(crate) fn entries_oldest_first(work_dir: &Path, state: &str) -> Vec<(String, SystemTime)> {
331    let mut found: Vec<(String, SystemTime)> = Vec::new();
332    if let Ok(listing) = std::fs::read_dir(state_directory(work_dir, state)) {
333        for entry in listing.flatten() {
334            let modified: SystemTime = entry.metadata().and_then(|meta| return meta.modified()).unwrap_or(SystemTime::UNIX_EPOCH);
335            found.push((entry.file_name().to_string_lossy().into_owned(), modified));
336        }
337    }
338    found.sort_by(|left, right| return left.1.cmp(&right.1).then_with(|| return left.0.cmp(&right.0)));
339    return found;
340}
341
342/// The path of the file that holds the claim of `id` now, or nothing when it is not claimed or being recovered.
343pub fn find_claim(work_dir: &Path, id: &str) -> Option<PathBuf> {
344    return [CLAIMED, RECOVERING]
345        .into_iter()
346        .flat_map(|state| { return entries_oldest_first(work_dir, state).into_iter().map(move |(name, _)| { return (state, name); }); })
347        .find(|(_, name)| { return id_of_entry(name.as_str()) == id; })
348        .map(|(state, name)| { return state_directory(work_dir, state).join(name); });
349}
350
351/// What:
352///  Claim the oldest pending item by renaming it into `claimed` under a name that carries a fresh generation and the
353///  reading `now`. A record that does not decode is moved to `failed` so it cannot block the queue.
354/// Why:
355///   A rename is atomic: of two consumers racing for one item, the loser sees the file gone and takes the next one. The
356///  generation and the reading are in the name, so the claim is whole from the instant it exists; nothing is written to it.
357///
358/// In TS you'd write (pseudocode):
359/// ```ts
360/// await rename(join(workDir, 'pending', id), join(workDir, 'claimed', claimFileName(id, newGeneration(), now)))
361/// ```
362pub fn claim_next(work_dir: &Path, now: &ClaimTime) -> Result<Option<Claimed>, String> {
363    return claim_next_of(work_dir, now, &[WorkKind::Review, WorkKind::Push, WorkKind::Maintenance, WorkKind::Trash]);
364}
365
366/// What:
367///  Claim the oldest pending item of the given kinds, leaving every other kind where it is.
368/// Why:
369///   Consumers of different kinds share one queue; the kind starts the file name, so a consumer never opens, moves or
370///   stamps an item that belongs to another.
371///
372/// In TS you'd write (pseudocode):
373/// ```ts
374/// await rename(join(workDir, 'pending', id), join(workDir, 'claimed', name)) // only ids that start with a handled kind
375/// ```
376pub fn claim_next_of(work_dir: &Path, now: &ClaimTime, kinds: &[WorkKind]) -> Result<Option<Claimed>, String> {
377    return claim_next_stamped(work_dir, now, kinds, &stamp_modification_time);
378}
379
380/// What:
381///  `claim_next_of` with the stamp of the claim's modification time given,
382/// so a test can make the stamp fail.
383/// Why:
384///   The entry is taken into `recovering` under its claim name first,
385/// where no reader of `claimed` looks and where the
386///  entry is this instance's alone;
387/// only there is it stamped,
388/// and only then published into `claimed`.
389/// A claim that cannot be
390///  stamped or published goes back to `pending` with the modification time it had,
391/// so a queue that cannot be drained keeps
392///  its true age and the backlog notice still fires.
393///
394/// In TS you'd write (pseudocode):
395/// ```ts
396/// await rename(pending, staged);
397/// await stamp(staged);
398/// await rename(staged, join(workDir, 'claimed', name))
399/// ```
400fn claim_next_stamped(work_dir: &Path, now: &ClaimTime, kinds: &[WorkKind], stamp: Stamp<'_>) -> Result<Option<Claimed>, String> {
401    for (id, queued) in entries_oldest_first(work_dir, PENDING) {
402        let Some(kind) = kinds.iter().find(|kind| return id.starts_with(format!("{}-", kind.name()).as_str())) else {
403            continue;
404        };
405        // An id that is not a kind and an object name could make names that do not fit; it never becomes a claim.
406        if !is_object_name(&id[kind.name().len() + 1..]) {
407            quarantine_pending(work_dir, id.as_str(), format!("is not a kind and an object name of at most {MAX_OBJECT_NAME_LENGTH} letters and digits").as_str());
408            continue;
409        }
410        let name: String = claim_file_name(id.as_str(), new_generation()?.as_str(), now);
411        let pending_path: PathBuf = state_directory(work_dir, PENDING).join(id.as_str());
412        let staged: PathBuf = state_directory(work_dir, RECOVERING).join(name.as_str());
413        let claimed_path: PathBuf = state_directory(work_dir, CLAIMED).join(name.as_str());
414        if create_private_directory_all(state_directory(work_dir, RECOVERING).as_path()).is_err() || std::fs::rename(pending_path.as_path(), staged.as_path()).is_err() {
415            continue;
416        }
417        let published: std::io::Result<()> = stamp(staged.as_path()).and_then(|()| return std::fs::rename(staged.as_path(), claimed_path.as_path()));
418        if let Err(error) = published {
419            log(format!("the claim of {id} could not be taken, so the item stays queued: {error}").as_str());
420            // The staged file is this instance's alone:
421            // the age it had is put back before it is queued again,
422            // whatever the stamp did.
423            let restored: std::io::Result<()> = std::fs::File::open(staged.as_path()).and_then(|file| return file.set_modified(queued));
424            if let Err(requeue_error) = restored.and_then(|()| return std::fs::rename(staged.as_path(), pending_path.as_path())) {
425                log(format!("the staged claim of {id} could not be queued again; its lease will run out and recover it: {requeue_error}").as_str());
426            }
427            continue;
428        }
429        let text: String = std::fs::read_to_string(claimed_path.as_path()).map_err(|error| return format!("reading the claim of {id} failed: {error}"))?;
430        match WorkItem::decode(text.as_str()) {
431            // The record is the work the file name says;
432            // a record of other work would be settled as done under this id.
433            Ok(item) if item.id() == id => return Ok(Some(Claimed { id, name, item })),
434            _ => {
435                let _ = std::fs::rename(claimed_path.as_path(), state_directory(work_dir, FAILED).join(id.as_str()));
436            }
437        }
438    }
439    return Ok(None);
440}
441
442/// Moves the entry `id` of `pending` to `failed` and says why,
443/// where nothing can be done about a failure of either.
444fn quarantine_pending(work_dir: &Path, id: &str, reason: &str) {
445    let failed: PathBuf = state_directory(work_dir, FAILED);
446    let moved: bool = create_private_directory_all(failed.as_path()).is_ok() && std::fs::rename(state_directory(work_dir, PENDING).join(id), failed.join(id)).is_ok();
447    log(format!("the queued entry {id:?} {reason}, so it was {}", if moved { "moved to failed" } else { "left in pending because moving it failed" }).as_str());
448}
449
450/// What:
451///  Move a claimed item to `done` (or to `failed`), after the consumer's result is durable.
452/// Why:
453///   A crash before this rename leaves the item claimed, and its lease runs out; the work is then done again, so a
454///   handler must tolerate a repeat. The rename names the claim's generation, so a holder whose claim was taken over and
455///  claimed again by another settles nothing of the other's.
456///
457/// In TS you'd write (pseudocode):
458/// ```ts
459/// await rename(join(workDir, 'claimed', claimed.name), join(workDir, 'done', claimed.id))
460/// ```
461pub fn settle(work_dir: &Path, claimed: &Claimed, state: &str) -> Result<(), String> {
462    return settle_named(work_dir, claimed.name.as_str(), state);
463}
464
465/// `settle` for the claim file `name`, whose id is the part of the name before the generation.
466pub fn settle_named(work_dir: &Path, name: &str, state: &str) -> Result<(), String> {
467    let id: &str = id_of_entry(name);
468    return std::fs::rename(state_directory(work_dir, CLAIMED).join(name), state_directory(work_dir, state).join(id))
469        .map_err(|error| return format!("moving {id} to {state} failed: {error}"));
470}
471
472/// What:
473///  Start the lease of the claim file `name` again at `now`, returning its new name; nothing when the claim is no longer in
474///  `claimed` under that name or its modification time could not be set.
475/// Why:
476///   A holder that is still working must not look abandoned: every consumer of a directory that several consumers drain reads
477///   the reading in the name as the proof of life. The renewal is a rename of the exact name the holder last wrote, so a
478///  claim that was taken over, settled, or taken over and claimed again by someone else is not found and is never brought
479///  back or renewed on the other's behalf; and an instance that looked at the old name and means to take it finds nothing.
480///
481/// In TS you'd write (pseudocode):
482/// ```ts
483/// await rename(join(workDir, 'claimed', name), join(workDir, 'claimed', claimFileName(id, token, now)))
484/// ```
485pub fn renew_claim(work_dir: &Path, name: &str, now: &ClaimTime) -> Option<String> {
486    return renew_claim_stamped(work_dir, name, now, &stamp_modification_time);
487}
488
489/// `renew_claim` with the stamp of the claim's modification time given,
490/// so a test can make the stamp fail.
491/// A renewal whose
492///  stamp failed is no renewal:
493/// an older reader would still see the old modification time and take the claim.
494fn renew_claim_stamped(work_dir: &Path, name: &str, now: &ClaimTime, stamp: Stamp<'_>) -> Option<String> {
495    let id: &str = id_of_entry(name);
496    // A claim by an earlier release has no generation; its renewal gives it one.
497    let token: String = match parse_claim_name(name) {
498        Some((token, _)) => token.to_owned(),
499        None => new_generation().ok()?,
500    };
501    let renewed: String = claim_file_name(id, token.as_str(), now);
502    let directory: PathBuf = state_directory(work_dir, CLAIMED);
503    if let Err(error) = stamp(directory.join(name).as_path()) {
504        log(format!("the claim {name} could not be renewed: {error}").as_str());
505        return None;
506    }
507    return std::fs::rename(directory.join(name), directory.join(renewed.as_str())).ok().map(|()| { return renewed; });
508}
509
510/// What:
511///  How long the oldest pending item has waited, or `None` when nothing is pending.
512/// Why:
513///   The wrapper's one-line notice that no consumer is draining the queue is this age past a bound; a process id in a
514///   file would be meaningless across process-id namespaces.
515///
516/// In TS you'd write (pseudocode):
517/// ```ts
518/// now - oldestPending.mtime
519/// ```
520pub fn oldest_pending_age(work_dir: &Path, now: SystemTime) -> Option<Duration> {
521    return entries_oldest_first(work_dir, PENDING).first().map(|(_, created)| return now.duration_since(*created).unwrap_or(Duration::ZERO));
522}
523
524/// Controls stay out of the release executable.
525#[cfg(test)]
526#[path = "work_item_tests.rs"]
527mod tests;