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. New variants go here.
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}
298
299/// Local output restoration work performed by one action-cache adapter hit.
300#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
301#[serde(deny_unknown_fields)]
302pub struct RestoreStats {
303 /// Cumulative time spent materializing and validating output files.
304 pub duration_ns: u64,
305 /// Compiler wall time recorded when this action was originally produced.
306 /// Zero means no timing hint was available.
307 pub avoided_compiler_duration_ns: u64,
308 /// Number of compiler output files restored.
309 pub output_files: u64,
310 /// Declared size of compiler output files restored.
311 pub output_bytes: u64,
312 /// Number of restored output files that share data blocks with the CAS.
313 pub reflinked_output_files: u64,
314 /// Declared size of restored outputs that share data blocks with the CAS.
315 pub reflinked_output_bytes: u64,
316 /// Number of restored output files that required a byte-for-byte copy.
317 pub copied_output_files: u64,
318 /// Declared size of restored outputs that required a byte-for-byte copy.
319 pub copied_output_bytes: u64,
320 /// Number of output files already in place with the cached contents, kept
321 /// rather than rewritten.
322 pub reused_output_files: u64,
323 /// Declared size of outputs kept in place rather than rewritten.
324 pub reused_output_bytes: u64,
325}
326
327/// One accounted cache decision, as it happens.
328///
329/// The agent already folds every one of these into [`AgentStats`]; an observer
330/// sees the same decisions individually, before that summing loses the crate
331/// they belong to. Delivered synchronously from the request handler, so an
332/// observer that blocks slows the build it is watching.
333#[derive(Debug, Clone)]
334#[non_exhaustive]
335pub enum AgentEvent {
336 /// An action's outputs were restored from cache.
337 ActionHit {
338 /// Compiler crate name, when the invocation supplied one.
339 crate_name: Option<String>,
340 /// Restoration work performed by the adapter.
341 restore: RestoreStats,
342 },
343 /// A compilation the adapter declined to cache.
344 Bypass {
345 /// Stable, low-cardinality bypass-reason name.
346 kind: String,
347 },
348 /// A compilation no lookup was possible for.
349 Unconsulted,
350 /// A real compiler invocation ran.
351 CompilerInvocation {
352 /// Stable outcome category such as `miss`, `unconsulted`, or `bypass`.
353 outcome: String,
354 /// Compiler crate name, when the invocation supplied one.
355 crate_name: Option<String>,
356 /// Wall time spent running the compiler.
357 duration_ns: u64,
358 },
359 /// A hit was rebuilt to verify it.
360 Verification {
361 /// Whether rebuilt and cached outputs matched.
362 matched: bool,
363 /// Restoration work performed before rebuilding.
364 restore: RestoreStats,
365 },
366 /// A shim reported a diagnostic for the session to surface.
367 Warning {
368 /// Human-readable single-line diagnostic.
369 message: String,
370 },
371 /// Cache-key material for the action event immediately following it.
372 ActionDiagnostic {
373 /// Outcome of the action this describes.
374 outcome: String,
375 /// Compiler crate name, when the invocation supplied one.
376 crate_name: Option<String>,
377 /// Privacy-preserving action-key decomposition.
378 diagnostic: ActionDiagnostic,
379 },
380}
381
382/// A privacy-preserving decomposition of an action key.
383///
384/// Values are content digests rather than source or environment contents. The
385/// names are enough to say what changed without copying secrets into session
386/// history.
387#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
388pub struct ActionDiagnostic {
389 /// Complete action-cache key.
390 pub action: CacheDigest,
391 /// Non-file key components, named for display.
392 pub components: BTreeMap<String, CacheDigest>,
393 /// Normalized input path to content digest.
394 pub inputs: BTreeMap<String, CacheDigest>,
395}
396
397/// A sink for [`AgentEvent`]s observed during one session.
398pub trait AgentEventObserver: Send + Sync {
399 /// Handle one event. Must not panic, and should not block.
400 fn event(&self, event: AgentEvent);
401}
402
403/// A response returned by the task-scoped cache agent.
404#[derive(Debug, Clone, Serialize, Deserialize)]
405#[serde(tag = "type", rename_all = "snake_case")]
406pub enum AgentResponse {
407 /// Successful protocol negotiation.
408 Hello {
409 /// Agent protocol version.
410 protocol: u8,
411 /// Human-readable agent version.
412 agent_version: String,
413 },
414 /// A task prediction run was begun.
415 TaskBegun {
416 /// Opaque run identifier to pass to compiler shims and commit later.
417 run: String,
418 },
419 /// A task prediction run was committed.
420 TaskCommitted,
421 /// A local CAS path already verified against the requested digest.
422 Blob {
423 /// Verified local path, or `None` on a cache miss.
424 path: Option<PathBuf>,
425 },
426 /// Local CAS paths already verified against the requested digests.
427 Blobs {
428 /// Verified local paths or misses, in request order.
429 paths: Vec<Option<PathBuf>>,
430 },
431 /// A blob was stored locally.
432 Stored {
433 /// Path of the stored object in the local CAS.
434 path: PathBuf,
435 },
436 /// Result of an action lookup.
437 ActionResult {
438 /// Validated action result, or `None` on a cache miss.
439 result: Option<RemoteActionResult>,
440 },
441 /// Hit statistics were updated.
442 ActionHitRecorded,
443 /// Verification statistics were updated.
444 ActionVerificationRecorded,
445 /// Bypass statistics were updated.
446 BypassRecorded,
447 /// Unconsulted-compilation statistics were updated.
448 UnconsultedRecorded,
449 /// Compiler invocation accounting was recorded.
450 CompilerInvocationRecorded,
451 /// An action result was stored.
452 ActionStored {
453 /// Path of the stored local action-result record.
454 path: PathBuf,
455 },
456 /// Result of an input-prediction lookup.
457 ActionPrediction {
458 /// Matching prediction, or `None` when none is known.
459 prediction: Option<ActionPrediction>,
460 },
461 /// An input prediction was recorded.
462 ActionPredictionRecorded,
463 /// Result of an executable-identity lookup.
464 ExecutableIdentity {
465 /// Captured output, or `None` when no identity is cached.
466 stdout: Option<Vec<u8>>,
467 },
468 /// The request failed without terminating the agent connection.
469 Error {
470 /// Human-readable failure description.
471 message: String,
472 },
473 /// A shim diagnostic was accepted for the session to surface.
474 ///
475 /// Sits past `Error` for the reason [`AgentRequest::RecordWarning`] sits
476 /// last: anywhere earlier renumbers the variants below it. New variants go
477 /// here.
478 WarningRecorded,
479 /// Digests recorded earlier for the requested file identities.
480 FileDigests {
481 /// Recorded digests or misses, in request order.
482 digests: Vec<Option<CacheDigest>>,
483 },
484 /// File digests were recorded.
485 FileDigestsRecorded,
486 /// State of an optional server-wide compilation promise.
487 ActionPromise {
488 /// Opaque lease owned by this client, when it should compile.
489 claim: Option<String>,
490 /// Completed prediction, when another client compiled first.
491 prediction: Option<ActionPrediction>,
492 },
493 /// A server-wide compilation promise was fulfilled or safely skipped.
494 ActionPromiseCompleted,
495 /// Digests and scope-specific validation outcomes for requested files.
496 FileDigestsResolved {
497 /// Resolution outcomes in request order.
498 resolutions: Vec<FileDigestResolution>,
499 },
500}