mbx-cache-core 0.19.2

Unstable CAS, transport, and cache-agent primitives for mbx embedders
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
use super::{FileDigestResolution, FileDigestScope, FileIdentity, RecordedFileDigest};
use crate::{ActionPrediction, CacheDigest, RemoteActionResult};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::path::PathBuf;

/// Wire protocol version used between an in-process cache agent and its shims.
pub const AGENT_PROTOCOL_VERSION: u8 = 11;
/// Largest single protocol request the agent will read.
///
/// Requests are small JSON objects; the largest legitimate ones carry an output
/// tree or a batch of digests, which stay far below this.
pub(super) const MAX_REQUEST_BYTES: usize = 16 * 1024 * 1024;

/// A file as a probe found it: absent, or present with a length and
/// modification time. Length alone would miss a rewrite that kept the size.
///
/// Described by the shim at the moment it reads the file, not by the agent
/// afterwards, so an executable replaced while its probe ran cannot be
/// recorded under the replacement's identity.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PinnedFile {
    /// The file, absent or present.
    pub path: PathBuf,
    /// What was there, or `None` for nothing.
    pub state: Option<PinnedState>,
}

/// What a present pinned file looked like: enough to notice it being
/// written, replaced, or made executable, without reading it.
///
/// Length and modification time alone would miss a replacement of the same
/// length whose timestamp was preserved, and a `chmod +x`, which touches
/// neither. So the inode says whether it is the same file, the change time,
/// which the kernel sets on every write, rename and permission change and
/// which no program can set back, says whether it was touched, and the mode
/// says whether it can run. Windows has no change time or inode to offer
/// through the standard library; its creation time and attributes stand in.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PinnedState {
    /// Length in bytes.
    pub len: u64,
    /// Modification time, seconds since the Unix epoch.
    pub modified_secs: u64,
    /// Modification time, nanoseconds past `modified_secs`.
    pub modified_nanos: u32,
    /// Change time on Unix, creation time on Windows, seconds since the
    /// Unix epoch.
    pub changed_secs: u64,
    /// Nanoseconds past `changed_secs`.
    pub changed_nanos: u32,
    /// The inode on Unix; zero on Windows.
    pub inode: u64,
    /// Permission bits on Unix, file attributes on Windows.
    pub mode: u32,
}

impl PinnedFile {
    /// Describe `path` as it is now, or nothing when the filesystem cannot
    /// say enough about it to notice a change later.
    pub fn describe(path: impl Into<PathBuf>) -> Option<Self> {
        let path = path.into();
        let state = match std::fs::metadata(&path) {
            Ok(metadata) => Some(PinnedState::of(&metadata)?),
            Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
            Err(_) => return None,
        };
        Some(Self { path, state })
    }

    /// Whether the file is still as this pin describes it.
    pub fn holds(&self) -> bool {
        PinnedFile::describe(self.path.clone()).as_ref() == Some(self)
    }
}

impl PinnedState {
    #[cfg(unix)]
    fn of(metadata: &std::fs::Metadata) -> Option<Self> {
        use std::os::unix::fs::MetadataExt as _;
        let modified = metadata
            .modified()
            .ok()?
            .duration_since(std::time::SystemTime::UNIX_EPOCH)
            .ok()?;
        Some(Self {
            len: metadata.len(),
            modified_secs: modified.as_secs(),
            modified_nanos: modified.subsec_nanos(),
            changed_secs: u64::try_from(metadata.ctime()).ok()?,
            changed_nanos: u32::try_from(metadata.ctime_nsec()).ok()?,
            inode: metadata.ino(),
            mode: metadata.mode(),
        })
    }

    #[cfg(windows)]
    fn of(metadata: &std::fs::Metadata) -> Option<Self> {
        use std::os::windows::fs::MetadataExt as _;
        let modified = metadata
            .modified()
            .ok()?
            .duration_since(std::time::SystemTime::UNIX_EPOCH)
            .ok()?;
        let created = metadata
            .created()
            .ok()?
            .duration_since(std::time::SystemTime::UNIX_EPOCH)
            .ok()?;
        Some(Self {
            len: metadata.len(),
            modified_secs: modified.as_secs(),
            modified_nanos: modified.subsec_nanos(),
            changed_secs: created.as_secs(),
            changed_nanos: created.subsec_nanos(),
            inode: 0,
            mode: metadata.file_attributes(),
        })
    }

