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