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 = 8;
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 such as `miss`, `unconsulted`, `bypass`, or
190 /// `incremental` for a compilation the adapter deliberately ran with
191 /// incremental state instead of publishing.
192 outcome: String,
193 /// Compiler crate name, when the invocation supplied one.
194 crate_name: Option<String>,
195 /// Wall time spent running the compiler.
196 duration_ns: u64,
197 },
198 /// Account for a cache hit that was rebuilt for correctness verification.
199 RecordActionVerification {
200 /// Whether rebuilt and cached outputs matched.
201 matched: bool,
202 /// Restoration work performed before rebuilding.
203 restore: RestoreStats,
204 },
205 /// Store an action-result record locally and enqueue remote publication.
206 StoreActionResult {
207 /// Action-result record to store.
208 result: RemoteActionResult,
209 },
210 /// Find an earlier input prediction for a task and invocation.
211 FindActionPrediction {
212 /// Stable task identity.
213 task: String,
214 /// Digest of the compiler invocation without discovered inputs.
215 invocation: CacheDigest,
216 },
217 /// Record an input prediction after a successful compilation.
218 RecordActionPrediction {
219 /// Stable task identity.
220 task: String,
221 /// Adapter-owned prediction record.
222 prediction: ActionPrediction,
223 },
224 /// Find cached identity output for an executable and environment.
225 FindExecutableIdentity {
226 /// Executable whose identity command would run.
227 executable: PathBuf,
228 /// Environment variables affecting identity output.
229 environment: BTreeMap<String, Option<String>>,
230 },
231 /// Cache identity output for an executable and environment.
232 StoreExecutableIdentity {
233 /// Executable whose identity command ran.
234 executable: PathBuf,
235 /// Environment variables affecting identity output.
236 environment: BTreeMap<String, Option<String>>,
237 /// Captured identity-command standard output.
238 stdout: Vec<u8>,
239 /// Files the probe read, as they were when it read them, which pin
240 /// the output beyond this session so the next one can skip the
241 /// probe. Empty keeps the identity for this session only.
242 #[serde(default, skip_serializing_if = "Vec::is_empty")]
243 pins: Vec<PinnedFile>,
244 },
245 /// Surface a shim diagnostic through the session that owns the build.
246 ///
247 /// A shim must not print diagnostics itself: its stderr belongs to the
248 /// compiler it stands in for, and build scripts read that stream as part
249 /// of the compiler's answer -- cc-rs treats any stderr output from a flag
250 /// probe as "unsupported", which changes the flags of every compilation
251 /// that follows and, with them, every action key the build produces.
252 ///
253 /// Appended rather than grouped with the other `Record*` requests it
254 /// belongs beside: these variants carry no `repr`, so inserting one moves
255 /// the discriminant of every variant after it, and a break nobody asked
256 /// for is worth less than the grouping.
257 RecordWarning {
258 /// Human-readable single-line diagnostic.
259 message: String,
260 },
261 /// Find session-recorded digests for files with these identities.
262 FindFileDigests {
263 /// What the recorded digests may stand in for.
264 scope: FileDigestScope,
265 /// File identities to resolve, preserving request order in the
266 /// response.
267 files: Vec<FileIdentity>,
268 },
269 /// Record digests of files a shim hashed or wrote this session.
270 RecordFileDigests {
271 /// What the recorded digests may stand in for.
272 scope: FileDigestScope,
273 /// Hashed files and the identities their digests describe.
274 entries: Vec<RecordedFileDigest>,
275 },
276 /// Join or claim an invocation-wide promise through the cache server.
277 JoinActionPromise {
278 /// Adapter that owns the invocation and prediction payload.
279 adapter: String,
280 /// Digest of the compiler invocation before input discovery.
281 invocation: CacheDigest,
282 },
283 /// Fulfill a claimed promise after its action result is remotely durable.
284 CompleteActionPromise {
285 /// Opaque claim token returned by [`Self::JoinActionPromise`].
286 claim: String,
287 /// Prediction through which waiters reconstruct the final action key.
288 prediction: ActionPrediction,
289 },
290 /// Resolve file digests, coalescing concurrent reads through the agent.
291 ResolveFileDigests {
292 /// What the digest and any accompanying validation may stand in for.
293 scope: FileDigestScope,
294 /// File identities to resolve, preserving request order.
295 files: Vec<FileIdentity>,
296 },
297 /// Record exclusive wrapper phase durations and bounded trace spans.
298 RecordWrapperTiming {
299 /// Completed invocation timings.
300 timing: WrapperTiming,
301 },
302 /// Surface a fatal shim diagnostic through the session that owns the build.
303 ///
304 /// This is appended to preserve every existing request variant and wire
305 /// shape while allowing a caller to distinguish fatal acknowledgement from
306 /// [`Self::RecordWarning`] and fall back locally when it is not accepted.
307 RecordError {
308 /// Human-readable single-line diagnostic.
309 message: String,
310 },
311}
312
313/// One wrapper invocation, independent of its cache hit/miss accounting.
314#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
315#[non_exhaustive]
316pub struct WrapperTiming {
317 /// Compiler adapter, such as rustc or cc.
318 pub adapter: String,
319 /// Crate or source name, when known.
320 pub unit: Option<String>,
321 /// Wrapper process, used as the trace lane.
322 pub pid: u32,
323 /// Wall-clock start in microseconds since the Unix epoch.
324 pub start_us: u64,
325 /// Monotonic elapsed time, excluding telemetry delivery.
326 pub duration_ns: u64,
327 /// Exclusive durations; nested work is subtracted from its parent.
328 pub phases_ns: std::collections::BTreeMap<String, u64>,
329 /// Nested spans relative to this wrapper's start, capped at 512.
330 pub spans: Vec<WrapperSpan>,
331}
332
333/// A trace interval inside one wrapper process.
334#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
335#[non_exhaustive]
336pub struct WrapperSpan {
337 /// Phase name.
338 pub name: String,
339 /// Monotonic offset from wrapper entry.
340 pub start_ns: u64,
341 /// Inclusive elapsed time.
342 pub duration_ns: u64,
343}
344
345/// Local output restoration work performed by one action-cache adapter hit.
346#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
347#[serde(deny_unknown_fields)]
348pub struct RestoreStats {
349 /// Cumulative time spent materializing and validating output files.
350 pub duration_ns: u64,
351 /// Compiler wall time recorded when this action was originally produced.
352 /// Zero means no timing hint was available.
353 pub avoided_compiler_duration_ns: u64,
354 /// Number of compiler output files restored.
355 pub output_files: u64,
356 /// Declared size of compiler output files restored.
357 pub output_bytes: u64,
358 /// Number of restored output files that share data blocks with the CAS.
359 pub reflinked_output_files: u64,
360 /// Declared size of restored outputs that share data blocks with the CAS.
361 pub reflinked_output_bytes: u64,
362 /// Number of restored output files that required a byte-for-byte copy.
363 pub copied_output_files: u64,
364 /// Declared size of restored outputs that required a byte-for-byte copy.
365 pub copied_output_bytes: u64,
366 /// Number of output files already in place with the cached contents, kept
367 /// rather than rewritten.
368 pub reused_output_files: u64,
369 /// Declared size of outputs kept in place rather than rewritten.
370 pub reused_output_bytes: u64,
371}
372
373/// One accounted cache decision, as it happens.
374///
375/// The agent already folds every one of these into [`AgentStats`]; an observer
376/// sees the same decisions individually, before that summing loses the crate
377/// they belong to. Delivered synchronously from the request handler, so an
378/// observer that blocks slows the build it is watching.
379#[derive(Debug, Clone)]
380#[non_exhaustive]
381pub enum AgentEvent {
382 /// An invocation completed its wrapper instrumentation.
383 WrapperTiming {
384 /// Completed invocation timings.
385 timing: WrapperTiming,
386 },
387 /// An action's outputs were restored from cache.
388 ActionHit {
389 /// Compiler crate name, when the invocation supplied one.
390 crate_name: Option<String>,
391 /// Restoration work performed by the adapter.
392 restore: RestoreStats,
393 },
394 /// A compilation the adapter declined to cache.
395 Bypass {
396 /// Stable, low-cardinality bypass-reason name.
397 kind: String,
398 },
399 /// A compilation no lookup was possible for.
400 Unconsulted,
401 /// A real compiler invocation ran.
402 CompilerInvocation {
403 /// Stable outcome category such as `miss`, `unconsulted`, or `bypass`.
404 outcome: String,
405 /// Compiler crate name, when the invocation supplied one.
406 crate_name: Option<String>,
407 /// Wall time spent running the compiler.
408 duration_ns: u64,
409 },
410 /// A hit was rebuilt to verify it.
411 Verification {
412 /// Whether rebuilt and cached outputs matched.
413 matched: bool,
414 /// Restoration work performed before rebuilding.
415 restore: RestoreStats,
416 },
417 /// A shim reported a diagnostic for the session to surface.
418 Warning {
419 /// Human-readable single-line diagnostic.
420 message: String,
421 },
422 /// A shim reported a fatal diagnostic for the session to surface.
423 Error {
424 /// Human-readable single-line diagnostic.
425 message: String,
426 },
427 /// Cache-key material for the action event immediately following it.
428 ActionDiagnostic {
429 /// Outcome of the action this describes.
430 outcome: String,
431 /// Compiler crate name, when the invocation supplied one.
432 crate_name: Option<String>,
433 /// Privacy-preserving action-key decomposition.
434 diagnostic: ActionDiagnostic,
435 },
436}
437
438/// A privacy-preserving decomposition of an action key.
439///
440/// Values are content digests rather than source or environment contents. The
441/// names are enough to say what changed without copying secrets into session
442/// history.
443#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
444pub struct ActionDiagnostic {
445 /// Complete action-cache key.
446 pub action: CacheDigest,
447 /// Non-file key components, named for display.
448 pub components: BTreeMap<String, CacheDigest>,
449 /// Normalized input path to content digest.
450 pub inputs: BTreeMap<String, CacheDigest>,
451}
452
453/// A sink for [`AgentEvent`]s observed during one session.
454pub trait AgentEventObserver: Send + Sync {
455 /// Handle one event. Must not panic, and should not block.
456 fn event(&self, event: AgentEvent);
457}
458
459/// A response returned by the task-scoped cache agent.
460#[derive(Debug, Clone, Serialize, Deserialize)]
461#[serde(tag = "type", rename_all = "snake_case")]
462pub enum AgentResponse {
463 /// Successful protocol negotiation.
464 Hello {
465 /// Agent protocol version.
466 protocol: u8,
467 /// Human-readable agent version.
468 agent_version: String,
469 },
470 /// A task prediction run was begun.
471 TaskBegun {
472 /// Opaque run identifier to pass to compiler shims and commit later.
473 run: String,
474 },
475 /// A task prediction run was committed.
476 TaskCommitted,
477 /// A local CAS path already verified against the requested digest.
478 Blob {
479 /// Verified local path, or `None` on a cache miss.
480 path: Option<PathBuf>,
481 },
482 /// Local CAS paths already verified against the requested digests.
483 Blobs {
484 /// Verified local paths or misses, in request order.
485 paths: Vec<Option<PathBuf>>,
486 },
487 /// A blob was stored locally.
488 Stored {
489 /// Path of the stored object in the local CAS.
490 path: PathBuf,
491 },
492 /// Result of an action lookup.
493 ActionResult {
494 /// Validated action result, or `None` on a cache miss.
495 result: Option<RemoteActionResult>,
496 },
497 /// Hit statistics were updated.
498 ActionHitRecorded,
499 /// Verification statistics were updated.
500 ActionVerificationRecorded,
501 /// Bypass statistics were updated.
502 BypassRecorded,
503 /// Unconsulted-compilation statistics were updated.
504 UnconsultedRecorded,
505 /// Compiler invocation accounting was recorded.
506 CompilerInvocationRecorded,
507 /// An action result was stored.
508 ActionStored {
509 /// Path of the stored local action-result record.
510 path: PathBuf,
511 },
512 /// Result of an input-prediction lookup.
513 ActionPrediction {
514 /// Matching prediction, or `None` when none is known.
515 prediction: Option<ActionPrediction>,
516 },
517 /// An input prediction was recorded.
518 ActionPredictionRecorded,
519 /// Result of an executable-identity lookup.
520 ExecutableIdentity {
521 /// Captured output, or `None` when no identity is cached.
522 stdout: Option<Vec<u8>>,
523 },
524 /// The request failed without terminating the agent connection.
525 Error {
526 /// Human-readable failure description.
527 message: String,
528 },
529 /// A shim diagnostic was accepted for the session to surface.
530 ///
531 /// Kept after `Error` so this acknowledgement remains alongside the
532 /// existing diagnostic response without changing earlier variants.
533 WarningRecorded,
534 /// Digests recorded earlier for the requested file identities.
535 FileDigests {
536 /// Recorded digests or misses, in request order.
537 digests: Vec<Option<CacheDigest>>,
538 },
539 /// File digests were recorded.
540 FileDigestsRecorded,
541 /// State of an optional server-wide compilation promise.
542 ActionPromise {
543 /// Opaque lease owned by this client, when it should compile.
544 claim: Option<String>,
545 /// Completed prediction, when another client compiled first.
546 prediction: Option<ActionPrediction>,
547 },
548 /// A server-wide compilation promise was fulfilled or safely skipped.
549 ActionPromiseCompleted,
550 /// Digests and scope-specific validation outcomes for requested files.
551 FileDigestsResolved {
552 /// Resolution outcomes in request order.
553 resolutions: Vec<FileDigestResolution>,
554 },
555 /// Wrapper phase timings were recorded.
556 WrapperTimingRecorded,
557 /// A fatal shim diagnostic was accepted for the session to surface.
558 ///
559 /// This is appended to preserve every existing response variant and wire
560 /// shape while giving fatal diagnostics their own acknowledgement.
561 ErrorRecorded,
562}