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}