Skip to main content

gsym_cache/
lib.rs

1//! Concurrent, process-safe storage for immutable [LLVM GSYM] files.
2//!
3//! `gsym-cache` separates the profiler's read path from the machinery that
4//! creates and maintains cached files. A lookup takes no lock and performs no
5//! write. Optional features add debounced recency markers, nonblocking
6//! population ownership, verified atomic publication, negative caching, and
7//! bounded maintenance.
8//!
9//! Conversion, downloads, worker scheduling, and resource limits remain with
10//! the application.
11#![cfg_attr(
12    feature = "lookup",
13    doc = r#"
14# Quick start
15
16Open a cache without creating it, then look up a binary build identifier:
17
18```no_run
19use gsym_cache::{BuildId, Cache, CacheEpoch};
20
21# fn main() -> Result<(), Box<dyn std::error::Error>> {
22let cache = Cache::open("/var/cache/my-profiler/gsym", CacheEpoch::new(1))?;
23let build_id: BuildId = "1212121212121212121212121212121212121212".parse()?;
24if let Some(entry) = cache.lookup(&build_id)? {
25    println!("cached GSYM: {} bytes", entry.len());
26}
27# Ok(())
28# }
29```
30"#
31)]
32#![cfg_attr(
33    feature = "manage",
34    doc = r#"
35Population is an explicit, nonblocking state machine:
36
37```no_run
38use std::fs::File;
39use std::io;
40use gsym_cache::{BuildId, Cache, CacheEpoch, PopulationOutcome};
41
42# fn main() -> Result<(), Box<dyn std::error::Error>> {
43let cache = Cache::open("/var/cache/my-profiler/gsym", CacheEpoch::new(1))?;
44let build_id = BuildId::new([0x12; 20])?;
45
46match cache.try_begin_population(&build_id)? {
47    PopulationOutcome::Present(entry) => drop(entry),
48    PopulationOutcome::Acquired(population) => {
49        let mut source = File::open("artifact.gsym")?;
50        let mut writer = population.into_writer()?;
51        io::copy(&mut source, &mut writer)?;
52        drop(writer.publish()?.into_entry());
53    }
54    PopulationOutcome::Suppressed(failure) => {
55        eprintln!("retry after {:?}", failure.expires_at());
56    }
57    PopulationOutcome::Busy => eprintln!("another process owns population"),
58}
59# Ok(())
60# }
61```
62"#
63)]
64#![cfg_attr(
65    feature = "lookup",
66    doc = r#"
67# Choosing an entry point
68
69| To … | Use | Notes |
70| --- | --- | --- |
71| use an explicit cache root | `Cache::open` | suitable for services and privileged applications |
72| follow the XDG cache convention | `Cache::open_xdg` | validates the cache home and application component |
73| read an immutable entry | `Cache::lookup` | lock-free and deliberately does not decode GSYM |
74| record coarse recency | `Cache::record_access` | `access` feature; call only after a hit |
75| coordinate a cache miss | `Cache::try_begin_population` | `manage` feature; never waits |
76| enforce capacity or age limits | `Cache::prune` | `manage` feature; uses 80% low-watermark hysteresis |
77| verify and repair stored state | `Cache::scrub` | `manage` feature; checks complete GSYM files |
78
79# Usage notes
80
81* A [`BuildId`] is an opaque cache key, not proof that input is trustworthy.
82  Keep one private root per trust domain and isolate converters that consume
83  untrusted binaries.
84
85* [`CacheEpoch`] versions the application's conversion policy. Increment it
86  when the same build ID could produce different bytes. Old epochs are separate
87  namespaces and can be removed after their workers stop.
88
89* [`Cache::lookup`] verifies ownership and file type, but not GSYM contents.
90  Managed publication and scrubbing perform complete GSYM verification so the
91  profiler-facing path stays small.
92
93* Directory descriptors are pinned after they are opened. Create a new
94  [`Cache`] after replacing a cache namespace directory.
95
96* Durability and no-clobber behavior assume a private cache root on a local
97  Linux filesystem with advisory `flock`, atomic rename, and directory `fsync`
98  semantics.
99
100# Feature flags
101
102| Feature | Default | Adds |
103| --- | --- | --- |
104| `lookup` | yes | build IDs, cache opening, and lock-free lookup |
105| `access` | no | debounced access markers; implies `lookup` |
106| `manage` | no | population, negative caching, pruning, and scrubbing; implies `access` |
107
108Read-only use needs only the default feature:
109
110```toml
111[dependencies]
112gsym-cache = "0.1"
113```
114
115# Errors
116
117Fallible entry points return [`Result<T>`](Result). [`Error`] distinguishes I/O,
118unsafe directory or entry layouts, invalid GSYM, build-ID mismatch, and invalid
119negative-cache lifetimes. It is `#[non_exhaustive]`, so exhaustive matches need
120a fallback arm.
121
122Cache misses, active population, and active maintenance are ordinary outcomes,
123not errors.
124
125# Guides
126
127- [`docs::deployment`]: roots, epochs, trust boundaries, filesystems, and
128  process behavior.
129"#
130)]
131#![cfg_attr(
132    feature = "manage",
133    doc = r"- [`docs::operations`]: population, negative caching, access tracking, pruning, scrubbing, and crash recovery.
134- [`docs::cookbook`]: complete lookup, publication, suppression, and maintenance recipes.
135"
136)]
137//! [LLVM GSYM]: <https://llvm.org/doxygen/namespacellvm_1_1gsym.html>
138#![deny(unsafe_code)]
139#![warn(missing_docs)]
140#![warn(clippy::indexing_slicing, clippy::arithmetic_side_effects)]
141#![cfg_attr(docsrs, feature(doc_cfg))]
142
143#[cfg(not(target_os = "linux"))]
144compile_error!("gsym-cache currently supports Linux only");
145
146#[cfg(all(target_os = "linux", feature = "access"))]
147mod access;
148#[cfg(all(target_os = "linux", feature = "lookup"))]
149mod build_id;
150#[cfg(all(target_os = "linux", feature = "lookup"))]
151pub mod docs;
152#[cfg(target_os = "linux")]
153mod error;
154#[cfg(all(target_os = "linux", feature = "lookup"))]
155mod layout;
156#[cfg(all(target_os = "linux", feature = "lookup"))]
157mod lookup;
158#[cfg(all(target_os = "linux", feature = "manage"))]
159mod maintenance;
160#[cfg(all(target_os = "linux", feature = "manage"))]
161mod manage;
162
163#[cfg(all(target_os = "linux", feature = "access"))]
164#[cfg_attr(docsrs, doc(cfg(feature = "access")))]
165#[doc(inline)]
166pub use access::AccessUpdate;
167#[cfg(all(target_os = "linux", feature = "lookup"))]
168#[cfg_attr(docsrs, doc(cfg(feature = "lookup")))]
169#[doc(inline)]
170pub use build_id::{BuildId, BuildIdError};
171#[cfg(all(target_os = "linux", feature = "manage"))]
172#[cfg_attr(docsrs, doc(cfg(feature = "manage")))]
173#[doc(inline)]
174pub use error::{BuildIdMismatchError, InvalidGsymError};
175#[cfg(target_os = "linux")]
176pub use error::{Error, Result};
177#[cfg(all(target_os = "linux", feature = "lookup"))]
178#[cfg_attr(docsrs, doc(cfg(feature = "lookup")))]
179#[doc(inline)]
180pub use lookup::{Cache, CacheEntry, CacheEpoch};
181#[cfg(all(target_os = "linux", feature = "manage"))]
182#[cfg_attr(docsrs, doc(cfg(feature = "manage")))]
183#[doc(inline)]
184pub use maintenance::{
185    ByteLimit, CacheStats, EntryLimit, PruneOutcome, PrunePolicy, PruneReport, ScrubOutcome,
186    ScrubReport,
187};
188#[cfg(all(target_os = "linux", feature = "manage"))]
189#[cfg_attr(docsrs, doc(cfg(feature = "manage")))]
190#[doc(inline)]
191pub use manage::{
192    CachedFailure, FailureKind, MAX_FAILURE_TTL, Population, PopulationOutcome, PopulationWriter,
193    PublishOutcome,
194};
195
196/// Compiles the README's examples as doctests without rendering it twice.
197#[cfg(all(doctest, feature = "lookup"))]
198#[doc = include_str!("../README.md")]
199pub struct ReadmeDoctests;