tor_persist/state_dir.rs
1//! State helper utility
2//!
3//! All the methods in this module perform appropriate mistrust checks.
4//!
5//! All the methods arrange to ensure suitably-finegrained exclusive access.
6//! "Read-only" or "shared" mode is not supported.
7//!
8//! ### Differences from `tor_persist::StorageHandle`
9//!
10//! * Explicit provision is made for multiple instances of a single facility.
11//! For example, multiple hidden services,
12//! each with their own state, and own lock.
13//!
14//! * Locking (via filesystem locks) is mandatory, rather than optional -
15//! there is no "shared" mode.
16//!
17//! * Locked state is represented in the Rust type system.
18//!
19//! * We don't use traits to support multiple implementations.
20//! Platform support would be done in the future with `#[cfg]`.
21//! Testing is done by temporary directories (as currently with `tor_persist`).
22//!
23//! * The serde-based `StorageHandle` requires `&mut` for writing.
24//! This ensures proper serialisation of 1. read-modify-write cycles
25//! and 2. use of the temporary file.
26//! Or to put it another way, we model `StorageHandle`
27//! as *containing* a `T` without interior mutability.
28//!
29//! * There's a way to get a raw directory for filesystem operations
30//! (currently, will be used for IPT replay logs).
31//!
32//! ### Implied filesystem structure
33//!
34//! ```text
35//! STATE_DIR/
36//! STATE_DIR/KIND/INSTANCE_ID/
37//! STATE_DIR/KIND/INSTANCE_ID/lock
38//! STATE_DIR/KIND/INSTANCE_ID/KEY.json
39//! STATE_DIR/KIND/INSTANCE_ID/KEY.new
40//! STATE_DIR/KIND/INSTANCE_ID/KEY/
41//!
42//! eg
43//!
44//! STATE_DIR/hss/allium-cepa.lock
45//! STATE_DIR/hss/allium-cepa/ipts.json
46//! STATE_DIR/hss/allium-cepa/iptpub.json
47//! STATE_DIR/hss/allium-cepa/iptreplay/
48//! STATE_DIR/hss/allium-cepa/iptreplay/9aa9517e6901c280a550911d3a3c679630403db1c622eedefbdf1715297f795f.bin
49//! ```
50//!
51// The instance's last modification time (see `purge_instances`) is the mtime of
52// the INSTANCE_ID directory. The lockfile mtime is not meaningful.
53//
54//! (The lockfile is outside the instance directory to facilitate
55//! concurrency-correct deletion.)
56//!
57// Specifically:
58//
59// The situation where there is only the lockfile, is an out-of-course but legal one.
60// Likewise, a lockfile plus a *partially* deleted instance state, is also legal.
61// Having an existing directory without associated lockfile is forbidden,
62// but if it should occur we handle it properly.
63//
64//! ### Comprehensive example
65//!
66//! ```
67//! use std::{collections::HashSet, fmt, time::{Duration, SystemTime}};
68//! use tor_error::{into_internal, Bug};
69//! use tor_persist::slug::SlugRef;
70//! use tor_persist::state_dir;
71//! use state_dir::{InstanceIdentity, InstancePurgeHandler};
72//! use state_dir::{InstancePurgeInfo, InstanceStateHandle, StateDirectory, StorageHandle};
73//! use web_time_compat::SystemTimeExt;
74//! #
75//! # // fake up some things; we do this rather than using real ones
76//! # // since this example will move, with the module, to a lower level crate.
77//! # struct OnionService { }
78//! # #[derive(derive_more::Display)] struct HsNickname(String);
79//! # type Error = anyhow::Error;
80//! # mod ipt_mgr { pub mod persist {
81//! # #[derive(serde::Serialize, serde::Deserialize)] pub struct StateRecord {}
82//! # } }
83//!
84//! impl InstanceIdentity for HsNickname {
85//! fn kind() -> &'static str { "hss" }
86//! fn write_identity(&self, f: &mut fmt::Formatter) -> fmt::Result {
87//! write!(f, "{self}")
88//! }
89//! }
90//!
91//! impl OnionService {
92//! fn new(
93//! nick: HsNickname,
94//! state_dir: &StateDirectory,
95//! ) -> Result<Self, Error> {
96//! let instance_state = state_dir.acquire_instance(&nick)?;
97//! let replay_log_dir = instance_state.raw_subdir("ipt_replay")?;
98//! let ipts_storage: StorageHandle<ipt_mgr::persist::StateRecord> =
99//! instance_state.storage_handle("ipts")?;
100//! // ..
101//! # Ok(OnionService { })
102//! }
103//! }
104//!
105//! struct PurgeHandler<'h>(&'h HashSet<&'h str>, Duration);
106//! impl InstancePurgeHandler for PurgeHandler<'_> {
107//! fn kind(&self) -> &'static str {
108//! <HsNickname as InstanceIdentity>::kind()
109//! }
110//! fn name_filter(&mut self, id: &SlugRef) -> state_dir::Result<state_dir::Liveness> {
111//! Ok(if self.0.contains(id.as_str()) {
112//! state_dir::Liveness::Live
113//! } else {
114//! state_dir::Liveness::PossiblyUnused
115//! })
116//! }
117//! fn age_filter(&mut self, id: &SlugRef, age: Duration)
118//! -> state_dir::Result<state_dir::Liveness>
119//! {
120//! Ok(if age > self.1 {
121//! state_dir::Liveness::PossiblyUnused
122//! } else {
123//! state_dir::Liveness::Live
124//! })
125//! }
126//! fn dispose(&mut self, _info: &InstancePurgeInfo, handle: InstanceStateHandle)
127//! -> state_dir::Result<()> {
128//! // here might be a good place to delete keys too
129//! handle.purge()
130//! }
131//! }
132//! pub fn expire_hidden_services(
133//! state_dir: &StateDirectory,
134//! currently_configured_nicks: &HashSet<&str>,
135//! retain_for: Duration,
136//! ) -> Result<(), Error> {
137//! state_dir.purge_instances(
138//! SystemTime::get(),
139//! &mut PurgeHandler(currently_configured_nicks, retain_for),
140//! )?;
141//! Ok(())
142//! }
143//! ```
144//!
145//! ### Platforms without a filesystem
146//!
147//! The implementation and (in places) the documentation
148//! is in terms of filesystems.
149//! But, everything except `InstanceStateHandle::raw_subdir`
150//! is abstract enough to implement some other way.
151//!
152//! If we wish to support such platforms, the approach is:
153//!
154//! * Decide on an approach for `StorageHandle`
155//! and for each caller of `raw_subdir`.
156//!
157//! * Figure out how the startup code will look.
158//! (Currently everything is in terms of `fs_mistrust` and filesystems.)
159//!
160//! * Provide a version of this module with a compatible API
161//! in terms of whatever underlying facilities are available.
162//! Use `#[cfg]` to select it.
163//! Don't implement `raw_subdir`.
164//!
165//! * Call sites using `raw_subdir` will no longer compile.
166//! Use `#[cfg]` at call sites to replace the `raw_subdir`
167//! with whatever is appropriate for the platform.
168
169#![forbid(unsafe_code)] // if you remove this, enable (or write) miri tests (git grep miri)
170
171use std::collections::HashSet;
172use std::fmt::{self, Display};
173use std::fs;
174use std::io;
175use std::marker::PhantomData;
176use std::path::Path;
177use std::sync::Arc;
178use web_time_compat::{Duration, SystemTime, SystemTimeExt};
179
180use derive_deftly::{Deftly, define_derive_deftly};
181use derive_more::{AsRef, Deref};
182use itertools::chain;
183use serde::{Serialize, de::DeserializeOwned};
184
185use fs_mistrust::{CheckedDir, Mistrust};
186use tor_error::ErrorReport as _;
187use tor_error::bad_api_usage;
188use tracing::trace;
189
190pub use crate::Error;
191use crate::err::{Action, ErrorSource, Resource};
192use crate::load_store;
193use crate::slug::{BadSlug, Slug, SlugRef, TryIntoSlug};
194
195#[allow(unused_imports)] // Simplifies a lot of references in our docs
196use crate::slug;
197
198define_derive_deftly! {
199 ContainsInstanceStateGuard:
200
201 impl<$tgens> ContainsInstanceStateGuard for $ttype where $twheres {
202 fn raw_lock_guard(&self) -> Arc<LockFileGuard> {
203 self.flock_guard.clone()
204 }
205 }
206}
207
208/// Re-export of the lock guard type, as obtained via [`ContainsInstanceStateGuard`]
209pub use fslock_guard::LockFileGuard;
210
211use std::result::Result as StdResult;
212
213use std::path::MAIN_SEPARATOR as PATH_SEPARATOR;
214
215/// [`Result`](StdResult) throwing a [`state_dir::Error`](Error)
216pub type Result<T> = StdResult<T, Error>;
217
218/// Extension for lockfiles
219const LOCK_EXTN: &str = "lock";
220/// Suffix for lockfiles, precisely `"." + LOCK_EXTN`
221// There's no way to concatenate constant strings with names!
222// We could use the const_format crate maybe?
223const DOT_LOCK: &str = ".lock";
224
225/// The whole program's state directory
226///
227/// Representation of `[storage] state_dir` and `permissions`
228/// from the Arti configuration.
229///
230/// This type does not embody any subpaths relating to
231/// any particular facility within Arti.
232///
233/// Constructing a `StateDirectory` may involve filesystem permissions checks,
234/// so ideally it would be created once per process for performance reasons.
235///
236/// Existence of a `StateDirectory` also does not imply exclusive access.
237///
238/// This type is passed to each facility's constructor;
239/// the facility implements [`InstanceIdentity`]
240/// and calls [`acquire_instance`](StateDirectory::acquire_instance).
241///
242/// ### Use for caches
243///
244/// In principle this type and the methods and subtypes available
245/// would be suitable for cache data as well as state data.
246///
247/// However the locking scheme does not tolerate random removal of files.
248/// And cache directories are sometimes configured to point to locations
249/// with OS-supplied automatic file cleaning.
250/// That would not be correct,
251/// since the automatic file cleaner might remove an in-use lockfile,
252/// effectively unlocking the instance state
253/// even while a process exists that thinks it still has the lock.
254#[derive(Debug, Clone)]
255pub struct StateDirectory {
256 /// The actual directory, including mistrust config
257 dir: CheckedDir,
258}
259
260/// An instance of a facility that wants to save persistent state (caller-provided impl)
261///
262/// Each value of a type implementing `InstanceIdentity`
263/// designates a specific instance of a specific facility.
264///
265/// For example, `HsNickname` implements `state_dir::InstanceIdentity`.
266///
267/// The kind and identity are [`slug`]s.
268pub trait InstanceIdentity {
269 /// Return the kind. For example `hss` for a Tor Hidden Service.
270 ///
271 /// This must return a fixed string,
272 /// since usually all instances represented the same Rust type
273 /// are also the same kind.
274 ///
275 /// The returned value must be valid as a [`slug`].
276 //
277 // This precludes dynamically chosen instance kind identifiers.
278 // If we ever want that, we'd need an InstanceKind trait that is implemented
279 // not for actual instances, but for values representing a kind.
280 fn kind() -> &'static str;
281
282 /// Obtain identity
283 ///
284 /// The instance identity distinguishes different instances of the same kind.
285 ///
286 /// For example, for a Tor Hidden Service the identity is the nickname.
287 ///
288 /// The generated string must be valid as a [`slug`].
289 /// If it is not, the functions in this module will throw `Bug` errors.
290 /// (Returning `fmt::Error` will cause a panic, as is usual with the fmt API.)
291 fn write_identity(&self, f: &mut fmt::Formatter) -> fmt::Result;
292}
293
294/// For a facility to be expired using [`purge_instances`](StateDirectory::purge_instances) (caller-provided impl)
295///
296/// A filter which decides which instances to delete,
297/// and deletes them if appropriate.
298///
299/// See [`purge_instances`](StateDirectory::purge_instances) for full documentation.
300pub trait InstancePurgeHandler {
301 /// What kind to iterate over
302 fn kind(&self) -> &'static str;
303
304 /// Can we tell by its name that this instance is still live ?
305 fn name_filter(&mut self, identity: &SlugRef) -> Result<Liveness>;
306
307 /// Can we tell by recent modification that this instance is still live ?
308 ///
309 /// Many implementations won't need to use the `identity` parameter.
310 ///
311 /// ### Concurrency
312 ///
313 /// The `age` passed to this callback might
314 /// sometimes not be the most recent modification time of the instance.
315 /// But. before calling `dispose`, `purge_instances` will call this
316 /// function at least once with a fully up-to-date modification time.
317 fn age_filter(&mut self, identity: &SlugRef, age: Duration) -> Result<Liveness>;
318
319 /// Decide whether to keep this instance
320 ///
321 /// When it has made its decision, `dispose` should
322 /// either call [`delete`](InstanceStateHandle::purge),
323 /// or simply drop `handle`.
324 ///
325 /// Called only after `name_filter` and `age_filter`
326 /// both returned [`Liveness::PossiblyUnused`].
327 ///
328 /// `info` includes the instance name and other useful information
329 /// such as the last modification time.
330 ///
331 /// Note that although the existence of `handle` implies
332 /// there can be no other `InstanceStateHandle`s for this instance,
333 /// the last modification time of this instance has *not* been updated,
334 /// as it would be by [`acquire_instance`](StateDirectory::acquire_instance).
335 fn dispose(&mut self, info: &InstancePurgeInfo, handle: InstanceStateHandle) -> Result<()>;
336}
337
338/// Information about an instance, passed to [`InstancePurgeHandler::dispose`]
339#[derive(Debug, Clone, amplify::Getters, AsRef)]
340pub struct InstancePurgeInfo<'i> {
341 /// The instance's identity string
342 #[as_ref]
343 identity: &'i SlugRef,
344
345 /// When the instance state was last updated, according to the filesystem timestamps
346 ///
347 /// See `[InstanceStateHandle::purge_instances]`
348 /// for details of what kinds of events count as modifications.
349 last_modified: SystemTime,
350}
351
352/// Is an instance still relevant?
353///
354/// Returned by [`InstancePurgeHandler::name_filter`].
355///
356/// See [`StateDirectory::purge_instances`] for details of the semantics.
357#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
358#[allow(clippy::exhaustive_enums)] // this is a boolean
359pub enum Liveness {
360 /// This instance is not known to be interesting
361 ///
362 /// It could be perhaps expired, if it's been long enough
363 PossiblyUnused,
364 /// This instance is still wanted
365 Live,
366}
367
368/// Objects that co-own a lock on an instance
369///
370/// Each type implementing this trait mutually excludes independently-acquired
371/// [`InstanceStateHandle`]s, and anything derived from them
372/// (including, therefore, `ContainsInstanceStateGuard` implementors
373/// with independent provenance.)
374pub trait ContainsInstanceStateGuard {
375 /// Obtain a raw clone of the underlying filesystem lock
376 ///
377 /// This lock (and clones of it) will mutually exclude
378 /// re-acquisition of the same instance.
379 fn raw_lock_guard(&self) -> Arc<LockFileGuard>;
380}
381
382/// Instance identity string formatter, type-erased
383type InstanceIdWriter<'i> = &'i dyn Fn(&mut fmt::Formatter) -> fmt::Result;
384
385impl StateDirectory {
386 /// Create a new `StateDirectory` from a directory and mistrust configuration
387 pub fn new(state_dir: impl AsRef<Path>, mistrust: &Mistrust) -> Result<Self> {
388 /// Implementation, taking non-generic path
389 fn inner(path: &Path, mistrust: &Mistrust) -> Result<StateDirectory> {
390 let resource = || Resource::Directory {
391 dir: path.to_owned(),
392 };
393 let handle_err = |source| Error::new(source, Action::Initializing, resource());
394
395 let dir = mistrust
396 .verifier()
397 .make_secure_dir(path)
398 .map_err(handle_err)?;
399
400 Ok(StateDirectory { dir })
401 }
402 inner(state_dir.as_ref(), mistrust)
403 }
404
405 /// Acquires (creates and locks) a storage for an instance
406 ///
407 /// Ensures the existence and suitability of a subdirectory named `kind/identity`,
408 /// and locks it for exclusive access.
409 pub fn acquire_instance<I: InstanceIdentity>(
410 &self,
411 identity: &I,
412 ) -> Result<InstanceStateHandle> {
413 /// Implementation, taking non-generic values for identity
414 fn inner(
415 sd: &StateDirectory,
416 kind_str: &'static str,
417 id_writer: InstanceIdWriter,
418 ) -> Result<InstanceStateHandle> {
419 sd.with_instance_path_pieces(kind_str, id_writer, |kind, id, resource| {
420 let handle_err =
421 |action, source: ErrorSource| Error::new(source, action, resource());
422
423 // Obtain (creating if necessary) a subdir for a Checked
424 let make_secure_directory = |parent: &CheckedDir, subdir| {
425 let resource = || Resource::Directory {
426 dir: parent.as_path().join(subdir),
427 };
428 parent
429 .make_secure_directory(subdir)
430 .map_err(|source| Error::new(source, Action::Initializing, resource()))
431 };
432
433 // ---- obtain the lock ----
434
435 let kind_dir = make_secure_directory(&sd.dir, kind)?;
436
437 let lock_path = kind_dir
438 .join(format!("{id}.{LOCK_EXTN}"))
439 .map_err(|source| handle_err(Action::Initializing, source.into()))?;
440
441 let flock_guard = match LockFileGuard::try_lock(&lock_path) {
442 Ok(Some(y)) => {
443 trace!("locked {lock_path:?}");
444 y.into()
445 }
446 Err(source) => {
447 trace!("locking {lock_path:?}, error {}", source.report());
448 return Err(handle_err(Action::Locking, source.into()));
449 }
450 Ok(None) => {
451 trace!("locking {lock_path:?}, in use",);
452 return Err(handle_err(Action::Locking, ErrorSource::AlreadyLocked));
453 }
454 };
455
456 // ---- we have the lock, calculate the directory (creating it if need be) ----
457
458 let dir = make_secure_directory(&kind_dir, id)?;
459
460 touch_instance_dir(&dir)?;
461
462 Ok(InstanceStateHandle { dir, flock_guard })
463 })
464 }
465
466 inner(self, I::kind(), &|f| identity.write_identity(f))
467 }
468
469 /// Given a kind and id, obtain pieces of its path and call a "doing work" callback
470 ///
471 /// This function factors out common functionality needed by
472 /// [`StateDirectory::acquire_instance`] and [`StateDirectory::instance_peek_storage`],
473 /// particularly relating to instance kind and id, and errors.
474 ///
475 /// `kind` and `id` are from an `InstanceIdentity`.
476 fn with_instance_path_pieces<T>(
477 self: &StateDirectory,
478 kind_str: &'static str,
479 id_writer: InstanceIdWriter,
480 // fn call(kind: &SlugRef, id: &SlugRef, resource_for_error: &impl Fn) -> _
481 call: impl FnOnce(&SlugRef, &SlugRef, &dyn Fn() -> Resource) -> Result<T>,
482 ) -> Result<T> {
483 /// Struct that impls `Display` for formatting an instance id
484 //
485 // This exists because we want implementors of InstanceIdentity to be able to
486 // use write! to format their identity string.
487 struct InstanceIdDisplay<'i>(InstanceIdWriter<'i>);
488
489 impl Display for InstanceIdDisplay<'_> {
490 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
491 (self.0)(f)
492 }
493 }
494 let id_string = InstanceIdDisplay(id_writer).to_string();
495
496 // Both we and caller use this for our error reporting
497 let resource = || Resource::InstanceState {
498 state_dir: self.dir.as_path().to_owned(),
499 kind: kind_str.to_string(),
500 identity: id_string.clone(),
501 };
502
503 let handle_bad_slug = |source| Error::new(source, Action::Initializing, resource());
504
505 if kind_str.is_empty() {
506 return Err(handle_bad_slug(BadSlug::EmptySlugNotAllowed));
507 }
508 let kind = SlugRef::new(kind_str).map_err(handle_bad_slug)?;
509 let id = SlugRef::new(&id_string).map_err(handle_bad_slug)?;
510
511 call(kind, id, &resource)
512 }
513
514 /// List the instances of a particular kind
515 ///
516 /// Returns the instance identities.
517 ///
518 /// (The implementation lists subdirectories named `kind_*`.)
519 ///
520 /// Concurrency:
521 /// An instance which is not being removed or created will be
522 /// listed (or not) according to whether it's present.
523 /// But, in the presence of concurrent calls to `acquire_instance` and `delete`
524 /// on different instances,
525 /// is not guaranteed to provide a snapshot:
526 /// serialisation is not guaranteed across different instances.
527 ///
528 /// It *is* guaranteed to list each instance only once.
529 pub fn list_instances<I: InstanceIdentity>(
530 &self,
531 ) -> impl Iterator<Item = Result<Slug>> + use<I> {
532 self.list_instances_inner(I::kind())
533 }
534
535 /// List the instances of a kind, where the kind is supplied as a value
536 ///
537 /// Used by `list_instances` and `purge_instances`.
538 ///
539 /// *Includes* instances that exists only as a stale lockfile.
540 #[allow(clippy::blocks_in_conditions)] // TODO #1176 this wants to be global
541 #[allow(clippy::redundant_closure_call)] // false positive, re handle_err
542 fn list_instances_inner(
543 &self,
544 kind: &'static str,
545 ) -> impl Iterator<Item = Result<Slug>> + use<> {
546 // We collect the output into these
547 let mut out = HashSet::new();
548 let mut errs = Vec::new();
549
550 // Error handling
551
552 let resource = || Resource::InstanceState {
553 state_dir: self.dir.as_path().into(),
554 kind: kind.into(),
555 identity: "*".into(),
556 };
557
558 /// `fn handle_err!()(source: impl Into<ErrorSource>) -> Error`
559 //
560 // (Generic, so can't be a closure. Uses local bindings, so can't be a fn.)
561 macro_rules! handle_err { { } => {
562 |source| Error::new(source, Action::Enumerating, resource())
563 } }
564
565 // Obtain an iterator of Result<DirEntry>
566 match (|| {
567 let kind = SlugRef::new(kind).map_err(handle_err!())?;
568 self.dir.read_directory(kind).map_err(handle_err!())
569 })() {
570 Err(e) => errs.push(e),
571 Ok(ents) => {
572 for ent in ents {
573 match ent {
574 Err(e) => errs.push(handle_err!()(e)),
575 Ok(ent) => {
576 // Actually handle a directory entry!
577
578 let Some(id) = (|| {
579 // look for either ID or ID.lock
580 let id = ent.file_name();
581 let id = id.to_str()?; // ignore non-UTF-8
582 let id = id.strip_suffix(DOT_LOCK).unwrap_or(id);
583 let id = SlugRef::new(id).ok()?; // ignore other things
584 Some(id.to_owned())
585 })() else {
586 continue;
587 };
588
589 out.insert(id);
590 }
591 }
592 }
593 }
594 }
595
596 chain!(errs.into_iter().map(Err), out.into_iter().map(Ok),)
597 }
598
599 /// Delete instances according to selections made by the caller
600 ///
601 /// Each instance is considered in three stages.
602 ///
603 /// Firstly, it is passed to [`name_filter`](InstancePurgeHandler::name_filter).
604 /// If `name_filter` returns `Live`,
605 /// further consideration is skipped and the instance is retained.
606 ///
607 /// Secondly, the last time the instance was written to is determined,
608 // This must be done with the lock held, for correctness
609 // but the lock must be acquired in a way that doesn't itself update the modification time.
610 // On Unix this is straightforward because opening for write doesn't update the mtime.
611 // If this is hard on another platform, we'll need a separate stamp file updated
612 // by an explicit Acquire operation.
613 // This is tested by `test_reset_expiry`.
614 /// and passed to
615 /// [`age_filter`](InstancePurgeHandler::age_filter).
616 /// Again, this might mean ensure the instance is retained.
617 ///
618 /// Thirdly, the resulting `InstanceStateHandle` is passed to
619 /// [`dispose`](InstancePurgeHandler::dispose).
620 /// `dispose` may choose to call `handle.delete()`,
621 /// or simply drop the handle.
622 ///
623 /// Concurrency:
624 /// In the presence of multiple concurrent calls to `acquire_instance` and `delete`:
625 /// `filter` may be called for an instance which is being created or deleted
626 /// by another task.
627 /// `dispose` will be properly serialised with other activities on the same instance,
628 /// as implied by it receiving an `InstanceStateHandle`.
629 ///
630 /// The expiry time is reset by calls to `acquire_instance`,
631 /// `StorageHandle::store` and `InstanceStateHandle::raw_subdir`;
632 /// it *may* be reset by calls to `StorageHandle::delete`.
633 ///
634 /// Instances that are currently locked by another task will not be purged,
635 /// but the expiry time is *not* reset by *unlocking* an instance
636 /// (dropping the last clone of an `InstanceStateHandle`).
637 ///
638 /// ### Sequencing of `InstancePurgeHandler` callbacks
639 ///
640 /// Each instance will be processed
641 /// (and callbacks made for it) at most once;
642 /// and calls for different instances will not be interleaved.
643 ///
644 /// During the processing of a particular instance
645 /// The callbacks will be made in order,
646 /// progressing monotonically through the methods in the order listed.
647 /// But `name_filter` and `age_filter` might each be called
648 /// more than once for the same instance.
649 // We don't actually call name_filter more than once.
650 ///
651 /// Between each stage,
652 /// the purge implementation may discover that the instance
653 /// ought not to be processed further.
654 /// So returning `Liveness::PossiblyUnused` from a filter does not
655 /// guarantee that the next callback will be made.
656 pub fn purge_instances(
657 &self,
658 now: SystemTime,
659 filter: &mut (dyn InstancePurgeHandler + '_),
660 ) -> Result<()> {
661 let kind = filter.kind();
662
663 for id in self.list_instances_inner(kind) {
664 let id = id?;
665 self.with_instance_path_pieces(kind, &|f| write!(f, "{id}"), |kind, id, resource| {
666 self.maybe_purge_instance(now, kind, id, resource, filter)
667 })?;
668 }
669
670 Ok(())
671 }
672
673 /// Consider whether to purge an instance
674 ///
675 /// Performs all the necessary steps, including liveness checks,
676 /// passing an InstanceStateHandle to filter.dispose,
677 /// and deleting stale lockfiles without associated state.
678 #[allow(clippy::cognitive_complexity)] // splitting this would be more, not less, confusing
679 fn maybe_purge_instance(
680 &self,
681 now: SystemTime,
682 kind: &SlugRef,
683 id: &SlugRef,
684 resource: &dyn Fn() -> Resource,
685 filter: &mut (dyn InstancePurgeHandler + '_),
686 ) -> Result<()> {
687 /// If `$l` is `Liveness::Live`, returns early with `Ok(())`.
688 macro_rules! check_liveness { { $l:expr } => {
689 match $l {
690 Liveness::Live => return Ok(()),
691 Liveness::PossiblyUnused => {},
692 }
693 } }
694
695 check_liveness!(filter.name_filter(id)?);
696
697 let dir_path = self.dir.as_path().join(kind).join(id);
698
699 // Checks whether it should be kept due to being recently modified.
700 // None::<SystemTime> means the instance directory is ENOENT
701 // (which must mean that the instance exists only as a stale lockfile).
702 let mut age_check = || -> Result<(Liveness, Option<SystemTime>)> {
703 let handle_io_error = |source| Error::new(source, Action::Enumerating, resource());
704
705 // 1. stat the instance dir
706 let md = match fs::metadata(&dir_path) {
707 // If instance dir is ENOENT, treat as old (maybe there was just a lockfile)
708 Err(e) if e.kind() == io::ErrorKind::NotFound => {
709 return Ok((Liveness::PossiblyUnused, None));
710 }
711 other => other.map_err(handle_io_error)?,
712 };
713 let mtime = md.modified().map_err(handle_io_error)?;
714
715 // 2. calculate the age
716 let age = now.duration_since(mtime).unwrap_or(Duration::ZERO);
717
718 // 3. do the age check
719 let liveness = filter.age_filter(id, age)?;
720
721 Ok((liveness, Some(mtime)))
722 };
723
724 // preliminary check, without locking yet
725 check_liveness!(age_check()?.0);
726
727 // ok we're probably doing to pass it to dispose (for possible deletion)
728
729 let lock_path = dir_path.with_extension(LOCK_EXTN);
730 let flock_guard = match LockFileGuard::try_lock(&lock_path) {
731 Ok(Some(y)) => {
732 trace!("locked {lock_path:?} (for purge)");
733 y
734 }
735 Err(source) if source.kind() == io::ErrorKind::NotFound => {
736 // We couldn't open the lockfile due to ENOENT
737 // (Presumably) a containing directory is gone, so we don't need to do anything.
738 trace!("locking {lock_path:?} (for purge), not found");
739 return Ok(());
740 }
741 Ok(None) => {
742 // Someone else has it locked. Skip purging it.
743 trace!("locking {lock_path:?} (for purge), in use");
744 return Ok(());
745 }
746 Err(source) => {
747 trace!(
748 "locking {lock_path:?} (for purge), error {}",
749 source.report()
750 );
751 return Err(Error::new(source, Action::Locking, resource()));
752 }
753 };
754
755 // recheck to see if anyone has updated it
756 let (age, mtime) = age_check()?;
757 check_liveness!(age);
758
759 // We have locked it and the filters say to maybe purge it.
760
761 match mtime {
762 None => {
763 // And it doesn't even exist! All we have is a leftover lockfile. Delete it.
764 let lockfile_rsrc = || Resource::File {
765 container: lock_path.parent().expect("no /!").into(),
766 file: lock_path.file_name().expect("no /!").into(),
767 };
768 flock_guard
769 .delete_lock_file(&lock_path)
770 .map_err(|source| Error::new(source, Action::Deleting, lockfile_rsrc()))?;
771 }
772 Some(last_modified) => {
773 // Construct a state handle.
774 let dir = self
775 .dir
776 .make_secure_directory(format!("{kind}/{id}"))
777 .map_err(|source| Error::new(source, Action::Enumerating, resource()))?;
778 let flock_guard = Arc::new(flock_guard);
779
780 filter.dispose(
781 &InstancePurgeInfo {
782 identity: id,
783 last_modified,
784 },
785 InstanceStateHandle { dir, flock_guard },
786 )?;
787 }
788 }
789
790 Ok(())
791 }
792
793 /// Tries to peek at something written by [`StorageHandle::store`]
794 ///
795 /// It is guaranteed that this will return either the `T` that was stored,
796 /// or `None` if `store` was never called,
797 /// or `StorageHandle::delete` was called
798 ///
799 /// So the operation is atomic, but there is no further synchronisation.
800 //
801 // Not sure if we need this, but it's logically permissible
802 pub fn instance_peek_storage<I: InstanceIdentity, T: DeserializeOwned>(
803 &self,
804 identity: &I,
805 key: &(impl TryIntoSlug + ?Sized),
806 ) -> Result<Option<T>> {
807 self.with_instance_path_pieces(
808 I::kind(),
809 &|f| identity.write_identity(f),
810 // This closure is generic over T, so with_instance_path_pieces will be too;
811 // this isn't desirable (code bloat) but avoiding it would involves some contortions.
812 |kind_slug: &SlugRef, id_slug: &SlugRef, _resource| {
813 // Throwing this error here will give a slightly wrong Error for this Bug
814 // (because with_instance_path_pieces has its own notion of Action & Resource)
815 // but that seems OK.
816 let key_slug = key.try_into_slug()?;
817
818 let rel_fname = format!(
819 "{}{PATH_SEPARATOR}{}{PATH_SEPARATOR}{}.json",
820 kind_slug, id_slug, key_slug,
821 );
822
823 let target = load_store::Target {
824 dir: &self.dir,
825 rel_fname: rel_fname.as_ref(),
826 };
827
828 target
829 .load()
830 // This Resource::File isn't consistent with those from StorageHandle:
831 // StorageHandle's `container` is the instance directory;
832 // here `container` is the top-level `state_dir`,
833 // and `file` is `KIND+INSTANCE/STORAGE.json".
834 .map_err(|source| {
835 Error::new(
836 source,
837 Action::Loading,
838 Resource::File {
839 container: self.dir.as_path().to_owned(),
840 file: rel_fname.into(),
841 },
842 )
843 })
844 },
845 )
846 }
847}
848
849/// State or cache directory for an instance of a facility
850///
851/// Implies exclusive access:
852/// there is only one `InstanceStateHandle` at a time,
853/// across any number of processes, tasks, and threads,
854/// for the same instance.
855///
856/// # Key uniqueness and syntactic restrictions
857///
858/// Methods on `InstanceStateHandle` typically take a [`TryIntoSlug`].
859///
860/// **It is important that keys are distinct within an instance.**
861///
862/// Specifically:
863/// each key provided to a method on the same [`InstanceStateHandle`]
864/// (or a clone of it)
865/// must be different.
866/// Violating this rule does not result in memory-unsafety,
867/// but might result in incorrect operation due to concurrent filesystem access,
868/// including possible data loss and corruption.
869/// (Typically, the key is fixed, and the [`StorageHandle`]s are usually
870/// obtained during instance construction, so ensuring this is straightforward.)
871///
872/// There are also syntactic restrictions on keys. See [slug].
873// We could implement a runtime check for this by retaining a table of in-use keys,
874// possibly only with `cfg(debug_assertions)`. However I think this isn't worth the code:
875// it would involve an Arc<Mutex<SlugsInUseTable>> in InstanceStateHnndle and StorageHandle,
876// and Drop impls to remove unused entries (and `raw_subdir` would have imprecise checking
877// unless it returned a Drop newtype around CheckedDir).
878#[derive(Debug, Clone, Deftly)]
879#[derive_deftly(ContainsInstanceStateGuard)]
880pub struct InstanceStateHandle {
881 /// The directory
882 dir: CheckedDir,
883 /// Lock guard
884 flock_guard: Arc<LockFileGuard>,
885}
886
887impl InstanceStateHandle {
888 /// Obtain a [`StorageHandle`], usable for storing/retrieving a `T`
889 ///
890 /// [`key` has syntactic and uniqueness restrictions.](InstanceStateHandle#key-uniqueness-and-syntactic-restrictions)
891 pub fn storage_handle<T>(&self, key: &(impl TryIntoSlug + ?Sized)) -> Result<StorageHandle<T>> {
892 /// Implementation, not generic over `slug` and `T`
893 fn inner(
894 ih: &InstanceStateHandle,
895 key: StdResult<Slug, BadSlug>,
896 ) -> Result<(CheckedDir, String, Arc<LockFileGuard>)> {
897 let key = key?;
898 let instance_dir = ih.dir.clone();
899 let leafname = format!("{key}.json");
900 let flock_guard = ih.flock_guard.clone();
901 Ok((instance_dir, leafname, flock_guard))
902 }
903
904 let (instance_dir, leafname, flock_guard) = inner(self, key.try_into_slug())?;
905 Ok(StorageHandle {
906 instance_dir,
907 leafname,
908 marker: PhantomData,
909 flock_guard,
910 })
911 }
912
913 /// Obtain a raw filesystem subdirectory, within the directory for this instance
914 ///
915 /// This API is unsuitable platforms without a filesystem accessible via `std::fs`.
916 /// May therefore only be used within Arti for features
917 /// where we're happy to not to support such platforms (eg WASM without WASI)
918 /// without substantial further work.
919 ///
920 /// [`key` has syntactic and uniqueness restrictions.](InstanceStateHandle#key-uniqueness-and-syntactic-restrictions)
921 pub fn raw_subdir(&self, key: &(impl TryIntoSlug + ?Sized)) -> Result<InstanceRawSubdir> {
922 /// Implementation, not generic over `slug`
923 fn inner(
924 ih: &InstanceStateHandle,
925 key: StdResult<Slug, BadSlug>,
926 ) -> Result<InstanceRawSubdir> {
927 let key = key?;
928 let irs = (|| {
929 trace!("ensuring/using {:?}/{:?}", ih.dir.as_path(), key.as_str());
930 let dir = ih.dir.make_secure_directory(&key)?;
931 let flock_guard = ih.flock_guard.clone();
932 Ok::<_, ErrorSource>(InstanceRawSubdir { dir, flock_guard })
933 })()
934 .map_err(|source| {
935 Error::new(
936 source,
937 Action::Initializing,
938 Resource::Directory {
939 dir: ih.dir.as_path().join(key),
940 },
941 )
942 })?;
943 touch_instance_dir(&ih.dir)?;
944 Ok(irs)
945 }
946 inner(self, key.try_into_slug())
947 }
948
949 /// Unconditionally delete this instance directory
950 ///
951 /// For expiry, use `StateDirectory::purge_instances`,
952 /// and then call this in the `dispose` method.
953 ///
954 /// Will return a `BadAPIUsage` if other clones of this `InstanceStateHandle` exist.
955 ///
956 /// ### Deletion is *not* atomic
957 ///
958 /// If a deletion operation doesn't complete for any reason
959 /// (maybe it was interrupted, or there was a filesystem access problem),
960 /// *part* of the instance contents may remain.
961 ///
962 /// After such an interrupted deletion,
963 /// storage items ([`StorageHandle`]) are might each independently
964 /// be deleted ([`load`](StorageHandle::load) returns `None`)
965 /// or retained (`Some`).
966 ///
967 /// Deletion of the contents of raw subdirectories
968 /// ([`InstanceStateHandle::raw_subdir`])
969 /// is done with `std::fs::remove_dir_all`.
970 /// If deletion is interrupted, the raw subdirectory may contain partial contents.
971 //
972 // In principle we could provide atomic deletion, but it would lead to instances
973 // that were in "limbo": they exist, but wouldn't appear in list_instances,
974 // and the deletion would need to be completed next time they were acquired
975 // (or during a purge_instances run).
976 //
977 // In practice we expect that callers will not try to use a partially-deleted instance,
978 // and that if they do they will fail with a "state corrupted" error, which would be fine.
979 pub fn purge(self) -> Result<()> {
980 let dir = self.dir.as_path();
981
982 (|| {
983 // use Arc::into_inner on the lock object,
984 // to make sure we're actually the only surviving InstanceStateHandle
985 let flock_guard = Arc::into_inner(self.flock_guard).ok_or_else(|| {
986 bad_api_usage!(
987 "InstanceStateHandle::purge called for {:?}, but other clones of the handle exist",
988 self.dir.as_path(),
989 )
990 })?;
991
992 trace!("purging {:?} (and {})", dir, DOT_LOCK);
993 fs::remove_dir_all(dir)?;
994 flock_guard.delete_lock_file(
995 // dir.with_extension is right because the last component of dir
996 // is KIND+ID which doesn't contain `.` so no extension will be stripped
997 dir.with_extension(LOCK_EXTN),
998 )?;
999
1000 Ok::<_, ErrorSource>(())
1001 })()
1002 .map_err(|source| {
1003 Error::new(
1004 source,
1005 Action::Deleting,
1006 Resource::Directory { dir: dir.into() },
1007 )
1008 })
1009 }
1010}
1011
1012/// Touch an instance the state directory, `dir`, for expiry purposes
1013fn touch_instance_dir(dir: &CheckedDir) -> Result<()> {
1014 let dir = dir.as_path();
1015 let resource = || Resource::Directory { dir: dir.into() };
1016
1017 let mtime = filetime::FileTime::from_system_time(SystemTime::get());
1018 filetime::set_file_mtime(dir, mtime)
1019 .map_err(|source| Error::new(source, Action::Initializing, resource()))
1020}
1021
1022/// A place in the state or cache directory, where we can load/store a serialisable type
1023///
1024/// Implies exclusive access.
1025///
1026/// Rust mutability-xor-sharing rules enforce proper synchronisation,
1027/// unless multiple `StorageHandle`s are created
1028/// using the same [`InstanceStateHandle`] and key.
1029#[derive(Deftly, Debug)] // not Clone, to enforce mutability rules (see above)
1030#[derive_deftly(ContainsInstanceStateGuard)]
1031pub struct StorageHandle<T> {
1032 /// The directory and leafname
1033 instance_dir: CheckedDir,
1034 /// `KEY.json`
1035 leafname: String,
1036 /// We can load and store a `T`.
1037 ///
1038 /// Invariant in `T`. But we're `Sync` and `Send` regardless of `T`.
1039 /// (From the Table of PhantomData patterns in the Nomicon.)
1040 marker: PhantomData<fn(T) -> T>,
1041 /// Clone of the InstanceStateHandle's lock
1042 flock_guard: Arc<LockFileGuard>,
1043}
1044
1045// Like tor_persist, but writing needs `&mut`
1046impl<T: Serialize + DeserializeOwned> StorageHandle<T> {
1047 /// Load this persistent state
1048 ///
1049 /// `None` means the state was most recently [`delete`](StorageHandle::delete)ed
1050 pub fn load(&self) -> Result<Option<T>> {
1051 self.with_load_store_target(Action::Loading, |t| t.load())
1052 }
1053 /// Store this persistent state
1054 pub fn store(&mut self, v: &T) -> Result<()> {
1055 // The renames will cause a directory mtime update
1056 self.with_load_store_target(Action::Storing, |t| t.store(v))
1057 }
1058 /// Delete this persistent state
1059 pub fn delete(&mut self) -> Result<()> {
1060 // Only counts as a recent modification if this state *did* exist
1061 self.with_load_store_target(Action::Deleting, |t| t.delete())
1062 }
1063
1064 /// Operate using a `load_store::Target`
1065 fn with_load_store_target<R, F>(&self, action: Action, f: F) -> Result<R>
1066 where
1067 F: FnOnce(load_store::Target<'_>) -> std::result::Result<R, ErrorSource>,
1068 {
1069 f(load_store::Target {
1070 dir: &self.instance_dir,
1071 rel_fname: self.leafname.as_ref(),
1072 })
1073 .map_err(self.map_err(action))
1074 }
1075
1076 /// Helper to convert an `ErrorSource` to an `Error`, if we were performing `action`
1077 fn map_err(&self, action: Action) -> impl FnOnce(ErrorSource) -> Error + use<T> {
1078 let resource = self.err_resource();
1079 move |source| crate::Error::new(source, action, resource)
1080 }
1081
1082 /// Return the proper `Resource` for reporting errors
1083 fn err_resource(&self) -> Resource {
1084 Resource::File {
1085 // TODO ideally we would remember what proportion of instance_dir
1086 // came from the original state_dir, so we can put state_dir in the container
1087 container: self.instance_dir.as_path().to_owned(),
1088 file: self.leafname.clone().into(),
1089 }
1090 }
1091}
1092
1093/// Subdirectory within an instance's state, for raw filesystem operations
1094///
1095/// Dereferences to `fs_mistrust::CheckedDir` and can be used mostly like one.
1096/// Obtained from [`InstanceStateHandle::raw_subdir`].
1097///
1098/// Existence of this value implies exclusive access to the instance.
1099///
1100/// If you need to manage the lock, and the directory path, separately,
1101/// [`raw_lock_guard`](ContainsInstanceStateGuard::raw_lock_guard)
1102/// will help.
1103#[derive(Deref, Clone, Debug, Deftly)]
1104#[derive_deftly(ContainsInstanceStateGuard)]
1105pub struct InstanceRawSubdir {
1106 /// The actual directory, as a [`fs_mistrust::CheckedDir`]
1107 #[deref]
1108 dir: CheckedDir,
1109 /// Clone of the InstanceStateHandle's lock
1110 flock_guard: Arc<LockFileGuard>,
1111}
1112
1113#[cfg(all(test, not(miri) /* filesystem access */))]
1114mod test {
1115 // @@ begin test lint list maintained by maint/add_warning @@
1116 #![allow(clippy::bool_assert_comparison)]
1117 #![allow(clippy::clone_on_copy)]
1118 #![allow(clippy::dbg_macro)]
1119 #![allow(clippy::mixed_attributes_style)]
1120 #![allow(clippy::print_stderr)]
1121 #![allow(clippy::print_stdout)]
1122 #![allow(clippy::single_char_pattern)]
1123 #![allow(clippy::unwrap_used)]
1124 #![allow(clippy::unchecked_time_subtraction)]
1125 #![allow(clippy::useless_vec)]
1126 #![allow(clippy::needless_pass_by_value)]
1127 //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
1128
1129 use super::*;
1130 use derive_deftly::{Deftly, derive_deftly_adhoc};
1131 use itertools::{Itertools, iproduct};
1132 use serde::{Deserialize, Serialize};
1133 use std::collections::BTreeSet;
1134 use std::fmt::Display;
1135 use std::fs::File;
1136 use std::io;
1137 use std::str::FromStr;
1138 use test_temp_dir::test_temp_dir;
1139 use tor_basic_utils::PathExt as _;
1140 use tor_error::HasKind as _;
1141 use tracing_test::traced_test;
1142 use web_time_compat::SystemTimeExt;
1143
1144 use tor_error::ErrorKind as TEK;
1145
1146 type AgeDays = i8;
1147
1148 fn days(days: AgeDays) -> Duration {
1149 Duration::from_secs(86400 * u64::try_from(days).unwrap())
1150 }
1151
1152 fn now() -> SystemTime {
1153 SystemTime::get()
1154 }
1155
1156 struct Garlic(Slug);
1157
1158 impl InstanceIdentity for Garlic {
1159 fn kind() -> &'static str {
1160 "garlic"
1161 }
1162 fn write_identity(&self, f: &mut fmt::Formatter) -> fmt::Result {
1163 Display::fmt(&self.0, f)
1164 }
1165 }
1166
1167 #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
1168 struct StoredData {
1169 some_value: i32,
1170 }
1171
1172 fn mk_state_dir(dir: &Path) -> StateDirectory {
1173 StateDirectory::new(
1174 dir,
1175 &fs_mistrust::Mistrust::new_dangerously_trust_everyone(),
1176 )
1177 .unwrap()
1178 }
1179
1180 #[test]
1181 #[traced_test]
1182 fn test_api() {
1183 test_temp_dir!().used_by(|dir| {
1184 let sd = mk_state_dir(dir);
1185
1186 let garlic = Garlic("wild".try_into_slug().unwrap());
1187
1188 let acquire_instance = || sd.acquire_instance(&garlic);
1189
1190 let ih = acquire_instance().unwrap();
1191 let inst_path = dir.join("garlic/wild");
1192 assert!(fs::metadata(&inst_path).unwrap().is_dir());
1193
1194 assert_eq!(
1195 acquire_instance().unwrap_err().kind(),
1196 TEK::LocalResourceAlreadyInUse,
1197 );
1198
1199 let irsd = ih.raw_subdir("raw").unwrap();
1200 assert!(fs::metadata(irsd.as_path()).unwrap().is_dir());
1201 assert_eq!(irsd.as_path(), dir.join("garlic").join("wild").join("raw"));
1202
1203 let mut sh = ih.storage_handle::<StoredData>("stored_data").unwrap();
1204 let storage_path = dir.join("garlic/wild/stored_data.json");
1205
1206 let peek = || sd.instance_peek_storage(&garlic, "stored_data");
1207
1208 let expect_load = |sh: &StorageHandle<_>, expect| {
1209 let check_loaded = |what, loaded: Result<Option<StoredData>>| {
1210 assert_eq!(loaded.unwrap().as_ref(), expect, "{what}");
1211 };
1212 check_loaded("load", sh.load());
1213 check_loaded("peek", peek());
1214 };
1215
1216 expect_load(&sh, None);
1217
1218 let to_store = StoredData { some_value: 42 };
1219 sh.store(&to_store).unwrap();
1220 assert!(fs::metadata(storage_path).unwrap().is_file());
1221
1222 expect_load(&sh, Some(&to_store));
1223
1224 sh.delete().unwrap();
1225
1226 expect_load(&sh, None);
1227
1228 drop(sh);
1229 drop(irsd);
1230 ih.purge().unwrap();
1231
1232 assert_eq!(peek().unwrap(), None);
1233 assert_eq!(
1234 fs::metadata(&inst_path).unwrap_err().kind(),
1235 io::ErrorKind::NotFound
1236 );
1237 });
1238 }
1239
1240 #[test]
1241 #[traced_test]
1242 #[allow(clippy::comparison_chain)]
1243 #[allow(clippy::expect_fun_call)]
1244 fn test_iter() {
1245 // Tests list_instances and purge_instances.
1246 //
1247 // 1. Set up a single state directory containing a number of instances,
1248 // enumerating all the possible situations that purge_instance might find.
1249 // The instance is identified by a `Which` which specifies its properties,
1250 // and which is representable as the instance id slug.
1251 // 1b. Put some junk in the state directory too, that we expect to be ignored.
1252 //
1253 // 2. Call list_instances and check that we see what we expect.
1254 //
1255 // 3. Call purge_instances and check that all the callbacks happen as we expect.
1256 //
1257 // 4. Call list_instances again and check that we see what we now expect.
1258 //
1259 // 5. Check that the junk is still present.
1260
1261 let temp_dir = test_temp_dir!();
1262 let state_dir = temp_dir.used_by(mk_state_dir);
1263
1264 /// Reified test case spec for expiry
1265 //
1266 // For non-`bool` fields, `#[deftly(test = )]` gives the set of values to test.
1267 #[derive(Deftly, Eq, PartialEq, Debug)]
1268 #[derive_deftly_adhoc]
1269 struct Which {
1270 /// Does `name_filter` return `Live`?
1271 namefilter_live: bool,
1272 /// What is the oldest does `age_filter` will return `Live` for?
1273 #[deftly(test = "0, 2")]
1274 max_age: AgeDays,
1275 /// How long ago was the instance dir actually modified?
1276 #[deftly(test = "-1, 1, 3")]
1277 age: AgeDays,
1278 /// Does the instance dir exist?
1279 dir: bool,
1280 /// Does the instance !lockfile exist?
1281 lockfile: bool,
1282 }
1283
1284 /// Ad-hoc (de)serialisation scheme of `Which` as an instance id (a `Slug`)
1285 ///
1286 /// The serialisation is `n<namefilter_live>_m<max_age>_...`,
1287 /// ie, for each field, the initial letter of its name, followed by the value.
1288 /// (We don't bother suppressing the trailiong `_`).
1289 impl Display for Which {
1290 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1291 derive_deftly_adhoc! {
1292 Which:
1293 $(
1294 write!(
1295 f, "{}{}_",
1296 stringify!($fname).chars().next().unwrap(),
1297 self.$fname,
1298 )?;
1299 )
1300 }
1301 Ok(())
1302 }
1303 }
1304 impl FromStr for Which {
1305 type Err = Error;
1306 fn from_str(s: &str) -> Result<Self> {
1307 let mut fields = s.split('_');
1308 derive_deftly_adhoc! {
1309 Which:
1310 Ok(Which { $(
1311 $fname: fields.next().unwrap()
1312 .split_at(1).1
1313 .parse().unwrap(),
1314 )})
1315 }
1316 }
1317 }
1318
1319 impl InstanceIdentity for Which {
1320 fn kind() -> &'static str {
1321 "which"
1322 }
1323 fn write_identity(&self, f: &mut fmt::Formatter) -> fmt::Result {
1324 Display::fmt(self, f)
1325 }
1326 }
1327
1328 // 0. Calculate all possible whiches
1329
1330 let whiches = {
1331 derive_deftly_adhoc!(
1332 Which:
1333 iproduct!(
1334 $(
1335 ${if fmeta(test) { [ ${fmeta(test) as token_stream} ] }
1336 else { [false, true] }},
1337 )
1338 // iproduct hates a trailing comma, so add a dummy element
1339 // https://github.com/rust-itertools/itertools/issues/868
1340 [()]
1341 )
1342 )
1343 .map(derive_deftly_adhoc!(
1344 Which:
1345 //
1346 |($( $fname, ) ())| Which { $( $fname, ) }
1347 ))
1348 // if you want to debug one test case, you can do this:
1349 // .filter(|wh| wh.to_string() == "nfalse_r2_a3_lfalse_dtrue_")
1350 .collect_vec()
1351 };
1352
1353 // 1. Create all the test instances, according to the specifications
1354
1355 for which in &whiches {
1356 let s = which.to_string();
1357 println!("{s}");
1358 assert_eq!(&s.parse::<Which>().unwrap(), which);
1359
1360 let inst = state_dir.acquire_instance(which).unwrap();
1361
1362 if !which.dir {
1363 fs::remove_dir_all(inst.dir.as_path()).unwrap();
1364 } else {
1365 let now = now();
1366 let set_mtime = |mtime: SystemTime| {
1367 filetime::set_file_mtime(inst.dir.as_path(), mtime.into()).unwrap();
1368 };
1369 if which.age > 0 {
1370 set_mtime(now - days(which.age));
1371 } else if which.age < 0 {
1372 set_mtime(now + days(-which.age));
1373 };
1374 }
1375
1376 if !which.lockfile {
1377 let lock_path = inst.dir.as_path().with_extension(LOCK_EXTN);
1378 let flock_guard = Arc::into_inner(inst.flock_guard).unwrap();
1379 flock_guard
1380 .delete_lock_file(&lock_path)
1381 .expect(&lock_path.display_lossy().to_string());
1382 }
1383 }
1384
1385 // 1b. Create some junk that should be ignored
1386
1387 let junk = {
1388 let mut junk = Vec::new();
1389 let base = state_dir.dir.as_path();
1390 for rhs in ["+bad", &format!("+bad{DOT_LOCK}"), ".tmp"] {
1391 let mut mk = |lhs, is_dir| {
1392 let p = base.join(format!("{lhs}{rhs}"));
1393 junk.push((p.clone(), is_dir));
1394 p
1395 };
1396 File::create(mk("file", false)).unwrap();
1397 fs::create_dir(mk("dir", true)).unwrap();
1398 }
1399 junk
1400 };
1401
1402 // 2. Check that we see the ones we expect
1403
1404 let list_instances = || {
1405 state_dir
1406 .list_instances::<Which>()
1407 .map(Result::unwrap)
1408 .collect::<BTreeSet<_>>()
1409 };
1410
1411 let found = list_instances();
1412
1413 let expected: BTreeSet<_> = whiches
1414 .iter()
1415 .filter(|which| which.dir || which.lockfile)
1416 .map(|which| Slug::new(which.to_string()).unwrap())
1417 .collect();
1418
1419 itertools::assert_equal(&found, &expected);
1420
1421 // 3. Run a purge and check that we see the expected callbacks
1422
1423 struct PurgeHandler<'r> {
1424 expected: &'r BTreeSet<Slug>,
1425 }
1426
1427 impl Which {
1428 fn old_enough_to_vanish(&self) -> bool {
1429 self.age > self.max_age
1430 }
1431 }
1432
1433 impl InstancePurgeHandler for PurgeHandler<'_> {
1434 fn kind(&self) -> &'static str {
1435 "which"
1436 }
1437 fn name_filter(&mut self, id: &SlugRef) -> Result<Liveness> {
1438 eprintln!("{id} - name_filter");
1439 assert!(self.expected.contains(id));
1440 let which: Which = id.as_str().parse().unwrap();
1441 Ok(if which.namefilter_live {
1442 Liveness::Live
1443 } else {
1444 Liveness::PossiblyUnused
1445 })
1446 }
1447 fn age_filter(&mut self, id: &SlugRef, age: Duration) -> Result<Liveness> {
1448 eprintln!("{id} - age_filter({age:?})");
1449 let which: Which = id.as_str().parse().unwrap();
1450 assert!(!which.namefilter_live);
1451 Ok(if age <= days(which.max_age) {
1452 Liveness::Live
1453 } else {
1454 Liveness::PossiblyUnused
1455 })
1456 }
1457 fn dispose(
1458 &mut self,
1459 info: &InstancePurgeInfo,
1460 handle: InstanceStateHandle,
1461 ) -> Result<()> {
1462 let id = info.identity();
1463 eprintln!("{id} - dispose");
1464 let which: Which = id.as_str().parse().unwrap();
1465 assert!(!which.namefilter_live);
1466 assert!(which.old_enough_to_vanish());
1467 assert!(which.dir);
1468 handle.purge()
1469 }
1470 }
1471
1472 state_dir
1473 .purge_instances(
1474 now(),
1475 &mut PurgeHandler {
1476 expected: &expected,
1477 },
1478 )
1479 .unwrap();
1480
1481 // 4. List the instances again and check the results
1482
1483 let found = list_instances();
1484
1485 let expected: BTreeSet<_> = whiches
1486 .iter()
1487 .filter(|which| {
1488 if which.namefilter_live {
1489 // things filtered by the name filter are left alone;
1490 // we see them if any bits of them existed, even a stale lockfile
1491 which.dir || which.lockfile
1492 } else {
1493 // things *not* filtered by the name filter are retained
1494 // iff the directory exists and is new enough
1495 which.dir && !which.old_enough_to_vanish()
1496 }
1497 })
1498 .map(|which| Slug::new(which.to_string()).unwrap())
1499 .collect();
1500
1501 itertools::assert_equal(&found, &expected);
1502
1503 // 5. Check that the junk was ignored
1504
1505 for (p, is_dir) in junk {
1506 let md = fs::metadata(&p).unwrap();
1507 assert_eq!(md.is_dir(), is_dir, "{}", p.display_lossy());
1508 }
1509 }
1510
1511 #[test]
1512 #[traced_test]
1513 fn test_reset_expiry() {
1514 // Tests that things that should update the instance mtime do so,
1515 // and that things that shouldn't, don't.
1516 //
1517 // For each test case, we:
1518 // 1. create a new subdirectory of our temp dir, making a new StateDirectory.
1519 // 2. (optionally) set up one instance within it, containing one pre-prepared
1520 // existing storage file and one pre-prepared (empty) raw subdir
1521 // 3. perform test-case specific actions on the instance
1522 // 4. run a stunt `purge_instances` call that merely checks
1523 // that the right value was passed to age_filter
1524
1525 let temp_dir = test_temp_dir!();
1526
1527 const KIND: &str = "kind";
1528
1529 // keys for various sub-objects
1530 const S_EXISTS: &str = "state-existing";
1531 const S_ABSENT: &str = "state-initially-absent";
1532 const R_EXISTS: &str = "raw-subdir-existing";
1533 const R_ABSENT: &str = "raw-subdir-initially-absent";
1534
1535 struct FixedId;
1536 impl InstanceIdentity for FixedId {
1537 fn kind() -> &'static str {
1538 KIND
1539 }
1540 fn write_identity(&self, f: &mut fmt::Formatter) -> fmt::Result {
1541 write!(f, "id")
1542 }
1543 }
1544
1545 /// Did we expect this test case's actions to change the mtime?
1546 #[derive(PartialEq, Debug)]
1547 enum Expect {
1548 /// mtime should be updated
1549 New,
1550 /// mtime should be unchanged
1551 Old,
1552 }
1553 use Expect as Ex;
1554
1555 /// Callbacks for stunt purge
1556 ///
1557 /// `self == None` means we've called `age_filter` already.
1558 #[allow(non_local_definitions)] // rust-lang/rust#125068
1559 impl InstancePurgeHandler for Option<&'_ Expect> {
1560 fn kind(&self) -> &'static str {
1561 KIND
1562 }
1563 fn name_filter(&mut self, _identity: &SlugRef) -> Result<Liveness> {
1564 Ok(Liveness::PossiblyUnused)
1565 }
1566 fn age_filter(&mut self, _identity: &SlugRef, age: Duration) -> Result<Liveness> {
1567 let did_reset = if age < days(1) { Ex::New } else { Ex::Old };
1568 assert_eq!(&did_reset, self.unwrap());
1569 *self = None;
1570 // Stop processing the instance
1571 Ok(Liveness::Live)
1572 }
1573 fn dispose(
1574 &mut self,
1575 _info: &InstancePurgeInfo<'_>,
1576 _handle: InstanceStateHandle,
1577 ) -> Result<()> {
1578 panic!("disposed live")
1579 }
1580 }
1581
1582 /// Helper for test that purge iteration doesn't itself update the mtime
1583 ///
1584 /// Says `PossiblyUnused` so that `dispose` gets called,
1585 /// but then just drops the handle and doesn't delete.
1586 struct ExamineAll;
1587 impl InstancePurgeHandler for ExamineAll {
1588 fn kind(&self) -> &'static str {
1589 KIND
1590 }
1591 fn name_filter(&mut self, _identity: &SlugRef) -> Result<Liveness> {
1592 Ok(Liveness::PossiblyUnused)
1593 }
1594 fn age_filter(&mut self, _identity: &SlugRef, _age: Duration) -> Result<Liveness> {
1595 Ok(Liveness::PossiblyUnused)
1596 }
1597 fn dispose(
1598 &mut self,
1599 _info: &InstancePurgeInfo<'_>,
1600 _handle: InstanceStateHandle,
1601 ) -> Result<()> {
1602 Ok(())
1603 }
1604 }
1605
1606 // Run a check (raw - doesn't creating an initial instance state)
1607 let chk_without_create = |exp: Expect, which: &str, acts: &dyn Fn(&StateDirectory)| {
1608 temp_dir.subdir_used_by(which, |dir| {
1609 let state_dir = mk_state_dir(&dir);
1610 acts(&state_dir);
1611
1612 let mut exp = Some(&exp);
1613 state_dir.purge_instances(now(), &mut exp).unwrap();
1614 assert!(exp.is_none(), "age_filter not called, instance missing?");
1615 });
1616 };
1617
1618 // Run a check with a prepared instance state
1619 //
1620 // The prepared instance:
1621 // - has an existing storage at key S_EXISTS
1622 // - has an existing empty raw subdir at key R_EXISTS
1623 // - has been acquired, so `acts` gets an handle
1624 // - but all of this (looks like it) happened 2 days ago
1625 let chk =
1626 |exp: Expect, which: &str, acts: &dyn Fn(&StateDirectory, InstanceStateHandle)| {
1627 chk_without_create(exp, which, &|state_dir| {
1628 let inst = state_dir.acquire_instance(&FixedId).unwrap();
1629
1630 inst.storage_handle(S_EXISTS)
1631 .unwrap()
1632 .store(&StoredData { some_value: 1 })
1633 .unwrap();
1634 inst.raw_subdir(R_EXISTS).unwrap();
1635
1636 let mtime = now() - days(2);
1637 filetime::set_file_mtime(inst.dir.as_path(), mtime.into()).unwrap();
1638
1639 acts(state_dir, inst);
1640 });
1641 };
1642
1643 // Test things that shouldn't count for keeping an instance alive
1644
1645 chk(Ex::Old, "just-releasing-acquired", &|_, inst| {
1646 drop(inst);
1647 });
1648 chk(Ex::Old, "loading", &|_, inst| {
1649 let load = |key| {
1650 inst.storage_handle::<StoredData>(key)
1651 .unwrap()
1652 .load()
1653 .unwrap()
1654 };
1655 assert!(load(S_EXISTS).is_some());
1656 assert!(load(S_ABSENT).is_none());
1657 });
1658 chk(Ex::Old, "messing-in-subdir", &|_, inst| {
1659 // we don't have a raw subdir path here, but we know what it is
1660 let in_raw = inst.dir.as_path().join(R_EXISTS).join("new");
1661 let _: File = File::create(in_raw).unwrap();
1662 });
1663 chk(Ex::Old, "purge-iter-no-delete", &|state_dir, inst| {
1664 drop(inst);
1665 // ExamineAll looks at everything but never calls InstanceStateHandle::purge.
1666 // It it causes every instance to be locked, but not mtime-updated.
1667 state_dir.purge_instances(now(), &mut ExamineAll).unwrap();
1668 });
1669
1670 // Test things that *should* count for keeping an instance alive
1671
1672 chk_without_create(Ex::New, "acquire-new-instance", &|state_dir| {
1673 state_dir.acquire_instance(&FixedId).unwrap();
1674 });
1675 chk(Ex::New, "acquire-existing-instance", &|state_dir, inst| {
1676 drop(inst);
1677 state_dir.acquire_instance(&FixedId).unwrap();
1678 });
1679 for storage_key in [S_EXISTS, S_ABSENT] {
1680 chk(Ex::New, &format!("store-{}", storage_key), &|_, inst| {
1681 inst.storage_handle(storage_key)
1682 .unwrap()
1683 .store(&StoredData { some_value: 2 })
1684 .unwrap();
1685 });
1686 }
1687 for raw_dir in [R_EXISTS, R_ABSENT] {
1688 chk(Ex::New, &format!("raw_subdir-{}", raw_dir), &|_, inst| {
1689 let _: InstanceRawSubdir = inst.raw_subdir(raw_dir).unwrap();
1690 });
1691 }
1692 }
1693}