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 = 9;
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}
347
348/// A trace interval inside one wrapper process.
349#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
350#[non_exhaustive]
351pub struct WrapperSpan {
352 /// Phase name.
353 pub name: String,
354 /// Monotonic offset from wrapper entry.
355 pub start_ns: u64,
356 /// Inclusive elapsed time.
357 pub duration_ns: u64,
358}
359
360/// Local output restoration work performed by one action-cache adapter hit.
361#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
362#[serde(deny_unknown_fields)]
363pub struct RestoreStats {
364 /// Cumulative time spent materializing and validating output files.
365 pub duration_ns: u64,
366 /// Compiler wall time recorded when this action was originally produced.
367 /// Zero means no timing hint was available.
368 pub avoided_compiler_duration_ns: u64,
369 /// Number of compiler output files restored.
370 pub output_files: u64,
371 /// Declared size of compiler output files restored.
372 pub output_bytes: u64,
373 /// Number of restored output files that share data blocks with the CAS.
374 pub reflinked_output_files: u64,
375 /// Declared size of restored outputs that share data blocks with the CAS.
376 pub reflinked_output_bytes: u64,
377 /// Number of restored output files that required a byte-for-byte copy.
378 pub copied_output_files: u64,
379 /// Declared size of restored outputs that required a byte-for-byte copy.
380 pub copied_output_bytes: u64,
381 /// Number of output files already in place with the cached contents, kept
382 /// rather than rewritten.
383 pub reused_output_files: u64,
384 /// Declared size of outputs kept in place rather than rewritten.
385 pub reused_output_bytes: u64,
386}
387
388/// One accounted cache decision, as it happens.
389///
390/// The agent already folds every one of these into [`AgentStats`]; an observer
391/// sees the same decisions individually, before that summing loses the crate
392/// they belong to. Delivered synchronously from the request handler, so an
393/// observer that blocks slows the build it is watching.
394#[derive(Debug, Clone)]
395#[non_exhaustive]
396pub enum AgentEvent {
397 /// An invocation completed its wrapper instrumentation.
398 WrapperTiming {
399 /// Completed invocation timings.
400 timing: WrapperTiming,
401 },
402 /// An action's outputs were restored from cache.
403 ActionHit {
404 /// Compiler crate name, when the invocation supplied one.
405 crate_name: Option<String>,
406 /// Restoration work performed by the adapter.
407 restore: RestoreStats,
408 },
409 /// A compilation the adapter declined to cache.
410 Bypass {
411 /// Stable, low-cardinality bypass-reason name.
412 kind: String,
413 },
414 /// A compilation no lookup was possible for.
415 Unconsulted,
416 /// A real compiler invocation ran.
417 CompilerInvocation {
418 /// Stable outcome category such as `miss`, `unconsulted`, or `bypass`.
419 outcome: String,
420 /// Compiler crate name, when the invocation supplied one.
421 crate_name: Option<String>,
422 /// Wall time spent running the compiler.
423 duration_ns: u64,
424 },
425 /// A hit was rebuilt to verify it.
426 Verification {
427 /// Whether rebuilt and cached outputs matched.
428 matched: bool,
429 /// Restoration work performed before rebuilding.
430 restore: RestoreStats,
431 },
432 /// A shim reported a diagnostic for the session to surface.
433 Warning {
434 /// Human-readable single-line diagnostic.
435 message: String,
436 },
437 /// A shim reported a fatal diagnostic for the session to surface.
438 Error {
439 /// Human-readable single-line diagnostic.
440 message: String,
441 },
442 /// Cache-key material for the action event immediately following it.
443 ActionDiagnostic {
444 /// Outcome of the action this describes.
445 outcome: String,
446 /// Compiler crate name, when the invocation supplied one.
447 crate_name: Option<String>,
448 /// Privacy-preserving action-key decomposition.
449 diagnostic: ActionDiagnostic,
450 },
451}
452
453/// A privacy-preserving decomposition of an action key.
454///
455/// Values are content digests rather than source or environment contents. The
456/// names are enough to say what changed without copying secrets into session
457/// history.
458#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
459pub struct ActionDiagnostic {
460 /// Complete action-cache key.
461 pub action: CacheDigest,
462 /// Non-file key components, named for display.
463 pub components: BTreeMap<String, CacheDigest>,
464 /// Normalized input path to content digest.
465 pub inputs: BTreeMap<String, CacheDigest>,
466}
467
468/// A sink for [`AgentEvent`]s observed during one session.
469pub trait AgentEventObserver: Send + Sync {
470 /// Handle one event. Must not panic, and should not block.
471 fn event(&self, event: AgentEvent);
472}
473
474/// A response returned by the task-scoped cache agent.
475#[derive(Debug, Clone, Serialize, Deserialize)]
476#[serde(tag = "type", rename_all = "snake_case")]
477pub enum AgentResponse {
478 /// Successful protocol negotiation.
479 Hello {
480 /// Agent protocol version.
481 protocol: u8,
482 /// Human-readable agent version.
483 agent_version: String,
484 },
485 /// A task prediction run was begun.
486 TaskBegun {
487 /// Opaque run identifier to pass to compiler shims and commit later.
488 run: String,
489 },
490 /// A task prediction run was committed.
491 TaskCommitted,
492 /// A local CAS path already verified against the requested digest.
493 Blob {
494 /// Verified local path, or `None` on a cache miss.
495 path: Option<PathBuf>,
496 },
497 /// Local CAS paths already verified against the requested digests.
498 Blobs {
499 /// Verified local paths or misses, in request order.
500 paths: Vec<Option<PathBuf>>,
501 },
502 /// A blob was stored locally.
503 Stored {
504 /// Path of the stored object in the local CAS.
505 path: PathBuf,
506 },
507 /// Result of an action lookup.
508 ActionResult {
509 /// Validated action result, or `None` on a cache miss.
510 result: Option<RemoteActionResult>,
511 },
512 /// Hit statistics were updated.
513 ActionHitRecorded,
514 /// Verification statistics were updated.
515 ActionVerificationRecorded,
516 /// Bypass statistics were updated.
517 BypassRecorded,
518 /// Unconsulted-compilation statistics were updated.
519 UnconsultedRecorded,
520 /// Compiler invocation accounting was recorded.
521 CompilerInvocationRecorded,
522 /// An action result was stored.
523 ActionStored {
524 /// Path of the stored local action-result record.
525 path: PathBuf,
526 },
527 /// Result of an input-prediction lookup.
528 ActionPrediction {
529 /// Matching prediction, or `None` when none is known.
530 prediction: Option<ActionPrediction>,
531 },
532 /// An input prediction was recorded.
533 ActionPredictionRecorded,
534 /// Result of an executable-identity lookup.
535 ExecutableIdentity {
536 /// Captured output, or `None` when no identity is cached.
537 stdout: Option<Vec<u8>>,
538 },
539 /// The request failed without terminating the agent connection.
540 Error {
541 /// Human-readable failure description.
542 message: String,
543 },
544 /// A shim diagnostic was accepted for the session to surface.
545 ///
546 /// Kept after `Error` so this acknowledgement remains alongside the
547 /// existing diagnostic response without changing earlier variants.
548 WarningRecorded,
549 /// Digests recorded earlier for the requested file identities.
550 FileDigests {
551 /// Recorded digests or misses, in request order.
552 digests: Vec<Option<CacheDigest>>,
553 },
554 /// File digests were recorded.
555 FileDigestsRecorded,
556 /// State of an optional server-wide compilation promise.
557 ActionPromise {
558 /// Opaque lease owned by this client, when it should compile.
559 claim: Option<String>,
560 /// Completed prediction, when another client compiled first.
561 prediction: Option<ActionPrediction>,
562 },
563 /// A server-wide compilation promise was fulfilled or safely skipped.
564 ActionPromiseCompleted,
565 /// Digests and scope-specific validation outcomes for requested files.
566 FileDigestsResolved {
567 /// Resolution outcomes in request order.
568 resolutions: Vec<FileDigestResolution>,
569 },
570 /// Wrapper phase timings were recorded.
571 WrapperTimingRecorded,
572 /// A fatal shim diagnostic was accepted for the session to surface.
573 ///
574 /// This is appended to preserve every existing response variant and wire
575 /// shape while giving fatal diagnostics their own acknowledgement.
576 ErrorRecorded,
577 /// A debug record was accepted, including when filtered out.
578 DebugRecorded,
579}