Skip to main content

mbx_cache_core/agent/
wire.rs

1use super::{FileDigestResolution, FileDigestScope, FileIdentity, RecordedFileDigest};
2use crate::{ActionPrediction, CacheDigest, RemoteActionResult};
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5use std::path::PathBuf;
6
7/// Wire protocol version used between an in-process cache agent and its shims.
8pub const AGENT_PROTOCOL_VERSION: u8 = 11;
9/// Largest single protocol request the agent will read.
10///
11/// Requests are small JSON objects; the largest legitimate ones carry an output
12/// tree or a batch of digests, which stay far below this.
13pub(super) const MAX_REQUEST_BYTES: usize = 16 * 1024 * 1024;
14
15/// A file as a probe found it: absent, or present with a length and
16/// modification time. Length alone would miss a rewrite that kept the size.
17///
18/// Described by the shim at the moment it reads the file, not by the agent
19/// afterwards, so an executable replaced while its probe ran cannot be
20/// recorded under the replacement's identity.
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub struct PinnedFile {
23    /// The file, absent or present.
24    pub path: PathBuf,
25    /// What was there, or `None` for nothing.
26    pub state: Option<PinnedState>,
27}
28
29/// What a present pinned file looked like: enough to notice it being
30/// written, replaced, or made executable, without reading it.
31///
32/// Length and modification time alone would miss a replacement of the same
33/// length whose timestamp was preserved, and a `chmod +x`, which touches
34/// neither. So the inode says whether it is the same file, the change time,
35/// which the kernel sets on every write, rename and permission change and
36/// which no program can set back, says whether it was touched, and the mode
37/// says whether it can run. Windows has no change time or inode to offer
38/// through the standard library; its creation time and attributes stand in.
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub struct PinnedState {
41    /// Length in bytes.
42    pub len: u64,
43    /// Modification time, seconds since the Unix epoch.
44    pub modified_secs: u64,
45    /// Modification time, nanoseconds past `modified_secs`.
46    pub modified_nanos: u32,
47    /// Change time on Unix, creation time on Windows, seconds since the
48    /// Unix epoch.
49    pub changed_secs: u64,
50    /// Nanoseconds past `changed_secs`.
51    pub changed_nanos: u32,
52    /// The inode on Unix; zero on Windows.
53    pub inode: u64,
54    /// Permission bits on Unix, file attributes on Windows.
55    pub mode: u32,
56}
57
58impl PinnedFile {
59    /// Describe `path` as it is now, or nothing when the filesystem cannot
60    /// say enough about it to notice a change later.
61    pub fn describe(path: impl Into<PathBuf>) -> Option<Self> {
62        let path = path.into();
63        let state = match std::fs::metadata(&path) {
64            Ok(metadata) => Some(PinnedState::of(&metadata)?),
65            Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
66            Err(_) => return None,
67        };
68        Some(Self { path, state })
69    }
70
71    /// Whether the file is still as this pin describes it.
72    pub fn holds(&self) -> bool {
73        PinnedFile::describe(self.path.clone()).as_ref() == Some(self)
74    }
75}
76
77impl PinnedState {
78    #[cfg(unix)]
79    fn of(metadata: &std::fs::Metadata) -> Option<Self> {
80        use std::os::unix::fs::MetadataExt as _;
81        let modified = metadata
82            .modified()
83            .ok()?
84            .duration_since(std::time::SystemTime::UNIX_EPOCH)
85            .ok()?;
86        Some(Self {
87            len: metadata.len(),
88            modified_secs: modified.as_secs(),
89            modified_nanos: modified.subsec_nanos(),
90            changed_secs: u64::try_from(metadata.ctime()).ok()?,
91            changed_nanos: u32::try_from(metadata.ctime_nsec()).ok()?,
92            inode: metadata.ino(),
93            mode: metadata.mode(),
94        })
95    }
96
97    #[cfg(windows)]
98    fn of(metadata: &std::fs::Metadata) -> Option<Self> {
99        use std::os::windows::fs::MetadataExt as _;
100        let modified = metadata
101            .modified()
102            .ok()?
103            .duration_since(std::time::SystemTime::UNIX_EPOCH)
104            .ok()?;
105        let created = metadata
106            .created()
107            .ok()?
108            .duration_since(std::time::SystemTime::UNIX_EPOCH)
109            .ok()?;
110        Some(Self {
111            len: metadata.len(),
112            modified_secs: modified.as_secs(),
113            modified_nanos: modified.subsec_nanos(),
114            changed_secs: created.as_secs(),
115            changed_nanos: created.subsec_nanos(),
116            inode: 0,
117            mode: metadata.file_attributes(),
118        })
119    }
120
121    #[cfg(not(any(unix, windows)))]
122    fn of(_metadata: &std::fs::Metadata) -> Option<Self> {
123        None
124    }
125}
126
127/// A request accepted by the task-scoped cache agent.
128#[derive(Debug, Clone, Serialize, Deserialize)]
129#[serde(tag = "type", rename_all = "snake_case")]
130pub enum AgentRequest {
131    /// Negotiate protocol and application versions.
132    Hello {
133        /// Agent protocol version understood by the caller.
134        protocol: u8,
135        /// Human-readable mbx client version.
136        client_version: String,
137    },
138    /// Begin a prediction-manifest run for a stable Cargo or task identity.
139    BeginTask {
140        /// Stable 64-character lowercase hexadecimal task identity.
141        task: String,
142    },
143    /// Commit predictions collected by an earlier [`Self::BeginTask`].
144    CommitTask {
145        /// Opaque run identifier returned by the agent.
146        run: String,
147    },
148    /// Resolve a blob to a session-verified local CAS path.
149    FindBlob {
150        /// Blob to resolve.
151        digest: CacheDigest,
152    },
153    /// Resolve blobs to session-verified local CAS paths.
154    FindBlobs {
155        /// Blobs to resolve, preserving request order in the response.
156        digests: Vec<CacheDigest>,
157    },
158    /// Import a file into the local content-addressed store.
159    StoreBlob {
160        /// Digest the source must match.
161        digest: CacheDigest,
162        /// File to verify and import.
163        source: PathBuf,
164    },
165    /// Look up an action-result record.
166    FindActionResult {
167        /// Action digest to resolve.
168        action: CacheDigest,
169    },
170    /// Account for a successfully restored cache hit.
171    RecordActionHit {
172        /// Action that supplied the outputs.
173        action: CacheDigest,
174        /// Restoration work performed by the adapter.
175        restore: RestoreStats,
176        /// Compiler crate name, when the invocation supplied one.
177        crate_name: Option<String>,
178    },
179    /// A compilation the adapter declined to cache, grouped by reason.
180    RecordBypass {
181        /// Stable, low-cardinality bypass-reason name.
182        kind: String,
183    },
184    /// A compilation the adapter could not look up, having no key to look up
185    /// with. Distinct from a bypass: these are cached once compiled.
186    RecordUnconsulted,
187    /// Account for one real compiler invocation performed by the adapter.
188    RecordCompilerInvocation {
189        /// Stable outcome category: `miss`, `unconsulted`, `bypass` or
190        /// `verification`.
191        ///
192        /// A compilation the adapter deliberately ran with incremental state
193        /// instead of publishing reports `incremental-miss` or
194        /// `incremental-unconsulted`, saying both what its lookup did and that
195        /// its result was withheld. The agent counts those separately; a bare
196        /// `incremental` is no longer an outcome, because it could not say
197        /// whether the cache had been consulted.
198        outcome: String,
199        /// Compiler crate name, when the invocation supplied one.
200        crate_name: Option<String>,
201        /// Wall time spent running the compiler.
202        duration_ns: u64,
203    },
204    /// Account for a cache hit that was rebuilt for correctness verification.
205    RecordActionVerification {
206        /// Whether rebuilt and cached outputs matched.
207        matched: bool,
208        /// Restoration work performed before rebuilding.
209        restore: RestoreStats,
210    },
211    /// Store an action-result record locally and enqueue remote publication.
212    StoreActionResult {
213        /// Action-result record to store.
214        result: RemoteActionResult,
215    },
216    /// Find an earlier input prediction for a task and invocation.
217    FindActionPrediction {
218        /// Stable task identity.
219        task: String,
220        /// Digest of the compiler invocation without discovered inputs.
221        invocation: CacheDigest,
222    },
223    /// Record an input prediction after a successful compilation.
224    RecordActionPrediction {
225        /// Stable task identity.
226        task: String,
227        /// Adapter-owned prediction record.
228        prediction: ActionPrediction,
229    },
230    /// Find cached identity output for an executable and environment.
231    FindExecutableIdentity {
232        /// Executable whose identity command would run.
233        executable: PathBuf,
234        /// Environment variables affecting identity output.
235        environment: BTreeMap<String, Option<String>>,
236    },
237    /// Cache identity output for an executable and environment.
238    StoreExecutableIdentity {
239        /// Executable whose identity command ran.
240        executable: PathBuf,
241        /// Environment variables affecting identity output.
242        environment: BTreeMap<String, Option<String>>,
243        /// Captured identity-command standard output.
244        stdout: Vec<u8>,
245        /// Files the probe read, as they were when it read them, which pin
246        /// the output beyond this session so the next one can skip the
247        /// probe. Empty keeps the identity for this session only.
248        #[serde(default, skip_serializing_if = "Vec::is_empty")]
249        pins: Vec<PinnedFile>,
250    },
251    /// Surface a shim diagnostic through the session that owns the build.
252    ///
253    /// A shim must not print diagnostics itself: its stderr belongs to the
254    /// compiler it stands in for, and build scripts read that stream as part
255    /// of the compiler's answer -- cc-rs treats any stderr output from a flag
256    /// probe as "unsupported", which changes the flags of every compilation
257    /// that follows and, with them, every action key the build produces.
258    ///
259    /// Appended rather than grouped with the other `Record*` requests it
260    /// belongs beside: these variants carry no `repr`, so inserting one moves
261    /// the discriminant of every variant after it, and a break nobody asked
262    /// for is worth less than the grouping.
263    RecordWarning {
264        /// Human-readable single-line diagnostic.
265        message: String,
266    },
267    /// Find session-recorded digests for files with these identities.
268    FindFileDigests {
269        /// What the recorded digests may stand in for.
270        scope: FileDigestScope,
271        /// File identities to resolve, preserving request order in the
272        /// response.
273        files: Vec<FileIdentity>,
274    },
275    /// Record digests of files a shim hashed or wrote this session.
276    RecordFileDigests {
277        /// What the recorded digests may stand in for.
278        scope: FileDigestScope,
279        /// Hashed files and the identities their digests describe.
280        entries: Vec<RecordedFileDigest>,
281    },
282    /// Join or claim an invocation-wide promise through the cache server.
283    JoinActionPromise {
284        /// Adapter that owns the invocation and prediction payload.
285        adapter: String,
286        /// Digest of the compiler invocation before input discovery.
287        invocation: CacheDigest,
288    },
289    /// Fulfill a claimed promise after its action result is remotely durable.
290    CompleteActionPromise {
291        /// Opaque claim token returned by [`Self::JoinActionPromise`].
292        claim: String,
293        /// Prediction through which waiters reconstruct the final action key.
294        prediction: ActionPrediction,
295    },
296    /// Resolve file digests, coalescing concurrent reads through the agent.
297    ResolveFileDigests {
298        /// What the digest and any accompanying validation may stand in for.
299        scope: FileDigestScope,
300        /// File identities to resolve, preserving request order.
301        files: Vec<FileIdentity>,
302    },
303    /// Record exclusive wrapper phase durations and bounded trace spans.
304    RecordWrapperTiming {
305        /// Completed invocation timings.
306        timing: WrapperTiming,
307    },
308    /// Surface a fatal shim diagnostic through the session that owns the build.
309    ///
310    /// This is appended to preserve every existing request variant and wire
311    /// shape while allowing a caller to distinguish fatal acknowledgement from
312    /// [`Self::RecordWarning`] and fall back locally when it is not accepted.
313    RecordError {
314        /// Human-readable single-line diagnostic.
315        message: String,
316    },
317    /// Emit a routine shim log through the owning session's logger.
318    /// Filtering uses the original target; these records never consume the
319    /// warning/error allowance or enter compiler output.
320    RecordDebug {
321        /// Original logging module target.
322        target: String,
323        /// Human-readable single-line message.
324        message: String,
325    },
326}
327
328/// One wrapper invocation, independent of its cache hit/miss accounting.
329#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
330#[non_exhaustive]
331pub struct WrapperTiming {
332    /// Compiler adapter, such as rustc or cc.
333    pub adapter: String,
334    /// Crate or source name, when known.
335    pub unit: Option<String>,
336    /// Wrapper process, used as the trace lane.
337    pub pid: u32,
338    /// Wall-clock start in microseconds since the Unix epoch.
339    pub start_us: u64,
340    /// Monotonic elapsed time, excluding telemetry delivery.
341    pub duration_ns: u64,
342    /// Exclusive durations; nested work is subtracted from its parent.
343    pub phases_ns: std::collections::BTreeMap<String, u64>,
344    /// Nested spans relative to this wrapper's start, capped at 512.
345    pub spans: Vec<WrapperSpan>,
346    /// The build unit this invocation produced, when it can be identified.
347    #[serde(default, skip_serializing_if = "Option::is_none")]
348    pub unit_id: Option<String>,
349    /// Units whose outputs this invocation consumed, by [`Self::unit_id`].
350    #[serde(default, skip_serializing_if = "Vec::is_empty")]
351    pub dependencies: Vec<String>,
352}
353
354/// A trace interval inside one wrapper process.
355#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
356#[non_exhaustive]
357pub struct WrapperSpan {
358    /// Phase name.
359    pub name: String,
360    /// Monotonic offset from wrapper entry.
361    pub start_ns: u64,
362    /// Inclusive elapsed time.
363    pub duration_ns: u64,
364}
365
366/// Local output restoration work performed by one action-cache adapter hit.
367#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
368#[serde(deny_unknown_fields)]
369pub struct RestoreStats {
370    /// Cumulative time spent materializing and validating output files.
371    pub duration_ns: u64,
372    /// Compiler wall time recorded when this action was originally produced.
373    /// Zero means no timing hint was available.
374    pub avoided_compiler_duration_ns: u64,
375    /// Number of compiler output files restored.
376    pub output_files: u64,
377    /// Declared size of compiler output files restored.
378    pub output_bytes: u64,
379    /// Number of restored output files that share data blocks with the CAS.
380    pub reflinked_output_files: u64,
381    /// Declared size of restored outputs that share data blocks with the CAS.
382    pub reflinked_output_bytes: u64,
383    /// Number of restored output files that share an inode with the CAS blob.
384    pub hardlinked_output_files: u64,
385    /// Declared size of restored outputs that share an inode with the CAS blob.
386    pub hardlinked_output_bytes: u64,
387    /// Number of restored output files that required a byte-for-byte copy.
388    pub copied_output_files: u64,
389    /// Declared size of restored outputs that required a byte-for-byte copy.
390    pub copied_output_bytes: u64,
391    /// Number of output files already in place with the cached contents, kept
392    /// rather than rewritten.
393    pub reused_output_files: u64,
394    /// Declared size of outputs kept in place rather than rewritten.
395    pub reused_output_bytes: u64,
396}
397
398/// One accounted cache decision, as it happens.
399///
400/// The agent already folds every one of these into [`AgentStats`]; an observer
401/// sees the same decisions individually, before that summing loses the crate
402/// they belong to. Delivered synchronously from the request handler, so an
403/// observer that blocks slows the build it is watching.
404#[derive(Debug, Clone)]
405#[non_exhaustive]
406pub enum AgentEvent {
407    /// An invocation completed its wrapper instrumentation.
408    WrapperTiming {
409        /// Completed invocation timings.
410        timing: WrapperTiming,
411    },
412    /// An action's outputs were restored from cache.
413    ActionHit {
414        /// Compiler crate name, when the invocation supplied one.
415        crate_name: Option<String>,
416        /// Restoration work performed by the adapter.
417        restore: RestoreStats,
418    },
419    /// A compilation the adapter declined to cache.
420    Bypass {
421        /// Stable, low-cardinality bypass-reason name.
422        kind: String,
423    },
424    /// A compilation no lookup was possible for.
425    Unconsulted,
426    /// A compilation the adapter declined to cache, and the compiler run that
427    /// followed.
428    ///
429    /// Emitted in place of [`Self::Bypass`] and a `bypass`
430    /// [`Self::CompilerInvocation`] when a shim reports both over one
431    /// connection, so the reason arrives with the crate and time it cost.
432    BypassedCompilation {
433        /// Stable, low-cardinality bypass-reason name.
434        kind: String,
435        /// Compiler crate name, when the invocation supplied one.
436        crate_name: Option<String>,
437        /// Wall time spent running the compiler.
438        duration_ns: u64,
439    },
440    /// A real compiler invocation ran.
441    CompilerInvocation {
442        /// Stable outcome category such as `miss`, `unconsulted`, or `bypass`.
443        outcome: String,
444        /// Compiler crate name, when the invocation supplied one.
445        crate_name: Option<String>,
446        /// Wall time spent running the compiler.
447        duration_ns: u64,
448    },
449    /// A hit was rebuilt to verify it.
450    Verification {
451        /// Whether rebuilt and cached outputs matched.
452        matched: bool,
453        /// Restoration work performed before rebuilding.
454        restore: RestoreStats,
455    },
456    /// A shim reported a diagnostic for the session to surface.
457    Warning {
458        /// Human-readable single-line diagnostic.
459        message: String,
460    },
461    /// A shim reported a fatal diagnostic for the session to surface.
462    Error {
463        /// Human-readable single-line diagnostic.
464        message: String,
465    },
466    /// Cache-key material for the action event immediately following it.
467    ActionDiagnostic {
468        /// Outcome of the action this describes.
469        outcome: String,
470        /// Compiler crate name, when the invocation supplied one.
471        crate_name: Option<String>,
472        /// Privacy-preserving action-key decomposition.
473        diagnostic: ActionDiagnostic,
474    },
475}
476
477/// A privacy-preserving decomposition of an action key.
478///
479/// Values are content digests rather than source or environment contents. The
480/// names are enough to say what changed without copying secrets into session
481/// history.
482#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
483pub struct ActionDiagnostic {
484    /// Complete action-cache key.
485    pub action: CacheDigest,
486    /// Non-file key components, named for display.
487    pub components: BTreeMap<String, CacheDigest>,
488    /// Normalized input path to content digest.
489    pub inputs: BTreeMap<String, CacheDigest>,
490}
491
492/// A sink for [`AgentEvent`]s observed during one session.
493pub trait AgentEventObserver: Send + Sync {
494    /// Handle one event. Must not panic, and should not block.
495    fn event(&self, event: AgentEvent);
496}
497
498/// A response returned by the task-scoped cache agent.
499#[derive(Debug, Clone, Serialize, Deserialize)]
500#[serde(tag = "type", rename_all = "snake_case")]
501pub enum AgentResponse {
502    /// Successful protocol negotiation.
503    Hello {
504        /// Agent protocol version.
505        protocol: u8,
506        /// Human-readable agent version.
507        agent_version: String,
508    },
509    /// A task prediction run was begun.
510    TaskBegun {
511        /// Opaque run identifier to pass to compiler shims and commit later.
512        run: String,
513    },
514    /// A task prediction run was committed.
515    TaskCommitted,
516    /// A local CAS path already verified against the requested digest.
517    Blob {
518        /// Verified local path, or `None` on a cache miss.
519        path: Option<PathBuf>,
520    },
521    /// Local CAS paths already verified against the requested digests.
522    Blobs {
523        /// Verified local paths or misses, in request order.
524        paths: Vec<Option<PathBuf>>,
525    },
526    /// A blob was stored locally.
527    Stored {
528        /// Path of the stored object in the local CAS.
529        path: PathBuf,
530    },
531    /// Result of an action lookup.
532    ActionResult {
533        /// Validated action result, or `None` on a cache miss.
534        result: Option<RemoteActionResult>,
535    },
536    /// Hit statistics were updated.
537    ActionHitRecorded,
538    /// Verification statistics were updated.
539    ActionVerificationRecorded,
540    /// Bypass statistics were updated.
541    BypassRecorded,
542    /// Unconsulted-compilation statistics were updated.
543    UnconsultedRecorded,
544    /// Compiler invocation accounting was recorded.
545    CompilerInvocationRecorded,
546    /// An action result was stored.
547    ActionStored {
548        /// Path of the stored local action-result record.
549        path: PathBuf,
550    },
551    /// Result of an input-prediction lookup.
552    ActionPrediction {
553        /// Matching prediction, or `None` when none is known.
554        prediction: Option<ActionPrediction>,
555    },
556    /// An input prediction was recorded.
557    ActionPredictionRecorded,
558    /// Result of an executable-identity lookup.
559    ExecutableIdentity {
560        /// Captured output, or `None` when no identity is cached.
561        stdout: Option<Vec<u8>>,
562    },
563    /// The request failed without terminating the agent connection.
564    Error {
565        /// Human-readable failure description.
566        message: String,
567    },
568    /// A shim diagnostic was accepted for the session to surface.
569    ///
570    /// Kept after `Error` so this acknowledgement remains alongside the
571    /// existing diagnostic response without changing earlier variants.
572    WarningRecorded,
573    /// Digests recorded earlier for the requested file identities.
574    FileDigests {
575        /// Recorded digests or misses, in request order.
576        digests: Vec<Option<CacheDigest>>,
577    },
578    /// File digests were recorded.
579    FileDigestsRecorded,
580    /// State of an optional server-wide compilation promise.
581    ActionPromise {
582        /// Opaque lease owned by this client, when it should compile.
583        claim: Option<String>,
584        /// Completed prediction, when another client compiled first.
585        prediction: Option<ActionPrediction>,
586    },
587    /// A server-wide compilation promise was fulfilled or safely skipped.
588    ActionPromiseCompleted,
589    /// Digests and scope-specific validation outcomes for requested files.
590    FileDigestsResolved {
591        /// Resolution outcomes in request order.
592        resolutions: Vec<FileDigestResolution>,
593    },
594    /// Wrapper phase timings were recorded.
595    WrapperTimingRecorded,
596    /// A fatal shim diagnostic was accepted for the session to surface.
597    ///
598    /// This is appended to preserve every existing response variant and wire
599    /// shape while giving fatal diagnostics their own acknowledgement.
600    ErrorRecorded,
601    /// A debug record was accepted, including when filtered out.
602    DebugRecorded,
603}