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