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