    #[cfg(not(any(unix, windows)))]
    fn of(_metadata: &std::fs::Metadata) -> Option<Self> {
        None
    }
}

/// A request accepted by the task-scoped cache agent.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum AgentRequest {
    /// Negotiate protocol and application versions.
    Hello {
        /// Agent protocol version understood by the caller.
        protocol: u8,
        /// Human-readable mbx client version.
        client_version: String,
    },
    /// Begin a prediction-manifest run for a stable Cargo or task identity.
    BeginTask {
        /// Stable 64-character lowercase hexadecimal task identity.
        task: String,
    },
    /// Commit predictions collected by an earlier [`Self::BeginTask`].
    CommitTask {
        /// Opaque run identifier returned by the agent.
        run: String,
    },
    /// Resolve a blob to a session-verified local CAS path.
    FindBlob {
        /// Blob to resolve.
        digest: CacheDigest,
    },
    /// Resolve blobs to session-verified local CAS paths.
    FindBlobs {
        /// Blobs to resolve, preserving request order in the response.
        digests: Vec<CacheDigest>,
    },
    /// Import a file into the local content-addressed store.
    StoreBlob {
        /// Digest the source must match.
        digest: CacheDigest,
        /// File to verify and import.
        source: PathBuf,
    },
    /// Look up an action-result record.
    FindActionResult {
        /// Action digest to resolve.
        action: CacheDigest,
    },
    /// Account for a successfully restored cache hit.
    RecordActionHit {
        /// Action that supplied the outputs.
        action: CacheDigest,
        /// Restoration work performed by the adapter.
        restore: RestoreStats,
        /// Compiler crate name, when the invocation supplied one.
        crate_name: Option<String>,
    },
    /// A compilation the adapter declined to cache, grouped by reason.
    RecordBypass {
        /// Stable, low-cardinality bypass-reason name.
        kind: String,
    },
    /// A compilation the adapter could not look up, having no key to look up
    /// with. Distinct from a bypass: these are cached once compiled.
    RecordUnconsulted,
    /// Account for one real compiler invocation performed by the adapter.
    RecordCompilerInvocation {
        /// Stable outcome category: `miss`, `unconsulted`, `bypass` or
        /// `verification`.
        ///
        /// A compilation the adapter deliberately ran with incremental state
        /// instead of publishing reports `incremental-miss` or
        /// `incremental-unconsulted`, saying both what its lookup did and that
        /// its result was withheld. The agent counts those separately; a bare
        /// `incremental` is no longer an outcome, because it could not say
        /// whether the cache had been consulted.
        outcome: String,
        /// Compiler crate name, when the invocation supplied one.
        crate_name: Option<String>,
        /// Wall time spent running the compiler.
        duration_ns: u64,
    },
    /// Account for a cache hit that was rebuilt for correctness verification.
    RecordActionVerification {
        /// Whether rebuilt and cached outputs matched.
        matched: bool,
        /// Restoration work performed before rebuilding.
        restore: RestoreStats,
    },
    /// Store an action-result record locally and enqueue remote publication.
    StoreActionResult {
        /// Action-result record to store.
        result: RemoteActionResult,
    },
    /// Find an earlier input prediction for a task and invocation.
    FindActionPrediction {
        /// Stable task identity.
        task: String,
        /// Digest of the compiler invocation without discovered inputs.
        invocation: CacheDigest,
    },
    /// Record an input prediction after a successful compilation.
    RecordActionPrediction {
        /// Stable task identity.
        task: String,
        /// Adapter-owned prediction record.
        prediction: ActionPrediction,
    },
    /// Find cached identity output for an executable and environment.
    FindExecutableIdentity {
        /// Executable whose identity command would run.
        executable: PathBuf,
        /// Environment variables affecting identity output.
        environment: BTreeMap<String, Option<String>>,
    },
    /// Cache identity output for an executable and environment.
    StoreExecutableIdentity {
        /// Executable whose identity command ran.
        executable: PathBuf,
        /// Environment variables affecting identity output.
        environment: BTreeMap<String, Option<String>>,
        /// Captured identity-command standard output.
        stdout: Vec<u8>,
        /// Files the probe read, as they were when it read them, which pin
        /// the output beyond this session so the next one can skip the
        /// probe. Empty keeps the identity for this session only.
        #[serde(default, skip_serializing_if = "Vec::is_empty")]
        pins: Vec<PinnedFile>,
    },
    /// Surface a shim diagnostic through the session that owns the build.
    ///
    /// A shim must not print diagnostics itself: its stderr belongs to the
    /// compiler it stands in for, and build scripts read that stream as part
    /// of the compiler's answer -- cc-rs treats any stderr output from a flag
    /// probe as "unsupported", which changes the flags of every compilation
    /// that follows and, with them, every action key the build produces.
    ///
    /// Appended rather than grouped with the other `Record*` requests it
    /// belongs beside: these variants carry no `repr`, so inserting one moves
    /// the discriminant of every variant after it, and a break nobody asked
    /// for is worth less than the grouping.
    RecordWarning {
        /// Human-readable single-line diagnostic.
        message: String,
    },
    /// Find session-recorded digests for files with these identities.
    FindFileDigests {
        /// What the recorded digests may stand in for.
        scope: FileDigestScope,
        /// File identities to resolve, preserving request order in the
        /// response.
        files: Vec<FileIdentity>,
    },
    /// Record digests of files a shim hashed or wrote this session.
    RecordFileDigests {
        /// What the recorded digests may stand in for.
        scope: FileDigestScope,
        /// Hashed files and the identities their digests describe.
        entries: Vec<RecordedFileDigest>,
    },
    /// Join or claim an invocation-wide promise through the cache server.
    JoinActionPromise {
        /// Adapter that owns the invocation and prediction payload.
        adapter: String,
        /// Digest of the compiler invocation before input discovery.
        invocation: CacheDigest,
    },
    /// Fulfill a claimed promise after its action result is remotely durable.
    CompleteActionPromise {
        /// Opaque claim token returned by [`Self::JoinActionPromise`].
        claim: String,
        /// Prediction through which waiters reconstruct the final action key.
        prediction: ActionPrediction,
    },
    /// Resolve file digests, coalescing concurrent reads through the agent.
    ResolveFileDigests {
        /// What the digest and any accompanying validation may stand in for.
        scope: FileDigestScope,
        /// File identities to resolve, preserving request order.
        files: Vec<FileIdentity>,
    },
    /// Record exclusive wrapper phase durations and bounded trace spans.
    RecordWrapperTiming {
        /// Completed invocation timings.
        timing: WrapperTiming,
    },
    /// Surface a fatal shim diagnostic through the session that owns the build.
    ///
    /// This is appended to preserve every existing request variant and wire
    /// shape while allowing a caller to distinguish fatal acknowledgement from
    /// [`Self::RecordWarning`] and fall back locally when it is not accepted.
    RecordError {
        /// Human-readable single-line diagnostic.
        message: String,
    },
    /// Emit a routine shim log through the owning session's logger.
    /// Filtering uses the original target; these records never consume the
    /// warning/error allowance or enter compiler output.
    RecordDebug {
        /// Original logging module target.
        target: String,
        /// Human-readable single-line message.
        message: String,
    },
}

