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