1use crate::uploads::{ConnectionUploads, UploadQueue, UploadSink};
2use crate::{
3 ActionPrediction, ActionPromiseCompletion, ActionPromiseState, CacheDigest, CacheDirectory,
4 LocalActionCache, LocalCas, MAX_STAGED_BLOB_PACK_BYTES, ManifestPutOutcome, RemoteActionResult,
5 RemoteCacheClient, RemoteCacheMode, RustcMetadata, TaskActionManifest, blob_pack_chunk,
6 canonical_json,
7};
8use eyre::{Context, Result, bail};
9use futures_util::{FutureExt, StreamExt, future::BoxFuture, stream};
10use log::{debug, warn};
11use serde::{Deserialize, Serialize};
12use std::collections::{BTreeMap, BTreeSet};
13use std::fs;
14use std::path::{Path, PathBuf};
15use std::sync::atomic::{AtomicU64, Ordering};
16use std::sync::{Arc, Mutex, Weak};
17use std::time::{Duration, Instant, SystemTime};
18
19const ACTION_PROMISE_WAIT: Duration = Duration::from_secs(60 * 60);
23use tokio::io::{AsyncBufRead, AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
24
25mod file_digest;
26mod manifest;
27mod prefetch;
28mod stats;
29mod wire;
30
31#[cfg(test)]
32pub(crate) use prefetch::select_prefetch_actions;
33
34pub use file_digest::{
35 FileDigestCache, FileDigestResolution, FileDigestScope, FileIdentity, FileObjectIdentity,
36 FileSnapshot, NoFileDigestCache, RecordedFileDigest, digest_file,
37};
38pub use manifest::{is_task_identity, task_manifest_actions};
39use manifest::{
40 merge_remote_task_manifest, merge_task_manifests, task_manifest_dir, validate_task_identity,
41 validate_task_manifest,
42};
43pub use stats::{AgentStats, CompilerStats};
44use wire::MAX_REQUEST_BYTES;
45pub use wire::{
46 AGENT_PROTOCOL_VERSION, ActionDiagnostic, AgentEvent, AgentEventObserver, AgentRequest,
47 AgentResponse, PinnedFile, PinnedState, RestoreStats, WrapperSpan, WrapperTiming,
48};
49
50const MAX_EXECUTABLE_IDENTITIES: usize = 64;
51const MAX_EXECUTABLE_IDENTITY_SIZE: usize = 64 * 1024;
52const MAX_EXECUTABLE_IDENTITY_BYTES: usize = 256 * 1024;
53const TASK_ACTION_MANIFEST_VERSION: u8 = 1;
54const MAX_TASK_ACTION_PREDICTIONS: usize = 16 * 1024;
55const MAX_DIAGNOSTIC_BYTES: usize = 4 * 1024;
57const MAX_DIAGNOSTICS: usize = 128;
63const ACTION_DIAGNOSTIC_PREFIX: &str = "@mbx-action-diagnostic\t";
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
66enum DiagnosticLevel {
67 Warning,
68 Error,
69}
70
71impl DiagnosticLevel {
72 fn label(self) -> &'static str {
73 match self {
74 Self::Warning => "warning",
75 Self::Error => "error",
76 }
77 }
78}
79
80const MAX_FILE_DIGEST_BATCH: usize = 16 * 1024;
82const MAX_FILE_DIGEST_ENTRIES: usize = 1024 * 1024;
87const MAX_CONCURRENT_FILE_DIGESTS: usize = 8;
90const MAX_REMOTE_TRANSFERS: usize = 64;
91const MAX_PREFETCH_TRANSFERS: usize = 48;
92const MAX_CONCURRENT_BLOB_PACKS: usize = 8;
98const MAX_PREFETCH_ACTIONS: usize = 1024;
109const MAX_PREFETCH_ACTION_WAVE: usize = 32;
114const MAX_PREFETCH_BATCH_LOOKUPS: usize = 1;
120const PREFETCH_ACTION_BATCH_DELAY: Duration = Duration::from_millis(5);
121const MAX_PREFETCH_DIRECTORY_OBJECTS: usize = 100_000;
122const MAX_PREFETCH_OBJECTS_PER_WAVE: usize = 100_000;
123const DEFAULT_MAX_REMOTE_DOWNLOAD_BYTES: u64 = 5 * 1024 * 1024 * 1024;
124
125pub type TaskFallbacks = Arc<dyn Fn() -> Vec<String> + Send + Sync>;
128type FileDigestFlightKey = (FileDigestScope, FileIdentity);
129type FileDigestFlights = BTreeMap<FileDigestFlightKey, Weak<FileDigestFlight>>;
130
131struct FileDigestFlight {
132 lock: tokio::sync::Mutex<()>,
133 resolution: Mutex<Option<FileDigestResolution>>,
134}
135
136const NEWEST_MANIFEST_CANDIDATES: usize = 8;
148
149pub struct AgentRemoteCache {
151 pub client: RemoteCacheClient,
153 pub mode: RemoteCacheMode,
155 pub staging_dir: PathBuf,
157}
158
159#[derive(Default)]
160struct AtomicAgentStats {
161 wrapper_phases_ns: Mutex<BTreeMap<String, u64>>,
162 lookups: AtomicU64,
163 unconsulted: AtomicU64,
164 hits: AtomicU64,
165 stores: AtomicU64,
166 stored_bytes: AtomicU64,
167 verifications: AtomicU64,
168 divergences: AtomicU64,
169 downloaded_bytes: AtomicU64,
170 uploaded_bytes: AtomicU64,
171 background_uploads: AtomicU64,
172 background_upload_failures: AtomicU64,
173 remote_blob_pack_uploads: AtomicU64,
174 remote_blob_pack_upload_blobs: AtomicU64,
175 upload_drain_duration_ns: AtomicU64,
176 prefetched_actions: AtomicU64,
177 predictions_loaded: AtomicU64,
178 remote_failures: AtomicU64,
179 remote_manifest_lookups: AtomicU64,
180 remote_manifest_lookup_duration_ns: AtomicU64,
181 remote_action_lookups: AtomicU64,
182 remote_action_lookup_duration_ns: AtomicU64,
183 remote_blob_requests: AtomicU64,
184 remote_blob_pack_requests: AtomicU64,
185 remote_blob_pack_blobs: AtomicU64,
186 remote_blob_transfer_duration_ns: AtomicU64,
187 local_cas_write_duration_ns: AtomicU64,
188 prefetch_runs: AtomicU64,
189 prefetch_duration_ns: AtomicU64,
190 materialization_duration_ns: AtomicU64,
191 bypasses: Mutex<BTreeMap<String, u64>>,
192 avoided_compiler_duration_ns: AtomicU64,
193 compiler: Mutex<BTreeMap<String, CompilerStats>>,
194 unit_outcomes: Mutex<BTreeMap<String, BTreeSet<String>>>,
195 slow_compilations: Mutex<BTreeMap<String, u64>>,
196 restored_output_files: AtomicU64,
197 restored_output_bytes: AtomicU64,
198 reflinked_output_files: AtomicU64,
199 reflinked_output_bytes: AtomicU64,
200 copied_output_files: AtomicU64,
201 copied_output_bytes: AtomicU64,
202 reused_output_files: AtomicU64,
203 reused_output_bytes: AtomicU64,
204}
205
206struct AtomicDurationTimer<'a> {
207 started: Instant,
208 target: &'a AtomicU64,
209}
210
211impl<'a> AtomicDurationTimer<'a> {
212 fn start(target: &'a AtomicU64) -> Self {
213 Self {
214 started: Instant::now(),
215 target,
216 }
217 }
218}
219
220impl Drop for AtomicDurationTimer<'_> {
221 fn drop(&mut self) {
222 atomic_saturating_add(self.target, duration_ns(self.started));
223 }
224}
225
226fn duration_ns(started: Instant) -> u64 {
227 started.elapsed().as_nanos().try_into().unwrap_or(u64::MAX)
228}
229
230fn validate_crate_name(crate_name: Option<&str>) -> Result<()> {
231 if let Some(crate_name) = crate_name
232 && (crate_name.len() > 256 || crate_name.contains(['\0', '\n', '\r']))
233 {
234 bail!("invalid compiler crate name");
235 }
236 Ok(())
237}
238
239#[derive(serde::Deserialize)]
240struct ActionDiagnosticEnvelope {
241 outcome: String,
242 crate_name: Option<String>,
243 diagnostic: ActionDiagnostic,
244}
245
246fn parse_action_diagnostic(
247 message: &str,
248) -> Option<Result<(String, Option<String>, ActionDiagnostic)>> {
249 let payload = message.strip_prefix(ACTION_DIAGNOSTIC_PREFIX)?;
250 Some((|| {
251 let envelope: ActionDiagnosticEnvelope = serde_json::from_str(payload)?;
252 if !matches!(envelope.outcome.as_str(), "hit" | "miss") {
253 bail!("invalid action diagnostic outcome");
254 }
255 validate_crate_name(envelope.crate_name.as_deref())?;
256 Ok((envelope.outcome, envelope.crate_name, envelope.diagnostic))
257 })())
258}
259
260fn atomic_saturating_add(target: &AtomicU64, value: u64) {
261 let _ = target.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
262 Some(current.saturating_add(value))
263 });
264}
265
266fn queue_prefetch_digest(
267 verified: &BTreeMap<CacheDigest, PathBuf>,
268 pending: &mut BTreeMap<CacheDigest, ()>,
269 digest: CacheDigest,
270) {
271 if verified.contains_key(&digest) || pending.contains_key(&digest) {
272 return;
273 }
274 pending.insert(digest, ());
275}
276
277fn queue_prefetch_directory(
278 seen: &BTreeMap<CacheDigest, ()>,
279 pending: &mut BTreeMap<CacheDigest, ()>,
280 digest: CacheDigest,
281 limit: usize,
282) -> bool {
283 if seen.contains_key(&digest) || pending.contains_key(&digest) {
284 return true;
285 }
286 if seen.len().saturating_add(pending.len()) >= limit {
287 return false;
288 }
289 pending.insert(digest, ());
290 true
291}
292
293pub type TaskLoader = dyn for<'a> Fn(&'a CacheAgent, &'a str) -> BoxFuture<'a, ()> + Send + Sync;
295
296#[derive(Clone)]
302pub struct CacheAgent {
303 cas: LocalCas,
304 actions: LocalActionCache,
305 verified_blobs: Arc<Mutex<BTreeMap<CacheDigest, VerifiedBlob>>>,
306 version: Arc<str>,
307 write_locks: Arc<Mutex<BTreeMap<CacheDigest, Weak<tokio::sync::Mutex<()>>>>>,
308 action_locks: Arc<Mutex<BTreeMap<CacheDigest, Weak<tokio::sync::Mutex<()>>>>>,
309 stats: Arc<AtomicAgentStats>,
310 observer: Option<Arc<dyn AgentEventObserver>>,
311 observer_emission: Arc<Mutex<()>>,
312 task_loader: Option<Arc<TaskLoader>>,
315 executable_identities: Arc<Mutex<BTreeMap<ExecutableIdentityKey, Vec<u8>>>>,
316 identity_dir: Arc<PathBuf>,
318 manifest_dir: Arc<PathBuf>,
319 task_actions: Arc<Mutex<BTreeMap<String, TaskActionState>>>,
320 task_fallbacks: Arc<Mutex<BTreeMap<String, TaskFallbacks>>>,
322 next_task_run: Arc<AtomicU64>,
323 manifest_write_lock: Arc<Mutex<()>>,
324 remote: Option<Arc<RemoteCacheClient>>,
325 remote_mode: RemoteCacheMode,
326 remote_staging_dir: Arc<PathBuf>,
327 remote_download_limit: u64,
328 remote_download_bytes: Arc<AtomicU64>,
329 pending_remote_actions: Arc<Mutex<BTreeMap<CacheDigest, RemoteActionResult>>>,
330 remote_transfers: Arc<tokio::sync::Semaphore>,
331 prefetch_transfers: Arc<tokio::sync::Semaphore>,
332 prefetch_tasks: Arc<Mutex<Vec<tokio::task::JoinHandle<()>>>>,
333 diagnostics: Arc<Mutex<BTreeSet<(DiagnosticLevel, String)>>>,
338 file_digests: Arc<Mutex<BTreeMap<(FileDigestScope, PathBuf), RecordedFileDigest>>>,
341 file_digest_locks: Arc<Mutex<FileDigestFlights>>,
343 file_digest_permits: Arc<tokio::sync::Semaphore>,
344 #[cfg(test)]
345 file_digest_reads: Arc<AtomicU64>,
346 uploads: Option<UploadQueue>,
348}
349
350fn validate_file_digest_record(entry: &RecordedFileDigest) -> Result<()> {
353 if !entry.file.path.is_absolute() {
354 bail!("file-digest records need absolute paths");
355 }
356 entry.digest.validate()?;
357 if entry.file.len != entry.digest.size {
358 bail!("file-digest record length does not match its digest");
359 }
360 Ok(())
361}
362
363struct AgentUploadSink {
365 stats: Arc<AtomicAgentStats>,
366}
367
368impl UploadSink for AgentUploadSink {
369 fn record_blob_uploaded(&self, bytes: u64) {
370 self.stats
371 .background_uploads
372 .fetch_add(1, Ordering::Relaxed);
373 self.stats
374 .uploaded_bytes
375 .fetch_add(bytes, Ordering::Relaxed);
376 }
377
378 fn record_action_uploaded(&self) {
379 self.stats
380 .background_uploads
381 .fetch_add(1, Ordering::Relaxed);
382 }
383
384 fn record_blob_pack_uploaded(&self, blobs: u64) {
385 self.stats
386 .remote_blob_pack_uploads
387 .fetch_add(1, Ordering::Relaxed);
388 self.stats
389 .remote_blob_pack_upload_blobs
390 .fetch_add(blobs, Ordering::Relaxed);
391 }
392
393 fn record_upload_failure(&self) {
394 self.stats
395 .background_upload_failures
396 .fetch_add(1, Ordering::Relaxed);
397 self.stats.remote_failures.fetch_add(1, Ordering::Relaxed);
398 }
399}
400
401#[derive(Debug, Clone)]
407struct VerifiedBlob {
408 path: PathBuf,
409 len: u64,
410 modified: SystemTime,
411}
412
413impl VerifiedBlob {
414 fn describe(path: &Path) -> Option<Self> {
417 let metadata = std::fs::metadata(path).ok()?;
418 Some(Self {
419 path: path.to_path_buf(),
420 len: metadata.len(),
421 modified: metadata.modified().ok()?,
422 })
423 }
424
425 fn is_unchanged(&self) -> bool {
427 let Ok(metadata) = std::fs::metadata(&self.path) else {
428 return false;
429 };
430 metadata.len() == self.len && metadata.modified().is_ok_and(|now| now == self.modified)
431 }
432}
433
434#[derive(Debug, Clone, Default)]
435struct TaskActionState {
436 manifest: String,
437 baseline_loaded: bool,
438 predictions: BTreeMap<CacheDigest, ActionPrediction>,
439 pending_predictions: BTreeMap<CacheDigest, ActionPrediction>,
440 changed_predictions: BTreeMap<CacheDigest, Option<ActionPrediction>>,
447 prefetched_adapters: BTreeSet<String>,
448 remote_etag: Option<String>,
449}
450
451fn activate_prediction_adapter(
453 state: &mut TaskActionState,
454 invocation: &CacheDigest,
455) -> (Option<ActionPrediction>, Option<Vec<ActionPrediction>>) {
456 let prediction = state.predictions.get(invocation).cloned();
457 let prefetch = prediction.as_ref().and_then(|prediction| {
458 if !state.prefetched_adapters.insert(prediction.adapter.clone()) {
459 return None;
460 }
461 Some(
462 state
463 .predictions
464 .values()
465 .filter(|candidate| candidate.adapter == prediction.adapter)
466 .cloned()
467 .collect(),
468 )
469 });
470 (prediction, prefetch)
471}
472
473struct PrefetchedAction {
474 adapter: String,
475 result: RemoteActionResult,
476}
477
478struct RemoteDownloadReservation {
479 counter: Arc<AtomicU64>,
480 reserved: u64,
481 committed: bool,
482}
483
484impl RemoteDownloadReservation {
485 fn bytes(&self) -> u64 {
486 self.reserved
487 }
488
489 fn commit(mut self, bytes: u64) {
490 debug_assert!(bytes <= self.reserved);
491 self.counter
492 .fetch_sub(self.reserved.saturating_sub(bytes), Ordering::AcqRel);
493 self.committed = true;
494 }
495}
496
497impl Drop for RemoteDownloadReservation {
498 fn drop(&mut self) {
499 if !self.committed {
500 self.counter.fetch_sub(self.reserved, Ordering::AcqRel);
501 }
502 }
503}
504
505#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
506struct ExecutableIdentityKey {
507 executable: PathBuf,
508 environment: BTreeMap<String, Option<String>>,
509}
510
511const PERSISTED_EXECUTABLE_IDENTITY_VERSION: u8 = 2;
513
514#[derive(Debug, Serialize, Deserialize)]
520struct PersistedExecutableIdentity {
521 version: u8,
522 executable: PathBuf,
523 environment: BTreeMap<String, Option<String>>,
524 pins: Vec<PinnedFile>,
525 stdout: String,
526}
527
528impl CacheAgent {
529 pub fn new(cache_dir: impl Into<PathBuf>, version: impl Into<Arc<str>>) -> Self {
531 Self::build(cache_dir.into(), version.into(), None, 0)
532 }
533
534 pub fn new_remote(
536 cache_dir: impl Into<PathBuf>,
537 version: impl Into<Arc<str>>,
538 remote: AgentRemoteCache,
539 ) -> Self {
540 Self::build(
541 cache_dir.into(),
542 version.into(),
543 Some(remote),
544 DEFAULT_MAX_REMOTE_DOWNLOAD_BYTES,
545 )
546 }
547
548 pub fn new_remote_with_download_limit(
550 cache_dir: impl Into<PathBuf>,
551 version: impl Into<Arc<str>>,
552 remote: AgentRemoteCache,
553 max_remote_download_bytes: u64,
554 ) -> Self {
555 Self::build(
556 cache_dir.into(),
557 version.into(),
558 Some(remote),
559 max_remote_download_bytes,
560 )
561 }
562
563 fn build(
564 cache_dir: PathBuf,
565 version: Arc<str>,
566 remote: Option<AgentRemoteCache>,
567 remote_download_limit: u64,
568 ) -> Self {
569 let remote_mode = remote
570 .as_ref()
571 .map_or(RemoteCacheMode::ReadOnly, |remote| remote.mode);
572 let remote_staging_dir = remote.as_ref().map_or_else(
573 || cache_dir.join("remote"),
574 |remote| remote.staging_dir.clone(),
575 );
576 let remote = remote.map(|remote| Arc::new(remote.client));
577 let stats = Arc::new(AtomicAgentStats::default());
578 let remote_transfers = Arc::new(tokio::sync::Semaphore::new(MAX_REMOTE_TRANSFERS));
579 let uploads = remote
582 .clone()
583 .filter(|_| remote_mode.writes())
584 .map(|client| {
585 UploadQueue::new(
586 client,
587 Arc::new(AgentUploadSink {
588 stats: stats.clone(),
589 }),
590 remote_transfers.clone(),
591 )
592 });
593 Self {
594 cas: LocalCas::new(cache_dir.clone()),
595 actions: LocalActionCache::new(cache_dir.clone()),
596 verified_blobs: Arc::new(Mutex::new(BTreeMap::new())),
597 version,
598 write_locks: Arc::new(Mutex::new(BTreeMap::new())),
599 action_locks: Arc::new(Mutex::new(BTreeMap::new())),
600 stats,
601 observer: None,
602 observer_emission: Arc::new(Mutex::new(())),
603 task_loader: None,
604 executable_identities: Arc::new(Mutex::new(BTreeMap::new())),
605 identity_dir: Arc::new(cache_dir.join("executable-identities").join("v1")),
606 manifest_dir: Arc::new(task_manifest_dir(&cache_dir)),
607 task_actions: Arc::new(Mutex::new(BTreeMap::new())),
608 task_fallbacks: Arc::new(Mutex::new(BTreeMap::new())),
609 next_task_run: Arc::new(AtomicU64::new(0)),
610 manifest_write_lock: Arc::new(Mutex::new(())),
611 remote,
612 remote_mode,
613 remote_staging_dir: Arc::new(remote_staging_dir),
614 remote_download_limit,
615 remote_download_bytes: Arc::new(AtomicU64::new(0)),
616 pending_remote_actions: Arc::new(Mutex::new(BTreeMap::new())),
617 remote_transfers,
618 prefetch_transfers: Arc::new(tokio::sync::Semaphore::new(MAX_PREFETCH_TRANSFERS)),
619 prefetch_tasks: Arc::new(Mutex::new(Vec::new())),
620 diagnostics: Arc::new(Mutex::new(BTreeSet::new())),
621 file_digests: Arc::new(Mutex::new(BTreeMap::new())),
622 file_digest_locks: Arc::new(Mutex::new(BTreeMap::new())),
623 file_digest_permits: Arc::new(tokio::sync::Semaphore::new(MAX_CONCURRENT_FILE_DIGESTS)),
624 #[cfg(test)]
625 file_digest_reads: Arc::new(AtomicU64::new(0)),
626 uploads,
627 }
628 }
629
630 #[must_use]
632 pub fn with_observer(mut self, observer: Arc<dyn AgentEventObserver>) -> Self {
633 self.observer = Some(observer);
634 self
635 }
636
637 pub fn with_task_loader(mut self, loader: Arc<TaskLoader>) -> Self {
644 self.task_loader = Some(loader);
645 self
646 }
647
648 async fn ensure_task_loaded(&self, task: &str) {
651 let Some(loader) = &self.task_loader else {
652 return;
653 };
654 if self.task_actions.lock().unwrap().contains_key(task) {
655 return;
656 }
657 loader(self, task).await;
658 }
659
660 fn emit(&self, event: impl FnOnce() -> AgentEvent) {
661 if let Some(observer) = &self.observer {
662 let _emission = self.observer_emission.lock().unwrap();
663 observer.event(event());
664 }
665 }
666
667 fn emit_action(&self, diagnostic: Option<AgentEvent>, action: AgentEvent) {
668 if let Some(observer) = &self.observer {
669 let _emission = self.observer_emission.lock().unwrap();
670 if let Some(diagnostic) = diagnostic {
671 observer.event(diagnostic);
672 }
673 observer.event(action);
674 }
675 }
676
677 fn reserve_remote_download(&self, bytes: u64) -> Result<RemoteDownloadReservation> {
678 let mut current = self.remote_download_bytes.load(Ordering::Acquire);
679 loop {
680 let next = current
681 .checked_add(bytes)
682 .ok_or_else(|| eyre::eyre!("remote cache download budget overflowed"))?;
683 if next > self.remote_download_limit {
684 bail!(
685 "remote cache download budget exceeded: {} bytes requested with {} of {} bytes already used",
686 bytes,
687 current,
688 self.remote_download_limit
689 );
690 }
691 match self.remote_download_bytes.compare_exchange_weak(
692 current,
693 next,
694 Ordering::AcqRel,
695 Ordering::Acquire,
696 ) {
697 Ok(_) => {
698 return Ok(RemoteDownloadReservation {
699 counter: self.remote_download_bytes.clone(),
700 reserved: bytes,
701 committed: false,
702 });
703 }
704 Err(observed) => current = observed,
705 }
706 }
707 }
708
709 fn reserve_remote_download_up_to(&self, requested: u64) -> Result<RemoteDownloadReservation> {
710 let mut current = self.remote_download_bytes.load(Ordering::Acquire);
711 loop {
712 let reserved = requested.min(self.remote_download_limit.saturating_sub(current));
713 let next = current + reserved;
714 match self.remote_download_bytes.compare_exchange_weak(
715 current,
716 next,
717 Ordering::AcqRel,
718 Ordering::Acquire,
719 ) {
720 Ok(_) => {
721 return Ok(RemoteDownloadReservation {
722 counter: self.remote_download_bytes.clone(),
723 reserved,
724 committed: false,
725 });
726 }
727 Err(observed) => current = observed,
728 }
729 }
730 }
731
732 pub async fn begin_task(&self, task: &str) -> Result<String> {
734 self.begin_task_with_remote_errors(task, false, None, true)
735 .await
736 }
737
738 pub async fn begin_task_on_prediction(&self, task: &str) -> Result<String> {
741 self.begin_task_with_remote_errors(task, false, None, false)
742 .await
743 }
744
745 pub async fn begin_session_task(&self, task: &str) -> Result<()> {
751 self.begin_task_with_remote_errors(task, false, Some(task.to_string()), false)
752 .await?;
753 Ok(())
754 }
755
756 pub fn register_task_fallbacks(
767 &self,
768 task: &str,
769 fallbacks: impl Fn() -> Vec<String> + Send + Sync + 'static,
770 ) -> Result<()> {
771 validate_task_identity(task)?;
772 self.task_fallbacks
773 .lock()
774 .unwrap()
775 .insert(task.to_string(), Arc::new(fallbacks));
776 Ok(())
777 }
778
779 pub async fn prefetch_task(&self, task: &str) -> Result<String> {
781 let run = self
782 .begin_task_with_remote_errors(task, true, None, true)
783 .await?;
784 self.wait_for_prefetches().await;
785 Ok(run)
786 }
787
788 async fn begin_task_with_remote_errors(
789 &self,
790 task: &str,
791 strict: bool,
792 run: Option<String>,
793 eager_prefetch: bool,
794 ) -> Result<String> {
795 validate_task_identity(task)?;
796 let early_manifest = {
802 let _write_guard = self.manifest_write_lock.lock().unwrap();
803 let _file_guard = self.lock_task_manifest(task)?;
804 self.load_task_manifest(task)?
805 };
806 let early_actions: BTreeSet<_> = early_manifest
807 .iter()
808 .flat_map(|manifest| manifest.predictions.iter())
809 .map(|prediction| prediction.action.clone())
810 .collect();
811 if eager_prefetch {
812 self.spawn_prefetch_predictions(
813 early_manifest
814 .as_ref()
815 .map(|manifest| manifest.predictions.clone())
816 .unwrap_or_default(),
817 );
818 }
819 let (remote_manifest, mut remote_etag) = if self.remote_mode.reads() {
820 match self.get_remote_task_manifest(task).await {
821 Ok(Some((manifest, etag))) => (Some(manifest), Some(etag)),
822 Ok(None) => (None, None),
823 Err(error) => {
824 if strict {
825 return Err(error).wrap_err_with(|| {
826 format!("remote task action manifest lookup failed for {task}")
827 });
828 }
829 self.note_remote_failure();
830 warn!("remote task action manifest lookup failed for {task}: {error}");
831 (None, None)
832 }
833 }
834 } else {
835 (None, None)
836 };
837 let manifest = if self.remote.is_some() && self.remote_mode.reads() {
841 let _write_guard = self.manifest_write_lock.lock().unwrap();
842 let _file_guard = self.lock_task_manifest(task)?;
843 let local_manifest = self.load_task_manifest(task)?;
844 let manifest = match (remote_manifest, local_manifest) {
845 (Some(remote), Some(local)) => {
846 let (manifest, merged) = merge_remote_task_manifest(task, remote, local);
847 if !merged {
848 remote_etag = None;
849 }
850 Some(manifest)
851 }
852 (Some(remote), None) => Some(remote),
853 (None, local) => local,
854 };
855 if let Some(manifest) = &manifest {
856 self.persist_task_manifest(manifest)?;
857 }
858 manifest
859 } else {
860 early_manifest
861 };
862 let (manifest, remote_etag) = match manifest {
863 Some(manifest) => (Some(manifest), remote_etag),
864 None => (self.inherit_task_manifest(task, strict).await?, None),
867 };
868 let mut state = if let Some(manifest) = manifest {
869 TaskActionState {
870 manifest: task.to_string(),
871 baseline_loaded: true,
872 predictions: manifest
873 .predictions
874 .into_iter()
875 .map(|prediction| (prediction.invocation.clone(), prediction))
876 .collect(),
877 pending_predictions: BTreeMap::new(),
878 changed_predictions: BTreeMap::new(),
879 prefetched_adapters: BTreeSet::new(),
880 remote_etag,
881 }
882 } else {
883 TaskActionState {
884 manifest: task.to_string(),
885 baseline_loaded: true,
886 remote_etag,
887 ..TaskActionState::default()
888 }
889 };
890 let run = run.unwrap_or_else(|| {
891 let sequence = self.next_task_run.fetch_add(1, Ordering::Relaxed);
892 CacheDigest::blake3(format!("{task}\0{}\0{sequence}", std::process::id()).as_bytes())
893 .hash
894 });
895 self.stats.predictions_loaded.fetch_max(
899 state.predictions.len().try_into().unwrap_or(u64::MAX),
900 Ordering::Relaxed,
901 );
902 if eager_prefetch {
903 state.prefetched_adapters.extend(
904 state
905 .predictions
906 .values()
907 .map(|prediction| prediction.adapter.clone()),
908 );
909 }
910 let predictions = state
914 .predictions
915 .values()
916 .filter(|prediction| !early_actions.contains(&prediction.action))
917 .cloned()
918 .collect();
919 self.task_actions.lock().unwrap().insert(run.clone(), state);
920 if eager_prefetch {
921 self.spawn_prefetch_predictions(predictions);
922 }
923 Ok(run)
924 }
925
926 async fn inherit_task_manifest(
934 &self,
935 task: &str,
936 strict: bool,
937 ) -> Result<Option<TaskActionManifest>> {
938 let fallbacks = self.task_fallbacks.lock().unwrap().get(task).cloned();
939 let Some(fallbacks) = fallbacks else {
940 return Ok(None);
941 };
942 let named = tokio::task::spawn_blocking(move || fallbacks()).await?;
943 let mut tried = BTreeSet::from([task.to_string()]);
944 let candidates = named.into_iter().map(|identity| (identity, true)).chain(
945 self.newest_task_identities()
946 .into_iter()
947 .map(|identity| (identity, false)),
948 );
949 for (identity, named) in candidates {
950 if !tried.insert(identity.clone()) || validate_task_identity(&identity).is_err() {
951 continue;
952 }
953 let mut found = self.load_task_manifest(&identity)?;
954 if found.is_none() && named && self.remote_mode.reads() {
955 found = match self.get_remote_task_manifest(&identity).await {
956 Ok(manifest) => manifest.map(|(manifest, _)| manifest),
957 Err(error) => {
958 if strict {
959 return Err(error).wrap_err_with(|| {
960 format!("remote task action manifest lookup failed for {identity}")
961 });
962 }
963 self.note_remote_failure();
964 warn!("remote task action manifest lookup failed for {identity}: {error}");
965 None
966 }
967 };
968 }
969 let Some(mut manifest) = found else {
970 continue;
971 };
972 if manifest.predictions.is_empty()
973 || (!named && manifest.predictions.len() > MAX_TASK_ACTION_PREDICTIONS / 2)
974 {
975 continue;
976 }
977 debug!(
978 "no predictions were recorded for {task}; inheriting {} from {identity}",
979 manifest.predictions.len()
980 );
981 manifest.task = task.to_string();
982 validate_task_manifest(&manifest, task)?;
983 let _write_guard = self.manifest_write_lock.lock().unwrap();
984 let _file_guard = self.lock_task_manifest(task)?;
985 if let Some(recorded) = self.load_task_manifest(task)? {
989 return Ok(Some(recorded));
990 }
991 self.persist_task_manifest(&manifest)?;
992 return Ok(Some(manifest));
993 }
994 Ok(None)
995 }
996
997 fn newest_task_identities(&self) -> Vec<String> {
999 let Ok(entries) = fs::read_dir(self.manifest_dir.as_path()) else {
1000 return Vec::new();
1001 };
1002 let mut manifests: Vec<(std::time::SystemTime, String)> = entries
1003 .filter_map(|entry| {
1004 let entry = entry.ok()?;
1005 let name = entry.file_name();
1006 let identity = name.to_str()?.strip_suffix(".json")?.to_string();
1007 validate_task_identity(&identity).ok()?;
1008 let modified = entry.metadata().ok()?.modified().ok()?;
1009 Some((modified, identity))
1010 })
1011 .collect();
1012 manifests.sort_by(|left, right| right.cmp(left));
1013 manifests
1014 .into_iter()
1015 .take(NEWEST_MANIFEST_CANDIDATES)
1016 .map(|(_, identity)| identity)
1017 .collect()
1018 }
1019
1020 pub async fn cancel_prefetches(&self) {
1022 let tasks = std::mem::take(&mut *self.prefetch_tasks.lock().unwrap());
1023 for task in &tasks {
1024 task.abort();
1025 }
1026 for task in tasks {
1027 if let Err(error) = task.await
1028 && !error.is_cancelled()
1029 {
1030 warn!("remote action prefetch task failed: {error}");
1031 }
1032 }
1033 }
1034
1035 pub async fn wait_for_uploads(&self) {
1042 let Some(uploads) = &self.uploads else {
1043 return;
1044 };
1045 let _timer = AtomicDurationTimer::start(&self.stats.upload_drain_duration_ns);
1046 uploads.drain().await;
1047 }
1048
1049 async fn wait_for_prefetches(&self) {
1050 let tasks = std::mem::take(&mut *self.prefetch_tasks.lock().unwrap());
1051 for task in tasks {
1052 if let Err(error) = task.await {
1053 warn!("remote action prefetch task failed: {error}");
1054 }
1055 }
1056 }
1057
1058 pub async fn commit_task(&self, run: &str) -> Result<()> {
1060 self.commit_task_actions(run).await.map(|_| ())
1061 }
1062
1063 pub async fn commit_task_actions(&self, run: &str) -> Result<Vec<ActionPrediction>> {
1069 validate_task_identity(run)?;
1070 let state = {
1071 let mut runs = self.task_actions.lock().unwrap();
1072 let state = runs
1073 .get(run)
1074 .ok_or_else(|| eyre::eyre!("task action manifest baseline was not loaded"))?;
1075 if !state.baseline_loaded {
1076 bail!("task action manifest baseline was not loaded");
1077 }
1078 debug!(
1084 "committing {} changed of {} recorded predictions for {}",
1085 state.changed_predictions.len(),
1086 state.pending_predictions.len(),
1087 state.manifest
1088 );
1089 if state.changed_predictions.is_empty()
1090 && (self.remote.is_none() || !self.remote_mode.writes())
1091 {
1092 let completed = state.pending_predictions.values().cloned().collect();
1093 runs.remove(run);
1094 return Ok(completed);
1095 }
1096 state.clone()
1097 };
1098 let task = state.manifest;
1099 validate_task_identity(&task)?;
1100 let completed = state
1101 .pending_predictions
1102 .values()
1103 .cloned()
1104 .collect::<Vec<_>>();
1105 let (manifest, introduced) = {
1106 let _write_guard = self.manifest_write_lock.lock().unwrap();
1107 let _file_guard = self.lock_task_manifest(&task)?;
1108 let mut predictions = self
1109 .load_task_manifest(&task)?
1110 .map(|manifest| {
1111 manifest
1112 .predictions
1113 .into_iter()
1114 .map(|prediction| (prediction.invocation.clone(), prediction))
1115 .collect::<BTreeMap<_, _>>()
1116 })
1117 .unwrap_or_default();
1118 let introduced: BTreeSet<CacheDigest> = state
1123 .pending_predictions
1124 .keys()
1125 .filter(|invocation| !predictions.contains_key(*invocation))
1126 .cloned()
1127 .collect();
1128 predictions.extend(state.pending_predictions);
1133 let manifest = TaskActionManifest {
1134 version: TASK_ACTION_MANIFEST_VERSION,
1135 task: task.clone(),
1136 predictions: predictions.into_values().collect(),
1137 };
1138 validate_task_manifest(&manifest, &task)?;
1139 self.persist_task_manifest(&manifest)?;
1140 (manifest, introduced)
1141 };
1142 self.task_actions.lock().unwrap().remove(run);
1143 if self.remote_mode.writes() {
1144 let mut manifest = manifest;
1148 if let Some(uploads) = &self.uploads {
1149 let actions: Vec<CacheDigest> = manifest
1150 .predictions
1151 .iter()
1152 .map(|prediction| prediction.action.clone())
1153 .collect();
1154 let unpublished = uploads.wait_for_actions(&actions).await;
1155 let withheld = manifest
1156 .predictions
1157 .iter()
1158 .filter(|prediction| {
1159 introduced.contains(&prediction.invocation)
1160 && unpublished.contains(&prediction.action)
1161 })
1162 .count();
1163 if withheld > 0 {
1164 warn!(
1167 "{withheld} of {} predicted actions were not published, so the remote task action manifest omits them",
1168 manifest.predictions.len()
1169 );
1170 manifest.predictions.retain(|prediction| {
1171 !(introduced.contains(&prediction.invocation)
1172 && unpublished.contains(&prediction.action))
1173 });
1174 }
1175 }
1176 match self
1177 .put_remote_task_manifest(&task, manifest, state.remote_etag)
1178 .await
1179 {
1180 Ok(remote_manifest) => {
1181 let _write_guard = self.manifest_write_lock.lock().unwrap();
1182 let reconciliation = (|| {
1183 let _file_guard = self.lock_task_manifest(&task)?;
1184 let manifest = match self.load_task_manifest(&task)? {
1185 Some(local) => {
1186 merge_remote_task_manifest(&task, remote_manifest, local).0
1187 }
1188 None => remote_manifest,
1189 };
1190 self.persist_task_manifest(&manifest)
1191 })();
1192 if let Err(error) = reconciliation {
1193 warn!(
1194 "remote task action manifest reconciliation failed for {task}: {error}"
1195 );
1196 }
1197 }
1198 Err(error) => {
1199 self.note_remote_failure();
1200 warn!("remote task action manifest upload failed for {task}: {error}");
1201 }
1202 }
1203 }
1204 Ok(completed)
1205 }
1206
1207 fn task_manifest_path(&self, task: &str) -> PathBuf {
1208 self.manifest_dir.join(format!("{task}.json"))
1209 }
1210
1211 fn task_manifest_lock_path(&self, task: &str) -> PathBuf {
1212 self.manifest_dir.join("locks").join(format!("{task}.lock"))
1213 }
1214
1215 fn lock_task_manifest(&self, task: &str) -> Result<fslock::LockFile> {
1216 let path = self.task_manifest_lock_path(task);
1217 fs::create_dir_all(path.parent().expect("task manifest lock has a parent"))?;
1218 let mut lock = fslock::LockFile::open(&path)?;
1219 lock.lock()?;
1220 Ok(lock)
1221 }
1222
1223 fn load_task_manifest(&self, task: &str) -> Result<Option<TaskActionManifest>> {
1224 match fs::read(self.task_manifest_path(task)) {
1225 Ok(contents) => Ok(Some(self.parse_task_manifest(task, &contents, false)?)),
1226 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
1227 Err(error) => Err(error.into()),
1228 }
1229 }
1230
1231 fn parse_task_manifest(
1232 &self,
1233 task: &str,
1234 contents: &[u8],
1235 require_canonical: bool,
1236 ) -> Result<TaskActionManifest> {
1237 let manifest: TaskActionManifest = serde_json::from_slice(contents)?;
1238 validate_task_manifest(&manifest, task)?;
1239 if require_canonical && canonical_json(&manifest)? != contents {
1240 bail!("task action manifest is not canonical JSON");
1241 }
1242 Ok(manifest)
1243 }
1244
1245 fn task_manifest_selector(task: &str) -> Result<(Vec<u8>, CacheDigest)> {
1246 TaskActionManifest::selector(task)
1247 }
1248
1249 fn persist_task_manifest(&self, manifest: &TaskActionManifest) -> Result<()> {
1250 let bytes = canonical_json(manifest)?;
1251 fs::create_dir_all(self.manifest_dir.as_path())?;
1252 let mut temporary = tempfile::NamedTempFile::new_in(self.manifest_dir.as_path())?;
1253 std::io::Write::write_all(temporary.as_file_mut(), &bytes)?;
1254 temporary.as_file_mut().sync_all()?;
1255 temporary
1256 .persist(self.task_manifest_path(&manifest.task))
1257 .map_err(|error| error.error)?;
1258 Ok(())
1259 }
1260
1261 async fn get_remote_task_manifest(
1262 &self,
1263 task: &str,
1264 ) -> Result<Option<(TaskActionManifest, String)>> {
1265 let Some(remote) = &self.remote else {
1266 return Ok(None);
1267 };
1268 let (_, selector) = Self::task_manifest_selector(task)?;
1269 let _permit = self.remote_transfers.acquire().await?;
1270 self.stats
1271 .remote_manifest_lookups
1272 .fetch_add(1, Ordering::Relaxed);
1273 let _timer = AtomicDurationTimer::start(&self.stats.remote_manifest_lookup_duration_ns);
1274 let Some(remote_manifest) = remote.get_action_manifest(&selector).await? else {
1275 return Ok(None);
1276 };
1277 let manifest = self.parse_task_manifest(task, &remote_manifest.bytes, true)?;
1278 Ok(Some((manifest, remote_manifest.etag)))
1279 }
1280
1281 async fn put_remote_task_manifest(
1282 &self,
1283 task: &str,
1284 mut manifest: TaskActionManifest,
1285 mut expected_etag: Option<String>,
1286 ) -> Result<TaskActionManifest> {
1287 let Some(remote) = &self.remote else {
1288 return Ok(manifest);
1289 };
1290 let (_, selector) = Self::task_manifest_selector(task)?;
1291 for _ in 0..4 {
1292 let bytes = canonical_json(&manifest)?;
1293 let outcome = {
1294 let _permit = self.remote_transfers.acquire().await?;
1295 remote
1296 .put_action_manifest(&selector, &bytes, expected_etag.as_deref())
1297 .await?
1298 };
1299 match outcome {
1300 ManifestPutOutcome::Stored => return Ok(manifest),
1301 ManifestPutOutcome::PreconditionFailed => {
1302 let Some((remote_manifest, etag)) = self.get_remote_task_manifest(task).await?
1303 else {
1304 expected_etag = None;
1305 continue;
1306 };
1307 manifest = merge_task_manifests(task, Some(remote_manifest), manifest)?;
1308 expected_etag = Some(etag);
1309 }
1310 }
1311 }
1312 bail!("remote task action manifest changed too frequently")
1313 }
1314
1315 pub fn stats(&self) -> AgentStats {
1317 AgentStats {
1318 session_duration_ns: 0,
1319 lookups: self.stats.lookups.load(Ordering::Relaxed),
1320 unconsulted: self.stats.unconsulted.load(Ordering::Relaxed),
1321 hits: self.stats.hits.load(Ordering::Relaxed),
1322 stores: self.stats.stores.load(Ordering::Relaxed),
1323 stored_bytes: self.stats.stored_bytes.load(Ordering::Relaxed),
1324 verifications: self.stats.verifications.load(Ordering::Relaxed),
1325 divergences: self.stats.divergences.load(Ordering::Relaxed),
1326 downloaded_bytes: self.stats.downloaded_bytes.load(Ordering::Relaxed),
1327 uploaded_bytes: self.stats.uploaded_bytes.load(Ordering::Relaxed),
1328 background_uploads: self.stats.background_uploads.load(Ordering::Relaxed),
1329 background_upload_failures: self
1330 .stats
1331 .background_upload_failures
1332 .load(Ordering::Relaxed),
1333 remote_blob_pack_uploads: self.stats.remote_blob_pack_uploads.load(Ordering::Relaxed),
1334 remote_blob_pack_upload_blobs: self
1335 .stats
1336 .remote_blob_pack_upload_blobs
1337 .load(Ordering::Relaxed),
1338 upload_drain_duration_ns: self.stats.upload_drain_duration_ns.load(Ordering::Relaxed),
1339 prefetched_actions: self.stats.prefetched_actions.load(Ordering::Relaxed),
1340 predictions_loaded: self.stats.predictions_loaded.load(Ordering::Relaxed),
1341 bypasses: self.stats.bypasses.lock().unwrap().clone(),
1342 avoided_compiler_duration_ns: self
1343 .stats
1344 .avoided_compiler_duration_ns
1345 .load(Ordering::Relaxed),
1346 wrapper_phases_ns: self.stats.wrapper_phases_ns.lock().unwrap().clone(),
1347 compiler: self.stats.compiler.lock().unwrap().clone(),
1348 slow_compilations: self.stats.slow_compilations.lock().unwrap().clone(),
1349 unit_outcomes: self.stats.unit_outcomes.lock().unwrap().clone(),
1350 remote_failures: self.stats.remote_failures.load(Ordering::Relaxed),
1351 remote_manifest_lookups: self.stats.remote_manifest_lookups.load(Ordering::Relaxed),
1352 remote_manifest_lookup_duration_ns: self
1353 .stats
1354 .remote_manifest_lookup_duration_ns
1355 .load(Ordering::Relaxed),
1356 remote_action_lookups: self.stats.remote_action_lookups.load(Ordering::Relaxed),
1357 remote_action_lookup_duration_ns: self
1358 .stats
1359 .remote_action_lookup_duration_ns
1360 .load(Ordering::Relaxed),
1361 remote_blob_requests: self.stats.remote_blob_requests.load(Ordering::Relaxed),
1362 remote_blob_pack_requests: self.stats.remote_blob_pack_requests.load(Ordering::Relaxed),
1363 remote_blob_pack_blobs: self.stats.remote_blob_pack_blobs.load(Ordering::Relaxed),
1364 remote_blob_transfer_duration_ns: self
1365 .stats
1366 .remote_blob_transfer_duration_ns
1367 .load(Ordering::Relaxed),
1368 local_cas_write_duration_ns: self
1369 .stats
1370 .local_cas_write_duration_ns
1371 .load(Ordering::Relaxed),
1372 prefetch_runs: self.stats.prefetch_runs.load(Ordering::Relaxed),
1373 prefetch_duration_ns: self.stats.prefetch_duration_ns.load(Ordering::Relaxed),
1374 materialization_duration_ns: self
1375 .stats
1376 .materialization_duration_ns
1377 .load(Ordering::Relaxed),
1378 restored_output_files: self.stats.restored_output_files.load(Ordering::Relaxed),
1379 restored_output_bytes: self.stats.restored_output_bytes.load(Ordering::Relaxed),
1380 reflinked_output_files: self.stats.reflinked_output_files.load(Ordering::Relaxed),
1381 reflinked_output_bytes: self.stats.reflinked_output_bytes.load(Ordering::Relaxed),
1382 copied_output_files: self.stats.copied_output_files.load(Ordering::Relaxed),
1383 copied_output_bytes: self.stats.copied_output_bytes.load(Ordering::Relaxed),
1384 reused_output_files: self.stats.reused_output_files.load(Ordering::Relaxed),
1385 reused_output_bytes: self.stats.reused_output_bytes.load(Ordering::Relaxed),
1386 }
1387 }
1388
1389 pub async fn handle_requests(
1396 &self,
1397 requests: impl IntoIterator<Item = AgentRequest>,
1398 ) -> Vec<AgentResponse> {
1399 let mut connection = ConnectionUploads::default();
1400 let mut responses = Vec::new();
1401 for request in requests {
1402 responses.push(self.respond_on(request, &mut connection).await);
1403 }
1404 responses
1405 }
1406
1407 fn write_lock(&self, digest: &CacheDigest) -> Arc<tokio::sync::Mutex<()>> {
1408 Self::digest_lock(&self.write_locks, digest)
1409 }
1410
1411 fn action_lock(&self, digest: &CacheDigest) -> Arc<tokio::sync::Mutex<()>> {
1412 Self::digest_lock(&self.action_locks, digest)
1413 }
1414
1415 fn digest_lock(
1416 locks: &Mutex<BTreeMap<CacheDigest, Weak<tokio::sync::Mutex<()>>>>,
1417 digest: &CacheDigest,
1418 ) -> Arc<tokio::sync::Mutex<()>> {
1419 let mut locks = locks.lock().unwrap();
1420 locks.retain(|_, lock| lock.strong_count() > 0);
1421 if let Some(lock) = locks.get(digest).and_then(Weak::upgrade) {
1422 return lock;
1423 }
1424 let lock = Arc::new(tokio::sync::Mutex::new(()));
1425 locks.insert(digest.clone(), Arc::downgrade(&lock));
1426 lock
1427 }
1428
1429 #[cfg(test)]
1434 async fn respond(&self, request: AgentRequest) -> AgentResponse {
1435 self.respond_on(request, &mut ConnectionUploads::default())
1436 .await
1437 }
1438
1439 async fn respond_on(
1440 &self,
1441 request: AgentRequest,
1442 connection: &mut ConnectionUploads,
1443 ) -> AgentResponse {
1444 match &request {
1445 AgentRequest::FindActionPrediction { task, .. }
1446 | AgentRequest::RecordActionPrediction { task, .. } => {
1447 self.ensure_task_loaded(task).await;
1448 }
1449 _ => {}
1450 }
1451 let result = match request {
1452 AgentRequest::BeginTask { task } => self
1453 .begin_task(&task)
1454 .await
1455 .map(|run| AgentResponse::TaskBegun { run }),
1456 AgentRequest::CommitTask { run } => self
1457 .commit_task(&run)
1458 .await
1459 .map(|()| AgentResponse::TaskCommitted),
1460 AgentRequest::FindBlob { digest } => self.find_blob(&digest).await,
1461 AgentRequest::FindBlobs { digests } => self.find_blobs(digests).await,
1462 AgentRequest::StoreBlob { digest, source } => {
1463 self.store_blob(&digest, &source, connection).await
1464 }
1465 AgentRequest::FindActionResult { action } => {
1466 self.stats.lookups.fetch_add(1, Ordering::Relaxed);
1467 self.find_action_result(&action).await
1468 }
1469 AgentRequest::RecordActionHit {
1470 action,
1471 restore,
1472 crate_name,
1473 } => {
1474 let diagnostic = connection
1475 .take_action_diagnostic("hit", crate_name.as_deref())
1476 .map(|diagnostic| AgentEvent::ActionDiagnostic {
1477 outcome: "hit".into(),
1478 crate_name: crate_name.clone(),
1479 diagnostic,
1480 });
1481 self.record_action_hit(&action, restore, crate_name, diagnostic)
1482 }
1483 AgentRequest::RecordBypass { kind } => {
1484 *self
1485 .stats
1486 .bypasses
1487 .lock()
1488 .unwrap()
1489 .entry(kind.clone())
1490 .or_insert(0) += 1;
1491 self.emit(|| AgentEvent::Bypass { kind });
1492 Ok(AgentResponse::BypassRecorded)
1493 }
1494 AgentRequest::RecordUnconsulted => {
1495 self.stats.unconsulted.fetch_add(1, Ordering::Relaxed);
1496 self.emit(|| AgentEvent::Unconsulted);
1497 Ok(AgentResponse::UnconsultedRecorded)
1498 }
1499 AgentRequest::RecordWarning { message } => match parse_action_diagnostic(&message) {
1500 Some(Ok((outcome, crate_name, diagnostic))) => {
1501 connection.record_action_diagnostic(outcome, crate_name, diagnostic);
1502 Ok(AgentResponse::WarningRecorded)
1503 }
1504 Some(Err(error)) => Err(error),
1505 None => self.record_diagnostic(DiagnosticLevel::Warning, message),
1506 },
1507 AgentRequest::RecordDebug { target, message } => (|| {
1508 if target.is_empty()
1509 || target.len() > 256
1510 || target.contains(['\n', '\r', '\0'])
1511 || message.is_empty()
1512 || message.len() > MAX_DIAGNOSTIC_BYTES
1513 || message.contains(['\n', '\r', '\0'])
1514 {
1515 bail!("invalid shim debug record");
1516 }
1517 if target == "mbx::unit-outcome" {
1518 let (unit, outcome): (String, String) = serde_json::from_str(&message)?;
1519 if unit.len() > 256
1520 || !unit.contains(':')
1521 || !matches!(
1522 outcome.as_str(),
1523 "hit"
1524 | "miss"
1525 | "bypass"
1526 | "unconsulted"
1527 | "incremental"
1528 | "verification"
1529 )
1530 {
1531 bail!("invalid unit outcome");
1532 }
1533 self.stats
1534 .unit_outcomes
1535 .lock()
1536 .unwrap()
1537 .entry(unit)
1538 .or_default()
1539 .insert(outcome);
1540 } else {
1541 debug!(target: &target, "{message}");
1542 }
1543 Ok(AgentResponse::DebugRecorded)
1544 })(),
1545 AgentRequest::RecordError { message } => {
1546 self.record_diagnostic(DiagnosticLevel::Error, message)
1547 }
1548 AgentRequest::FindFileDigests { scope, files } => self.find_file_digests(scope, files),
1549 AgentRequest::JoinActionPromise {
1550 adapter,
1551 invocation,
1552 } => self.join_action_promise(&adapter, &invocation).await,
1553 AgentRequest::CompleteActionPromise { claim, prediction } => {
1554 self.complete_action_promise(&claim, &prediction).await
1555 }
1556 AgentRequest::ResolveFileDigests { scope, files } => {
1557 self.resolve_file_digests(scope, files).await
1558 }
1559 AgentRequest::RecordFileDigests { scope, entries } => {
1560 self.record_file_digests(scope, entries)
1561 }
1562 AgentRequest::RecordWrapperTiming { timing } => (|| {
1563 if timing.spans.len() > 512
1564 || timing.phases_ns.len() > 32
1565 || timing.adapter.len() > 64
1566 || timing.unit.as_ref().is_some_and(|s| s.len() > 1024)
1567 || timing.phases_ns.keys().any(|s| s.len() > 64)
1568 || timing.spans.iter().any(|s| s.name.len() > 64)
1569 {
1570 bail!("wrapper timing exceeds telemetry limits");
1571 }
1572 {
1573 let mut phases = self.stats.wrapper_phases_ns.lock().unwrap();
1574 for (phase, duration) in &timing.phases_ns {
1575 let total = phases.entry(phase.clone()).or_default();
1576 *total = total.saturating_add(*duration);
1577 }
1578 }
1579 self.emit(|| AgentEvent::WrapperTiming { timing });
1580 Ok(AgentResponse::WrapperTimingRecorded)
1581 })(),
1582 AgentRequest::RecordCompilerInvocation {
1583 outcome,
1584 crate_name,
1585 duration_ns,
1586 } => {
1587 let diagnostic = connection
1588 .take_action_diagnostic(&outcome, crate_name.as_deref())
1589 .map(|diagnostic| AgentEvent::ActionDiagnostic {
1590 outcome: outcome.clone(),
1591 crate_name: crate_name.clone(),
1592 diagnostic,
1593 });
1594 self.record_compiler_invocation(
1595 &outcome,
1596 crate_name.as_deref(),
1597 duration_ns,
1598 diagnostic,
1599 )
1600 }
1601 AgentRequest::RecordActionVerification { matched, restore } => {
1602 self.record_materialization(restore);
1603 self.stats.verifications.fetch_add(1, Ordering::Relaxed);
1604 if !matched {
1605 self.stats.divergences.fetch_add(1, Ordering::Relaxed);
1606 }
1607 self.emit(|| AgentEvent::Verification { matched, restore });
1608 Ok(AgentResponse::ActionVerificationRecorded)
1609 }
1610 AgentRequest::StoreActionResult { result } => {
1611 self.store_action_result(&result, connection).await
1612 }
1613 AgentRequest::FindActionPrediction { task, invocation } => {
1614 self.find_action_prediction(&task, &invocation)
1615 }
1616 AgentRequest::RecordActionPrediction { task, prediction } => {
1617 self.record_action_prediction(&task, prediction)
1618 }
1619 AgentRequest::FindExecutableIdentity {
1620 executable,
1621 environment,
1622 } => self.find_executable_identity(executable, environment),
1623 AgentRequest::StoreExecutableIdentity {
1624 executable,
1625 environment,
1626 stdout,
1627 pins,
1628 } => self.store_executable_identity(executable, environment, stdout, pins),
1629 AgentRequest::Hello { .. } => {
1630 Err(eyre::eyre!("hello is only valid as the first request"))
1631 }
1632 };
1633 result.unwrap_or_else(|error| AgentResponse::Error {
1634 message: error.to_string(),
1635 })
1636 }
1637
1638 async fn find_blob(&self, digest: &CacheDigest) -> Result<AgentResponse> {
1639 if let Some(path) = self.find_verified_blob(digest)? {
1640 return Ok(AgentResponse::Blob { path: Some(path) });
1641 }
1642 if !self.remote_mode.reads() {
1643 return Ok(AgentResponse::Blob { path: None });
1644 }
1645 let Some(remote) = &self.remote else {
1646 return Ok(AgentResponse::Blob { path: None });
1647 };
1648 match self.fetch_remote_blob(remote, digest).await {
1649 Ok(path) => Ok(AgentResponse::Blob { path: Some(path) }),
1650 Err(error) => {
1651 warn!(
1652 "remote cache blob lookup failed for {}: {error}",
1653 digest.hash
1654 );
1655 Ok(AgentResponse::Blob { path: None })
1656 }
1657 }
1658 }
1659
1660 async fn find_blobs(&self, digests: Vec<CacheDigest>) -> Result<AgentResponse> {
1661 let mut paths = BTreeMap::new();
1662 let mut missing = Vec::new();
1663 for digest in &digests {
1664 match self.find_verified_blob(digest)? {
1665 Some(path) => {
1666 paths.insert(digest.clone(), path);
1667 }
1668 None => {
1669 missing.push(digest.clone());
1670 }
1671 }
1672 }
1673
1674 if !missing.is_empty()
1675 && self.remote_mode.reads()
1676 && let Some(remote) = &self.remote
1677 {
1678 paths.extend(self.fetch_remote_blobs(remote, missing, None).await);
1679 }
1680
1681 Ok(AgentResponse::Blobs {
1682 paths: digests
1683 .into_iter()
1684 .map(|digest| paths.get(&digest).cloned())
1685 .collect(),
1686 })
1687 }
1688
1689 async fn store_blob(
1690 &self,
1691 digest: &CacheDigest,
1692 source: &Path,
1693 connection: &mut ConnectionUploads,
1694 ) -> Result<AgentResponse> {
1695 let path = {
1696 let lock = self.write_lock(digest);
1697 let _guard = lock.lock().await;
1698 if let Some(path) = self.find_verified_blob(digest)? {
1699 path
1700 } else {
1701 let path = self.cas.store_file(digest, source)?;
1702 self.remember_verified_blob(digest, &path);
1703 self.stats.stores.fetch_add(1, Ordering::Relaxed);
1704 self.stats
1705 .stored_bytes
1706 .fetch_add(digest.size, Ordering::Relaxed);
1707 path
1708 }
1709 };
1710 if let Some(uploads) = &self.uploads {
1713 uploads.queue_blob(digest, path.clone(), connection);
1714 }
1715 Ok(AgentResponse::Stored { path })
1716 }
1717
1718 fn find_verified_blob(&self, digest: &CacheDigest) -> Result<Option<PathBuf>> {
1719 let remembered = self.verified_blobs.lock().unwrap().get(digest).cloned();
1720 if let Some(remembered) = remembered {
1721 if remembered.is_unchanged() {
1732 return Ok(Some(remembered.path));
1733 }
1734 self.verified_blobs.lock().unwrap().remove(digest);
1735 }
1736 let path = self.cas.find(digest)?;
1737 if let Some(path) = &path {
1738 self.remember_verified_blob(digest, path);
1739 }
1740 Ok(path)
1741 }
1742
1743 fn remember_verified_blob(&self, digest: &CacheDigest, path: &Path) {
1744 let Some(verified) = VerifiedBlob::describe(path) else {
1745 return;
1749 };
1750 self.verified_blobs
1751 .lock()
1752 .unwrap()
1753 .insert(digest.clone(), verified);
1754 }
1755
1756 async fn find_action_result(&self, action: &CacheDigest) -> Result<AgentResponse> {
1757 if let Some(result) = self.actions.find(action)? {
1758 return Ok(AgentResponse::ActionResult {
1759 result: Some(result),
1760 });
1761 }
1762 if !self.remote_mode.reads() {
1763 return Ok(AgentResponse::ActionResult { result: None });
1764 }
1765 let Some(remote) = &self.remote else {
1766 return Ok(AgentResponse::ActionResult { result: None });
1767 };
1768 let lock = self.action_lock(action);
1769 let _guard = lock.lock().await;
1770 if let Some(result) = self.actions.find(action)? {
1771 return Ok(AgentResponse::ActionResult {
1772 result: Some(result),
1773 });
1774 }
1775 if let Some(result) = self
1776 .pending_remote_actions
1777 .lock()
1778 .unwrap()
1779 .get(action)
1780 .cloned()
1781 {
1782 return Ok(AgentResponse::ActionResult {
1783 result: Some(result),
1784 });
1785 }
1786 let _permit = self.remote_transfers.acquire().await?;
1787 match self.get_remote_action_result(remote, action).await {
1788 Ok(Some(result)) => {
1789 self.pending_remote_actions
1790 .lock()
1791 .unwrap()
1792 .insert(action.clone(), result.clone());
1793 Ok(AgentResponse::ActionResult {
1794 result: Some(result),
1795 })
1796 }
1797 Ok(None) => Ok(AgentResponse::ActionResult { result: None }),
1798 Err(error) => {
1799 self.note_remote_failure();
1800 warn!(
1801 "remote cache action lookup failed for {}: {error}",
1802 action.hash
1803 );
1804 Ok(AgentResponse::ActionResult { result: None })
1805 }
1806 }
1807 }
1808
1809 async fn store_action_result(
1810 &self,
1811 result: &RemoteActionResult,
1812 connection: &ConnectionUploads,
1813 ) -> Result<AgentResponse> {
1814 let path = self.actions.store(result)?;
1815 if let Some(uploads) = &self.uploads {
1816 uploads.queue_action_result(result, connection);
1817 }
1818 Ok(AgentResponse::ActionStored { path })
1819 }
1820
1821 async fn join_action_promise(
1822 &self,
1823 adapter: &str,
1824 invocation: &CacheDigest,
1825 ) -> Result<AgentResponse> {
1826 if !self.remote_mode.reads() || !self.remote_mode.writes() {
1827 return Ok(AgentResponse::ActionPromise {
1828 claim: None,
1829 prediction: None,
1830 });
1831 }
1832 let Some(remote) = &self.remote else {
1833 return Ok(AgentResponse::ActionPromise {
1834 claim: None,
1835 prediction: None,
1836 });
1837 };
1838 let deadline = Instant::now() + ACTION_PROMISE_WAIT;
1839 loop {
1840 let _permit = self.remote_transfers.acquire().await?;
1841 let state = remote.join_action_promise(invocation, adapter).await;
1842 drop(_permit);
1843 match state {
1844 Ok(Some(ActionPromiseState::Claimed { claim })) => {
1845 return Ok(AgentResponse::ActionPromise {
1846 claim: Some(claim),
1847 prediction: None,
1848 });
1849 }
1850 Ok(Some(ActionPromiseState::Complete { prediction })) => {
1851 return Ok(AgentResponse::ActionPromise {
1852 claim: None,
1853 prediction: Some(prediction),
1854 });
1855 }
1856 Ok(Some(ActionPromiseState::Pending { retry_after_ms }))
1857 if Instant::now() < deadline =>
1858 {
1859 tokio::time::sleep(Duration::from_millis(retry_after_ms.clamp(10, 5_000)))
1860 .await;
1861 }
1862 Ok(Some(ActionPromiseState::Pending { .. }) | None) => {
1863 return Ok(AgentResponse::ActionPromise {
1864 claim: None,
1865 prediction: None,
1866 });
1867 }
1868 Ok(Some(_)) => {
1869 return Ok(AgentResponse::ActionPromise {
1870 claim: None,
1871 prediction: None,
1872 });
1873 }
1874 Err(error) => {
1875 self.note_remote_failure();
1876 warn!(
1877 "remote cache action promise failed for {}: {error}",
1878 invocation.hash
1879 );
1880 return Ok(AgentResponse::ActionPromise {
1881 claim: None,
1882 prediction: None,
1883 });
1884 }
1885 }
1886 }
1887 }
1888
1889 async fn complete_action_promise(
1890 &self,
1891 claim: &str,
1892 prediction: &ActionPrediction,
1893 ) -> Result<AgentResponse> {
1894 prediction.validate()?;
1895 let Some(remote) = &self.remote else {
1896 return Ok(AgentResponse::ActionPromiseCompleted);
1897 };
1898 let Some(uploads) = &self.uploads else {
1899 return Ok(AgentResponse::ActionPromiseCompleted);
1900 };
1901 if uploads
1902 .wait_for_actions(std::slice::from_ref(&prediction.action))
1903 .await
1904 .contains(&prediction.action)
1905 {
1906 return Ok(AgentResponse::ActionPromiseCompleted);
1909 }
1910 let completion = ActionPromiseCompletion {
1911 claim: claim.to_string(),
1912 prediction: prediction.clone(),
1913 };
1914 let _permit = self.remote_transfers.acquire().await?;
1915 if let Err(error) = remote
1916 .complete_action_promise(&prediction.invocation, &completion)
1917 .await
1918 {
1919 self.note_remote_failure();
1920 warn!(
1921 "remote cache action promise completion failed for {}: {error}",
1922 prediction.invocation.hash
1923 );
1924 }
1925 Ok(AgentResponse::ActionPromiseCompleted)
1926 }
1927
1928 fn note_remote_failure(&self) {
1930 self.stats.remote_failures.fetch_add(1, Ordering::Relaxed);
1931 }
1932
1933 async fn get_remote_action_result(
1934 &self,
1935 remote: &RemoteCacheClient,
1936 action: &CacheDigest,
1937 ) -> Result<Option<RemoteActionResult>> {
1938 self.stats
1939 .remote_action_lookups
1940 .fetch_add(1, Ordering::Relaxed);
1941 let _timer = AtomicDurationTimer::start(&self.stats.remote_action_lookup_duration_ns);
1942 remote.get_action_result(action).await
1943 }
1944
1945 fn record_action_hit(
1946 &self,
1947 action: &CacheDigest,
1948 restore: RestoreStats,
1949 crate_name: Option<String>,
1950 diagnostic: Option<AgentEvent>,
1951 ) -> Result<AgentResponse> {
1952 validate_crate_name(crate_name.as_deref())?;
1953 if self.actions.find(action)?.is_none() {
1954 let pending = self.pending_remote_actions.lock().unwrap().remove(action);
1955 if let Some(result) = pending {
1956 self.actions.store(&result)?;
1957 } else {
1958 bail!("cannot record a hit for a missing action result");
1959 }
1960 }
1961 self.record_restore(restore);
1962 self.stats.hits.fetch_add(1, Ordering::Relaxed);
1963 self.emit_action(
1964 diagnostic,
1965 AgentEvent::ActionHit {
1966 crate_name,
1967 restore,
1968 },
1969 );
1970 Ok(AgentResponse::ActionHitRecorded)
1971 }
1972
1973 fn record_restore(&self, restore: RestoreStats) {
1974 self.record_materialization(restore);
1975 atomic_saturating_add(
1976 &self.stats.avoided_compiler_duration_ns,
1977 restore.avoided_compiler_duration_ns,
1978 );
1979 atomic_saturating_add(&self.stats.restored_output_files, restore.output_files);
1980 atomic_saturating_add(&self.stats.restored_output_bytes, restore.output_bytes);
1981 atomic_saturating_add(
1982 &self.stats.reflinked_output_files,
1983 restore.reflinked_output_files,
1984 );
1985 atomic_saturating_add(
1986 &self.stats.reflinked_output_bytes,
1987 restore.reflinked_output_bytes,
1988 );
1989 atomic_saturating_add(&self.stats.copied_output_files, restore.copied_output_files);
1990 atomic_saturating_add(&self.stats.copied_output_bytes, restore.copied_output_bytes);
1991 atomic_saturating_add(&self.stats.reused_output_files, restore.reused_output_files);
1992 atomic_saturating_add(&self.stats.reused_output_bytes, restore.reused_output_bytes);
1993 }
1994
1995 fn record_compiler_invocation(
1996 &self,
1997 outcome: &str,
1998 crate_name: Option<&str>,
1999 duration_ns: u64,
2000 diagnostic: Option<AgentEvent>,
2001 ) -> Result<AgentResponse> {
2002 if !matches!(
2003 outcome,
2004 "miss" | "unconsulted" | "bypass" | "verification" | "incremental"
2005 ) {
2006 bail!("invalid compiler invocation outcome");
2007 }
2008 validate_crate_name(crate_name)?;
2009 let mut compiler = self.stats.compiler.lock().unwrap();
2010 let stats = compiler.entry(outcome.to_string()).or_default();
2011 stats.invocations = stats.invocations.saturating_add(1);
2012 stats.duration_ns = stats.duration_ns.saturating_add(duration_ns);
2013 drop(compiler);
2014 if outcome != "verification"
2015 && let Some(crate_name) = crate_name.filter(|name| !name.is_empty())
2016 {
2017 let mut slow = self.stats.slow_compilations.lock().unwrap();
2018 let duration = slow.entry(crate_name.to_string()).or_default();
2019 *duration = duration.saturating_add(duration_ns);
2020 }
2021 self.emit_action(
2022 diagnostic,
2023 AgentEvent::CompilerInvocation {
2024 outcome: outcome.to_string(),
2025 crate_name: crate_name.map(str::to_string),
2026 duration_ns,
2027 },
2028 );
2029 Ok(AgentResponse::CompilerInvocationRecorded)
2030 }
2031
2032 fn record_materialization(&self, restore: RestoreStats) {
2033 atomic_saturating_add(&self.stats.materialization_duration_ns, restore.duration_ns);
2034 }
2035
2036 fn record_diagnostic(&self, level: DiagnosticLevel, message: String) -> Result<AgentResponse> {
2047 if message.is_empty()
2048 || message.len() > MAX_DIAGNOSTIC_BYTES
2049 || message.contains(['\n', '\r', '\0'])
2050 {
2051 bail!("invalid shim {}", level.label());
2052 }
2053 let mut diagnostics = self.diagnostics.lock().unwrap();
2054 let key = (level, message.clone());
2055 if !diagnostics.contains(&key) {
2056 if diagnostics.len() >= MAX_DIAGNOSTICS {
2060 bail!("shim diagnostic limit reached");
2061 }
2062 eprintln!("mbx[{}]: {message}", level.label());
2063 self.emit(|| match level {
2064 DiagnosticLevel::Warning => AgentEvent::Warning {
2065 message: message.clone(),
2066 },
2067 DiagnosticLevel::Error => AgentEvent::Error {
2068 message: message.clone(),
2069 },
2070 });
2071 diagnostics.insert(key);
2072 }
2073 Ok(match level {
2074 DiagnosticLevel::Warning => AgentResponse::WarningRecorded,
2075 DiagnosticLevel::Error => AgentResponse::ErrorRecorded,
2076 })
2077 }
2078
2079 fn find_file_digests(
2086 &self,
2087 scope: FileDigestScope,
2088 files: Vec<FileIdentity>,
2089 ) -> Result<AgentResponse> {
2090 if files.len() > MAX_FILE_DIGEST_BATCH {
2091 bail!("too many file-digest lookups in one request");
2092 }
2093 let ledger = self.file_digests.lock().unwrap();
2094 let digests = files
2095 .into_iter()
2096 .map(|file| {
2097 let recorded = ledger.get(&(scope, file.path.clone()))?;
2098 (recorded.file == file).then(|| recorded.digest.clone())
2099 })
2100 .collect();
2101 Ok(AgentResponse::FileDigests { digests })
2102 }
2103
2104 async fn resolve_file_digests(
2107 &self,
2108 scope: FileDigestScope,
2109 files: Vec<FileIdentity>,
2110 ) -> Result<AgentResponse> {
2111 if files.len() > MAX_FILE_DIGEST_BATCH {
2112 bail!("too many file-digest resolutions in one request");
2113 }
2114 for file in &files {
2115 if !file.path.is_absolute() {
2116 bail!("file-digest resolutions need absolute paths");
2117 }
2118 }
2119 let resolutions = stream::iter(
2120 files
2121 .into_iter()
2122 .map(|file| async move { self.resolve_file_digest(scope, file).await }),
2123 )
2124 .buffered(MAX_CONCURRENT_FILE_DIGESTS)
2125 .collect()
2126 .await;
2127 Ok(AgentResponse::FileDigestsResolved { resolutions })
2128 }
2129
2130 async fn resolve_file_digest(
2131 &self,
2132 scope: FileDigestScope,
2133 file: FileIdentity,
2134 ) -> FileDigestResolution {
2135 let lock = {
2136 let key = (scope, file.clone());
2137 let mut locks = self.file_digest_locks.lock().unwrap();
2138 locks.retain(|_, lock| lock.strong_count() > 0);
2139 if let Some(lock) = locks.get(&key).and_then(Weak::upgrade) {
2140 lock
2141 } else {
2142 let lock = Arc::new(FileDigestFlight {
2143 lock: tokio::sync::Mutex::new(()),
2144 resolution: Mutex::new(None),
2145 });
2146 locks.insert(key, Arc::downgrade(&lock));
2147 lock
2148 }
2149 };
2150 let _flight = lock.lock.lock().await;
2151 if let Some(resolution) = lock.resolution.lock().unwrap().clone() {
2152 return resolution;
2153 }
2154 if let Some(digest) = self
2155 .file_digests
2156 .lock()
2157 .unwrap()
2158 .get(&(scope, file.path.clone()))
2159 .filter(|recorded| recorded.file == file)
2160 .map(|recorded| recorded.digest.clone())
2161 {
2162 return FileDigestResolution::Digest(digest);
2163 }
2164 let Ok(_permit) = self.file_digest_permits.acquire().await else {
2165 return FileDigestResolution::Unresolved;
2166 };
2167 #[cfg(test)]
2168 self.file_digest_reads.fetch_add(1, Ordering::Relaxed);
2169 let path = file.path.clone();
2170 let resolved = tokio::task::spawn_blocking(move || {
2171 let resolution = digest_file(scope, &path)?;
2172 let current = std::fs::metadata(&path)
2173 .and_then(|metadata| FileIdentity::for_digest_cache(&path, &metadata));
2174 Ok::<_, std::io::Error>((resolution, current?))
2175 })
2176 .await;
2177 let Ok(Ok((resolution, current))) = resolved else {
2178 return FileDigestResolution::Unresolved;
2179 };
2180 let resolution = match resolution {
2181 FileDigestResolution::Digest(digest)
2182 if current.as_ref() == Some(&file) && digest.size == file.len =>
2183 {
2184 let _ = self.record_file_digests(
2185 scope,
2186 vec![RecordedFileDigest {
2187 file: file.clone(),
2188 digest: digest.clone(),
2189 }],
2190 );
2191 FileDigestResolution::Digest(digest)
2192 }
2193 FileDigestResolution::EmbeddedTimestampMacro if current.as_ref() == Some(&file) => {
2194 FileDigestResolution::EmbeddedTimestampMacro
2195 }
2196 FileDigestResolution::Digest(_)
2197 | FileDigestResolution::EmbeddedTimestampMacro
2198 | FileDigestResolution::Unresolved => FileDigestResolution::Unresolved,
2199 };
2200 *lock.resolution.lock().unwrap() = Some(resolution.clone());
2201 resolution
2202 }
2203
2204 fn record_file_digests(
2210 &self,
2211 scope: FileDigestScope,
2212 entries: Vec<RecordedFileDigest>,
2213 ) -> Result<AgentResponse> {
2214 if entries.len() > MAX_FILE_DIGEST_BATCH {
2215 bail!("too many file-digest records in one request");
2216 }
2217 for entry in &entries {
2218 validate_file_digest_record(entry)?;
2219 }
2220 let mut ledger = self.file_digests.lock().unwrap();
2221 for entry in entries {
2222 if ledger.len() >= MAX_FILE_DIGEST_ENTRIES
2223 && !ledger.contains_key(&(scope, entry.file.path.clone()))
2224 {
2225 break;
2226 }
2227 ledger.insert((scope, entry.file.path.clone()), entry);
2228 }
2229 Ok(AgentResponse::FileDigestsRecorded)
2230 }
2231
2232 pub fn seed_file_digests(&self, entries: Vec<(FileDigestScope, RecordedFileDigest)>) -> usize {
2241 self.seed_file_digests_with(|| entries)
2242 }
2243
2244 pub fn seed_file_digests_with(
2250 &self,
2251 load: impl FnOnce() -> Vec<(FileDigestScope, RecordedFileDigest)>,
2252 ) -> usize {
2253 let mut ledger = self.file_digests.lock().unwrap();
2254 let entries = load();
2255 let mut seeded = 0;
2256 for (scope, entry) in entries {
2257 if ledger.len() >= MAX_FILE_DIGEST_ENTRIES {
2258 break;
2259 }
2260 if validate_file_digest_record(&entry).is_err() {
2261 continue;
2262 }
2263 if let std::collections::btree_map::Entry::Vacant(slot) =
2264 ledger.entry((scope, entry.file.path.clone()))
2265 {
2266 slot.insert(entry);
2267 seeded += 1;
2268 }
2269 }
2270 seeded
2271 }
2272
2273 pub fn file_digests(&self) -> Vec<(FileDigestScope, RecordedFileDigest)> {
2276 self.file_digests
2277 .lock()
2278 .unwrap()
2279 .iter()
2280 .map(|((scope, _), entry)| (*scope, entry.clone()))
2281 .collect()
2282 }
2283
2284 fn find_action_prediction(
2285 &self,
2286 task: &str,
2287 invocation: &CacheDigest,
2288 ) -> Result<AgentResponse> {
2289 validate_task_identity(task)?;
2290 invocation.validate()?;
2291 let (prediction, prefetch) = {
2292 let mut tasks = self.task_actions.lock().unwrap();
2293 let Some(state) = tasks.get_mut(task) else {
2294 return Ok(AgentResponse::ActionPrediction { prediction: None });
2295 };
2296 activate_prediction_adapter(state, invocation)
2297 };
2298 if let Some(prefetch) = prefetch {
2299 self.spawn_prefetch_predictions(prefetch);
2300 }
2301 Ok(AgentResponse::ActionPrediction { prediction })
2302 }
2303
2304 fn record_action_prediction(
2305 &self,
2306 task: &str,
2307 prediction: ActionPrediction,
2308 ) -> Result<AgentResponse> {
2309 validate_task_identity(task)?;
2310 prediction.validate()?;
2311 let mut tasks = self.task_actions.lock().unwrap();
2312 let state = tasks.entry(task.to_string()).or_default();
2313 if !state.predictions.contains_key(&prediction.invocation)
2314 && state.predictions.len() >= MAX_TASK_ACTION_PREDICTIONS
2315 {
2316 bail!("task action manifest contains too many predictions");
2317 }
2318 let current = state.predictions.get(&prediction.invocation);
2319 if current != Some(&prediction) {
2320 match state.changed_predictions.get(&prediction.invocation) {
2321 Some(baseline) if baseline.as_ref() == Some(&prediction) => {
2323 state.changed_predictions.remove(&prediction.invocation);
2324 }
2325 Some(_) => {}
2327 None => {
2328 state
2329 .changed_predictions
2330 .insert(prediction.invocation.clone(), current.cloned());
2331 }
2332 }
2333 }
2334 state
2335 .predictions
2336 .insert(prediction.invocation.clone(), prediction.clone());
2337 state
2338 .pending_predictions
2339 .insert(prediction.invocation.clone(), prediction);
2340 Ok(AgentResponse::ActionPredictionRecorded)
2341 }
2342
2343 fn executable_identity_key(
2344 &self,
2345 executable: PathBuf,
2346 environment: BTreeMap<String, Option<String>>,
2347 ) -> Result<ExecutableIdentityKey> {
2348 if !environment.keys().all(|name| {
2354 matches!(
2355 name.as_str(),
2356 "COMPILER_PATH"
2357 | "GCC_EXEC_PREFIX"
2358 | "LIBRARY_PATH"
2359 | "MBX_FUSE_LD"
2360 | "PATH"
2361 | "RUSTUP_HOME"
2362 | "RUSTUP_TOOLCHAIN"
2363 | "SDKROOT"
2364 | "MACOSX_DEPLOYMENT_TARGET"
2365 | "LIB"
2366 | "UCRTVersion"
2367 | "UniversalCRTSdkDir"
2368 | "VCToolsInstallDir"
2369 | "VCToolsVersion"
2370 | "WindowsSdkDir"
2371 | "WindowsSDKVersion"
2372 )
2373 }) {
2374 bail!("executable identity contains an unsupported environment variable");
2375 }
2376 Ok(ExecutableIdentityKey {
2377 executable,
2378 environment,
2379 })
2380 }
2381
2382 fn find_executable_identity(
2383 &self,
2384 executable: PathBuf,
2385 environment: BTreeMap<String, Option<String>>,
2386 ) -> Result<AgentResponse> {
2387 let key = self.executable_identity_key(executable, environment)?;
2388 let remembered = self
2389 .executable_identities
2390 .lock()
2391 .unwrap()
2392 .get(&key)
2393 .cloned();
2394 let stdout = match remembered {
2395 Some(stdout) => Some(stdout),
2396 None => {
2397 let persisted = self.load_persisted_identity(&key);
2398 if let Some(stdout) = &persisted {
2399 let _ = self.remember_executable_identity(&key, stdout.clone());
2402 }
2403 persisted
2404 }
2405 };
2406 Ok(AgentResponse::ExecutableIdentity { stdout })
2407 }
2408
2409 fn store_executable_identity(
2410 &self,
2411 executable: PathBuf,
2412 environment: BTreeMap<String, Option<String>>,
2413 stdout: Vec<u8>,
2414 pins: Vec<PinnedFile>,
2415 ) -> Result<AgentResponse> {
2416 if stdout.len() > MAX_EXECUTABLE_IDENTITY_SIZE {
2417 bail!("executable identity exceeds {MAX_EXECUTABLE_IDENTITY_SIZE} bytes");
2418 }
2419 let key = self.executable_identity_key(executable, environment)?;
2420 self.remember_executable_identity(&key, stdout.clone())?;
2421 if pins.is_empty() {
2426 } else if !pins.iter().all(PinnedFile::holds) {
2427 debug!("executable identity was not persisted: a pinned file changed during the probe");
2428 } else if let Err(error) = self.persist_executable_identity(&key, pins, &stdout) {
2429 debug!("executable identity was not persisted: {error:#}");
2430 }
2431 Ok(AgentResponse::ExecutableIdentity {
2432 stdout: Some(stdout),
2433 })
2434 }
2435
2436 fn remember_executable_identity(
2437 &self,
2438 key: &ExecutableIdentityKey,
2439 stdout: Vec<u8>,
2440 ) -> Result<()> {
2441 let mut identities = self.executable_identities.lock().unwrap();
2442 let is_new = !identities.contains_key(key);
2443 let previous_size = identities.get(key).map_or(0, Vec::len);
2444 if is_new && identities.len() >= MAX_EXECUTABLE_IDENTITIES {
2445 bail!("executable identity cache contains too many entries");
2446 }
2447 let retained_bytes = identities.values().map(Vec::len).sum::<usize>();
2448 if retained_bytes - previous_size + stdout.len() > MAX_EXECUTABLE_IDENTITY_BYTES {
2449 bail!("executable identity cache contains too many bytes");
2450 }
2451 identities.insert(key.clone(), stdout);
2452 Ok(())
2453 }
2454
2455 fn persisted_identity_path(&self, key: &ExecutableIdentityKey) -> Result<PathBuf> {
2456 let selector = serde_json::to_vec(&(&key.executable, &key.environment))?;
2457 let digest = CacheDigest::blake3(&selector);
2458 Ok(self.identity_dir.join(format!("{}.json", digest.hash)))
2459 }
2460
2461 fn load_persisted_identity(&self, key: &ExecutableIdentityKey) -> Option<Vec<u8>> {
2464 let path = self.persisted_identity_path(key).ok()?;
2465 let bytes = fs::read(path).ok()?;
2466 let persisted: PersistedExecutableIdentity = serde_json::from_slice(&bytes).ok()?;
2467 if persisted.version != PERSISTED_EXECUTABLE_IDENTITY_VERSION
2468 || persisted.executable != key.executable
2469 || persisted.environment != key.environment
2470 || persisted.pins.is_empty()
2471 || persisted.stdout.len() > MAX_EXECUTABLE_IDENTITY_SIZE
2472 {
2473 return None;
2474 }
2475 persisted
2476 .pins
2477 .iter()
2478 .all(PinnedFile::holds)
2479 .then(|| persisted.stdout.into_bytes())
2480 }
2481
2482 fn persist_executable_identity(
2483 &self,
2484 key: &ExecutableIdentityKey,
2485 pins: Vec<PinnedFile>,
2486 stdout: &[u8],
2487 ) -> Result<()> {
2488 let Ok(stdout) = std::str::from_utf8(stdout) else {
2490 return Ok(());
2491 };
2492 let persisted = PersistedExecutableIdentity {
2493 version: PERSISTED_EXECUTABLE_IDENTITY_VERSION,
2494 executable: key.executable.clone(),
2495 environment: key.environment.clone(),
2496 pins,
2497 stdout: stdout.to_owned(),
2498 };
2499 let path = self.persisted_identity_path(key)?;
2500 fs::create_dir_all(self.identity_dir.as_path())?;
2501 let mut temporary = tempfile::NamedTempFile::new_in(self.identity_dir.as_path())?;
2503 std::io::Write::write_all(temporary.as_file_mut(), &serde_json::to_vec(&persisted)?)?;
2504 temporary.persist(path).map_err(|error| error.error)?;
2505 Ok(())
2506 }
2507
2508 pub async fn handle_connection<S>(&self, stream: S) -> Result<()>
2510 where
2511 S: AsyncRead + AsyncWrite + Unpin,
2512 {
2513 let (reader, mut writer) = tokio::io::split(stream);
2514 let mut reader = BufReader::new(reader);
2515 let hello = read_request(&mut reader)
2516 .await?
2517 .ok_or_else(|| eyre::eyre!("connection closed before the agent handshake"))?;
2518 let request: AgentRequest = serde_json::from_str(&hello)?;
2519 match request {
2520 AgentRequest::Hello {
2521 protocol,
2522 client_version,
2523 } if protocol == AGENT_PROTOCOL_VERSION && client_version == self.version.as_ref() => {}
2524 AgentRequest::Hello { protocol, .. } if protocol != AGENT_PROTOCOL_VERSION => {
2525 send_response(
2526 &mut writer,
2527 &AgentResponse::Error {
2528 message: format!(
2529 "unsupported agent protocol {protocol}; expected {AGENT_PROTOCOL_VERSION}"
2530 ),
2531 },
2532 )
2533 .await?;
2534 return Ok(());
2535 }
2536 AgentRequest::Hello { client_version, .. } => {
2537 send_response(
2538 &mut writer,
2539 &AgentResponse::Error {
2540 message: format!(
2541 "cache client {client_version} does not match agent {}",
2542 self.version
2543 ),
2544 },
2545 )
2546 .await?;
2547 return Ok(());
2548 }
2549 _ => bail!("the first agent request must be hello"),
2550 }
2551 send_response(
2552 &mut writer,
2553 &AgentResponse::Hello {
2554 protocol: AGENT_PROTOCOL_VERSION,
2555 agent_version: self.version.to_string(),
2556 },
2557 )
2558 .await?;
2559
2560 let mut connection = ConnectionUploads::default();
2564 while let Some(line) = read_request(&mut reader).await? {
2565 let response = match serde_json::from_str(&line) {
2566 Ok(request) => self.respond_on(request, &mut connection).await,
2567 Err(error) => AgentResponse::Error {
2568 message: format!("invalid agent request: {error}"),
2569 },
2570 };
2571 send_response(&mut writer, &response).await?;
2572 }
2573 Ok(())
2574 }
2575}
2576
2577async fn read_request<R>(reader: &mut R) -> Result<Option<String>>
2583where
2584 R: AsyncBufRead + Unpin,
2585{
2586 let mut line = Vec::new();
2587 loop {
2588 let available = reader.fill_buf().await?;
2589 if available.is_empty() {
2590 break;
2591 }
2592 let (consumed, complete) = match available.iter().position(|byte| *byte == b'\n') {
2593 Some(index) => (index, true),
2594 None => (available.len(), false),
2595 };
2596 if line.len() + consumed > MAX_REQUEST_BYTES {
2597 bail!("agent request exceeded {MAX_REQUEST_BYTES} bytes");
2598 }
2599 line.extend_from_slice(&available[..consumed]);
2600 reader.consume(consumed + usize::from(complete));
2602 if complete {
2603 return Ok(Some(String::from_utf8(line)?));
2604 }
2605 }
2606 if line.is_empty() {
2607 Ok(None)
2608 } else {
2609 Ok(Some(String::from_utf8(line)?))
2610 }
2611}
2612
2613async fn send_response(
2614 writer: &mut (impl AsyncWrite + Unpin),
2615 response: &AgentResponse,
2616) -> Result<()> {
2617 let mut encoded = serde_json::to_vec(response)?;
2618 encoded.push(b'\n');
2619 writer.write_all(&encoded).await?;
2620 writer.flush().await?;
2621 Ok(())
2622}
2623
2624#[cfg(test)]
2625#[path = "agent_tests.rs"]
2626mod tests;