/// One wrapper invocation, independent of its cache hit/miss accounting.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct WrapperTiming {
    /// Compiler adapter, such as rustc or cc.
    pub adapter: String,
    /// Crate or source name, when known.
    pub unit: Option<String>,
    /// Wrapper process, used as the trace lane.
    pub pid: u32,
    /// Wall-clock start in microseconds since the Unix epoch.
    pub start_us: u64,
    /// Monotonic elapsed time, excluding telemetry delivery.
    pub duration_ns: u64,
    /// Exclusive durations; nested work is subtracted from its parent.
    pub phases_ns: std::collections::BTreeMap<String, u64>,
    /// Nested spans relative to this wrapper's start, capped at 512.
    pub spans: Vec<WrapperSpan>,
    /// The build unit this invocation produced, when it can be identified.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub unit_id: Option<String>,
    /// Units whose outputs this invocation consumed, by [`Self::unit_id`].
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub dependencies: Vec<String>,
}

/// A trace interval inside one wrapper process.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct WrapperSpan {
    /// Phase name.
    pub name: String,
    /// Monotonic offset from wrapper entry.
    pub start_ns: u64,
    /// Inclusive elapsed time.
    pub duration_ns: u64,
}

/// Local output restoration work performed by one action-cache adapter hit.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RestoreStats {
    /// Cumulative time spent materializing and validating output files.
    pub duration_ns: u64,
    /// Compiler wall time recorded when this action was originally produced.
    /// Zero means no timing hint was available.
    pub avoided_compiler_duration_ns: u64,
    /// Number of compiler output files restored.
    pub output_files: u64,
    /// Declared size of compiler output files restored.
    pub output_bytes: u64,
    /// Number of restored output files that share data blocks with the CAS.
    pub reflinked_output_files: u64,
    /// Declared size of restored outputs that share data blocks with the CAS.
    pub reflinked_output_bytes: u64,
    /// Number of restored output files that share an inode with the CAS blob.
    pub hardlinked_output_files: u64,
    /// Declared size of restored outputs that share an inode with the CAS blob.
    pub hardlinked_output_bytes: u64,
    /// Number of restored output files that required a byte-for-byte copy.
    pub copied_output_files: u64,
    /// Declared size of restored outputs that required a byte-for-byte copy.
    pub copied_output_bytes: u64,
    /// Number of output files already in place with the cached contents, kept
    /// rather than rewritten.
    pub reused_output_files: u64,
    /// Declared size of outputs kept in place rather than rewritten.
    pub reused_output_bytes: u64,
}

/// One accounted cache decision, as it happens.
///
/// The agent already folds every one of these into [`AgentStats`]; an observer
/// sees the same decisions individually, before that summing loses the crate
/// they belong to. Delivered synchronously from the request handler, so an
/// observer that blocks slows the build it is watching.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum AgentEvent {
    /// An invocation completed its wrapper instrumentation.
    WrapperTiming {
        /// Completed invocation timings.
        timing: WrapperTiming,
    },
    /// An action's outputs were restored from cache.
    ActionHit {
        /// Compiler crate name, when the invocation supplied one.
        crate_name: Option<String>,
        /// Restoration work performed by the adapter.
        restore: RestoreStats,
    },
    /// A compilation the adapter declined to cache.
    Bypass {
        /// Stable, low-cardinality bypass-reason name.
        kind: String,
    },
    /// A compilation no lookup was possible for.
    Unconsulted,
    /// A compilation the adapter declined to cache, and the compiler run that
    /// followed.
    ///
    /// Emitted in place of [`Self::Bypass`] and a `bypass`
    /// [`Self::CompilerInvocation`] when a shim reports both over one
    /// connection, so the reason arrives with the crate and time it cost.
    BypassedCompilation {
        /// Stable, low-cardinality bypass-reason name.
        kind: String,
        /// Compiler crate name, when the invocation supplied one.
        crate_name: Option<String>,
        /// Wall time spent running the compiler.
        duration_ns: u64,
    },
    /// A real compiler invocation ran.
    CompilerInvocation {
        /// Stable outcome category such as `miss`, `unconsulted`, or `bypass`.
        outcome: String,
        /// Compiler crate name, when the invocation supplied one.
        crate_name: Option<String>,
        /// Wall time spent running the compiler.
        duration_ns: u64,
    },
    /// A hit was rebuilt to verify it.
    Verification {
        /// Whether rebuilt and cached outputs matched.
        matched: bool,
        /// Restoration work performed before rebuilding.
        restore: RestoreStats,
    },
    /// A shim reported a diagnostic for the session to surface.
    Warning {
        /// Human-readable single-line diagnostic.
        message: String,
    },
    /// A shim reported a fatal diagnostic for the session to surface.
    Error {
        /// Human-readable single-line diagnostic.
        message: String,
    },
    /// Cache-key material for the action event immediately following it.
    ActionDiagnostic {
        /// Outcome of the action this describes.
        outcome: String,
        /// Compiler crate name, when the invocation supplied one.
        crate_name: Option<String>,
        /// Privacy-preserving action-key decomposition.
        diagnostic: ActionDiagnostic,
    },
}

/// A privacy-preserving decomposition of an action key.
///
/// Values are content digests rather than source or environment contents. The
/// names are enough to say what changed without copying secrets into session
/// history.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ActionDiagnostic {
    /// Complete action-cache key.
    pub action: CacheDigest,
    /// Non-file key components, named for display.
    pub components: BTreeMap<String, CacheDigest>,
    /// Normalized input path to content digest.
    pub inputs: BTreeMap<String, CacheDigest>,
}

/// A sink for [`AgentEvent`]s observed during one session.
pub trait AgentEventObserver: Send + Sync {
    /// Handle one event. Must not panic, and should not block.
    fn event(&self, event: AgentEvent);
}

/// A response returned by the task-scoped cache agent.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum AgentResponse {
    /// Successful protocol negotiation.
    Hello {
        /// Agent protocol version.
        protocol: u8,
        /// Human-readable agent version.
        agent_version: String,
    },
    /// A task prediction run was begun.
    TaskBegun {
        /// Opaque run identifier to pass to compiler shims and commit later.
        run: String,
    },
    /// A task prediction run was committed.
    TaskCommitted,
    /// A local CAS path already verified against the requested digest.
    Blob {
        /// Verified local path, or `None` on a cache miss.
        path: Option<PathBuf>,
    },
    /// Local CAS paths already verified against the requested digests.
    Blobs {
        /// Verified local paths or misses, in request order.
        paths: Vec<Option<PathBuf>>,
    },
    /// A blob was stored locally.
    Stored {
        /// Path of the stored object in the local CAS.
        path: PathBuf,
    },
    /// Result of an action lookup.
    ActionResult {
        /// Validated action result, or `None` on a cache miss.
        result: Option<RemoteActionResult>,
    },
    /// Hit statistics were updated.
    ActionHitRecorded,
    /// Verification statistics were updated.
    ActionVerificationRecorded,
    /// Bypass statistics were updated.
    BypassRecorded,
    /// Unconsulted-compilation statistics were updated.
    UnconsultedRecorded,
    /// Compiler invocation accounting was recorded.
    CompilerInvocationRecorded,
    /// An action result was stored.
    ActionStored {
        /// Path of the stored local action-result record.
        path: PathBuf,
    },
    /// Result of an input-prediction lookup.
    ActionPrediction {
        /// Matching prediction, or `None` when none is known.
        prediction: Option<ActionPrediction>,
    },
    /// An input prediction was recorded.
    ActionPredictionRecorded,
    /// Result of an executable-identity lookup.
    ExecutableIdentity {
        /// Captured output, or `None` when no identity is cached.
        stdout: Option<Vec<u8>>,
    },
    /// The request failed without terminating the agent connection.
    Error {
        /// Human-readable failure description.
        message: String,
    },
    /// A shim diagnostic was accepted for the session to surface.
    ///
    /// Kept after `Error` so this acknowledgement remains alongside the
    /// existing diagnostic response without changing earlier variants.
    WarningRecorded,
    /// Digests recorded earlier for the requested file identities.
    FileDigests {
        /// Recorded digests or misses, in request order.
        digests: Vec<Option<CacheDigest>>,
    },
    /// File digests were recorded.
    FileDigestsRecorded,
    /// State of an optional server-wide compilation promise.
    ActionPromise {
        /// Opaque lease owned by this client, when it should compile.
        claim: Option<String>,
        /// Completed prediction, when another client compiled first.
        prediction: Option<ActionPrediction>,
    },
    /// A server-wide compilation promise was fulfilled or safely skipped.
    ActionPromiseCompleted,
    /// Digests and scope-specific validation outcomes for requested files.
    FileDigestsResolved {
        /// Resolution outcomes in request order.
        resolutions: Vec<FileDigestResolution>,
    },
    /// Wrapper phase timings were recorded.
    WrapperTimingRecorded,
    /// A fatal shim diagnostic was accepted for the session to surface.
    ///
    /// This is appended to preserve every existing response variant and wire
    /// shape while giving fatal diagnostics their own acknowledgement.
    ErrorRecorded,
    /// A debug record was accepted, including when filtered out.
    DebugRecorded,
}