1use crate::uploads::{ConnectionUploads, UploadQueue, UploadSink};
2use crate::{
3 ActionPrediction, ActionPromiseCompletion, ActionPromiseState, BlobPackLimits, CacheDigest,
4 CacheDirectory, LocalActionCache, LocalCas, MAX_STAGED_BLOB_PACK_BYTES,
5 MAX_STAGED_BLOB_PACK_ITEMS, ManifestPutOutcome, RemoteActionResult, RemoteCacheClient,
6 RemoteCacheMode, RustcMetadata, TaskActionManifest, blob_pack_chunk, canonical_json,
7};
8use eyre::{Context, Result, bail};
9use futures_util::{FutureExt, StreamExt, future::BoxFuture, stream};
10use log::warn;
11use std::collections::{BTreeMap, BTreeSet};
12use std::fs;
13use std::path::{Path, PathBuf};
14use std::sync::atomic::{AtomicU64, Ordering};
15use std::sync::{Arc, Mutex, Weak};
16use std::time::{Duration, Instant, SystemTime};
17
18const ACTION_PROMISE_WAIT: Duration = Duration::from_secs(60 * 60);
22use tokio::io::{AsyncBufRead, AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
23
24mod file_digest;
25mod manifest;
26mod prefetch;
27mod stats;
28mod wire;
29
30#[cfg(test)]
31pub(crate) use prefetch::select_prefetch_actions;
32
33pub use file_digest::{
34 FileDigestCache, FileDigestScope, FileIdentity, NoFileDigestCache, RecordedFileDigest,
35};
36pub use manifest::{is_task_identity, task_manifest_actions};
37use manifest::{
38 merge_remote_task_manifest, merge_task_manifests, task_manifest_dir, validate_task_identity,
39 validate_task_manifest,
40};
41pub use stats::{AgentStats, CompilerStats};
42use wire::MAX_REQUEST_BYTES;
43pub use wire::{
44 AGENT_PROTOCOL_VERSION, ActionDiagnostic, AgentEvent, AgentEventObserver, AgentRequest,
45 AgentResponse, RestoreStats,
46};
47
48const MAX_EXECUTABLE_IDENTITIES: usize = 64;
49const MAX_EXECUTABLE_IDENTITY_SIZE: usize = 64 * 1024;
50const MAX_EXECUTABLE_IDENTITY_BYTES: usize = 256 * 1024;
51const TASK_ACTION_MANIFEST_VERSION: u8 = 1;
52const MAX_TASK_ACTION_PREDICTIONS: usize = 16 * 1024;
53const MAX_WARNING_BYTES: usize = 4 * 1024;
55const MAX_WARNINGS: usize = 128;
61const ACTION_DIAGNOSTIC_PREFIX: &str = "@mbx-action-diagnostic\t";
62const MAX_FILE_DIGEST_BATCH: usize = 16 * 1024;
64const MAX_FILE_DIGEST_ENTRIES: usize = 1024 * 1024;
69const MAX_REMOTE_TRANSFERS: usize = 64;
70const MAX_PREFETCH_TRANSFERS: usize = 48;
71const MAX_PREFETCH_ACTIONS: usize = 256;
81const MAX_PREFETCH_ACTION_WAVE: usize = 32;
86const MAX_PREFETCH_BATCH_LOOKUPS: usize = 1;
92const PREFETCH_ACTION_BATCH_DELAY: Duration = Duration::from_millis(5);
93const MAX_PREFETCH_DIRECTORY_OBJECTS: usize = 100_000;
94const MAX_PREFETCH_OBJECTS_PER_WAVE: usize = 100_000;
95const DEFAULT_MAX_REMOTE_DOWNLOAD_BYTES: u64 = 5 * 1024 * 1024 * 1024;
96
97pub struct AgentRemoteCache {
99 pub client: RemoteCacheClient,
101 pub mode: RemoteCacheMode,
103 pub staging_dir: PathBuf,
105}
106
107#[derive(Default)]
108struct AtomicAgentStats {
109 lookups: AtomicU64,
110 unconsulted: AtomicU64,
111 hits: AtomicU64,
112 stores: AtomicU64,
113 stored_bytes: AtomicU64,
114 verifications: AtomicU64,
115 divergences: AtomicU64,
116 downloaded_bytes: AtomicU64,
117 uploaded_bytes: AtomicU64,
118 background_uploads: AtomicU64,
119 background_upload_failures: AtomicU64,
120 remote_blob_pack_uploads: AtomicU64,
121 remote_blob_pack_upload_blobs: AtomicU64,
122 upload_drain_duration_ns: AtomicU64,
123 prefetched_actions: AtomicU64,
124 predictions_loaded: AtomicU64,
125 remote_failures: AtomicU64,
126 remote_manifest_lookups: AtomicU64,
127 remote_manifest_lookup_duration_ns: AtomicU64,
128 remote_action_lookups: AtomicU64,
129 remote_action_lookup_duration_ns: AtomicU64,
130 remote_blob_requests: AtomicU64,
131 remote_blob_pack_requests: AtomicU64,
132 remote_blob_pack_blobs: AtomicU64,
133 remote_blob_transfer_duration_ns: AtomicU64,
134 local_cas_write_duration_ns: AtomicU64,
135 prefetch_runs: AtomicU64,
136 prefetch_duration_ns: AtomicU64,
137 materialization_duration_ns: AtomicU64,
138 bypasses: Mutex<BTreeMap<String, u64>>,
139 avoided_compiler_duration_ns: AtomicU64,
140 compiler: Mutex<BTreeMap<String, CompilerStats>>,
141 slow_compilations: Mutex<BTreeMap<String, u64>>,
142 restored_output_files: AtomicU64,
143 restored_output_bytes: AtomicU64,
144 reflinked_output_files: AtomicU64,
145 reflinked_output_bytes: AtomicU64,
146 copied_output_files: AtomicU64,
147 copied_output_bytes: AtomicU64,
148 reused_output_files: AtomicU64,
149 reused_output_bytes: AtomicU64,
150}
151
152struct AtomicDurationTimer<'a> {
153 started: Instant,
154 target: &'a AtomicU64,
155}
156
157impl<'a> AtomicDurationTimer<'a> {
158 fn start(target: &'a AtomicU64) -> Self {
159 Self {
160 started: Instant::now(),
161 target,
162 }
163 }
164}
165
166impl Drop for AtomicDurationTimer<'_> {
167 fn drop(&mut self) {
168 atomic_saturating_add(self.target, duration_ns(self.started));
169 }
170}
171
172fn duration_ns(started: Instant) -> u64 {
173 started.elapsed().as_nanos().try_into().unwrap_or(u64::MAX)
174}
175
176fn validate_crate_name(crate_name: Option<&str>) -> Result<()> {
177 if let Some(crate_name) = crate_name
178 && (crate_name.len() > 256 || crate_name.contains(['\0', '\n', '\r']))
179 {
180 bail!("invalid compiler crate name");
181 }
182 Ok(())
183}
184
185#[derive(serde::Deserialize)]
186struct ActionDiagnosticEnvelope {
187 outcome: String,
188 crate_name: Option<String>,
189 diagnostic: ActionDiagnostic,
190}
191
192fn parse_action_diagnostic(
193 message: &str,
194) -> Option<Result<(String, Option<String>, ActionDiagnostic)>> {
195 let payload = message.strip_prefix(ACTION_DIAGNOSTIC_PREFIX)?;
196 Some((|| {
197 let envelope: ActionDiagnosticEnvelope = serde_json::from_str(payload)?;
198 if !matches!(envelope.outcome.as_str(), "hit" | "miss") {
199 bail!("invalid action diagnostic outcome");
200 }
201 validate_crate_name(envelope.crate_name.as_deref())?;
202 Ok((envelope.outcome, envelope.crate_name, envelope.diagnostic))
203 })())
204}
205
206fn atomic_saturating_add(target: &AtomicU64, value: u64) {
207 let _ = target.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
208 Some(current.saturating_add(value))
209 });
210}
211
212fn queue_prefetch_digest(
213 verified: &BTreeMap<CacheDigest, PathBuf>,
214 pending: &mut BTreeMap<CacheDigest, ()>,
215 digest: CacheDigest,
216) {
217 if verified.contains_key(&digest) || pending.contains_key(&digest) {
218 return;
219 }
220 pending.insert(digest, ());
221}
222
223fn queue_prefetch_directory(
224 seen: &BTreeMap<CacheDigest, ()>,
225 pending: &mut BTreeMap<CacheDigest, ()>,
226 digest: CacheDigest,
227 limit: usize,
228) -> bool {
229 if seen.contains_key(&digest) || pending.contains_key(&digest) {
230 return true;
231 }
232 if seen.len().saturating_add(pending.len()) >= limit {
233 return false;
234 }
235 pending.insert(digest, ());
236 true
237}
238
239#[derive(Clone)]
245pub struct CacheAgent {
246 cas: LocalCas,
247 actions: LocalActionCache,
248 verified_blobs: Arc<Mutex<BTreeMap<CacheDigest, VerifiedBlob>>>,
249 version: Arc<str>,
250 write_locks: Arc<Mutex<BTreeMap<CacheDigest, Weak<tokio::sync::Mutex<()>>>>>,
251 action_locks: Arc<Mutex<BTreeMap<CacheDigest, Weak<tokio::sync::Mutex<()>>>>>,
252 stats: Arc<AtomicAgentStats>,
253 observer: Option<Arc<dyn AgentEventObserver>>,
254 observer_emission: Arc<Mutex<()>>,
255 executable_identities: Arc<Mutex<BTreeMap<ExecutableIdentityKey, Vec<u8>>>>,
256 manifest_dir: Arc<PathBuf>,
257 task_actions: Arc<Mutex<BTreeMap<String, TaskActionState>>>,
258 next_task_run: Arc<AtomicU64>,
259 manifest_write_lock: Arc<Mutex<()>>,
260 remote: Option<Arc<RemoteCacheClient>>,
261 remote_mode: RemoteCacheMode,
262 remote_staging_dir: Arc<PathBuf>,
263 remote_download_limit: u64,
264 remote_download_bytes: Arc<AtomicU64>,
265 pending_remote_actions: Arc<Mutex<BTreeMap<CacheDigest, RemoteActionResult>>>,
266 remote_transfers: Arc<tokio::sync::Semaphore>,
267 prefetch_transfers: Arc<tokio::sync::Semaphore>,
268 prefetch_tasks: Arc<Mutex<Vec<tokio::task::JoinHandle<()>>>>,
269 warnings: Arc<Mutex<BTreeSet<String>>>,
272 file_digests: Arc<Mutex<BTreeMap<(FileDigestScope, PathBuf), RecordedFileDigest>>>,
275 uploads: Option<UploadQueue>,
277}
278
279struct AgentUploadSink {
281 stats: Arc<AtomicAgentStats>,
282}
283
284impl UploadSink for AgentUploadSink {
285 fn record_blob_uploaded(&self, bytes: u64) {
286 self.stats
287 .background_uploads
288 .fetch_add(1, Ordering::Relaxed);
289 self.stats
290 .uploaded_bytes
291 .fetch_add(bytes, Ordering::Relaxed);
292 }
293
294 fn record_action_uploaded(&self) {
295 self.stats
296 .background_uploads
297 .fetch_add(1, Ordering::Relaxed);
298 }
299
300 fn record_blob_pack_uploaded(&self, blobs: u64) {
301 self.stats
302 .remote_blob_pack_uploads
303 .fetch_add(1, Ordering::Relaxed);
304 self.stats
305 .remote_blob_pack_upload_blobs
306 .fetch_add(blobs, Ordering::Relaxed);
307 }
308
309 fn record_upload_failure(&self) {
310 self.stats
311 .background_upload_failures
312 .fetch_add(1, Ordering::Relaxed);
313 self.stats.remote_failures.fetch_add(1, Ordering::Relaxed);
314 }
315}
316
317#[derive(Debug, Clone)]
323struct VerifiedBlob {
324 path: PathBuf,
325 len: u64,
326 modified: SystemTime,
327}
328
329impl VerifiedBlob {
330 fn describe(path: &Path) -> Option<Self> {
333 let metadata = std::fs::metadata(path).ok()?;
334 Some(Self {
335 path: path.to_path_buf(),
336 len: metadata.len(),
337 modified: metadata.modified().ok()?,
338 })
339 }
340
341 fn is_unchanged(&self) -> bool {
343 let Ok(metadata) = std::fs::metadata(&self.path) else {
344 return false;
345 };
346 metadata.len() == self.len && metadata.modified().is_ok_and(|now| now == self.modified)
347 }
348}
349
350#[derive(Debug, Clone, Default)]
351struct TaskActionState {
352 manifest: String,
353 baseline_loaded: bool,
354 predictions: BTreeMap<CacheDigest, ActionPrediction>,
355 pending_predictions: BTreeMap<CacheDigest, ActionPrediction>,
356 remote_etag: Option<String>,
357}
358
359struct PrefetchedAction {
360 adapter: String,
361 result: RemoteActionResult,
362}
363
364struct RemoteDownloadReservation {
365 counter: Arc<AtomicU64>,
366 reserved: u64,
367 committed: bool,
368}
369
370impl RemoteDownloadReservation {
371 fn bytes(&self) -> u64 {
372 self.reserved
373 }
374
375 fn commit(mut self, bytes: u64) {
376 debug_assert!(bytes <= self.reserved);
377 self.counter
378 .fetch_sub(self.reserved.saturating_sub(bytes), Ordering::AcqRel);
379 self.committed = true;
380 }
381}
382
383impl Drop for RemoteDownloadReservation {
384 fn drop(&mut self) {
385 if !self.committed {
386 self.counter.fetch_sub(self.reserved, Ordering::AcqRel);
387 }
388 }
389}
390
391#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
392struct ExecutableIdentityKey {
393 executable: PathBuf,
394 environment: BTreeMap<String, Option<String>>,
395}
396
397impl CacheAgent {
398 pub fn new(cache_dir: impl Into<PathBuf>, version: impl Into<Arc<str>>) -> Self {
400 Self::build(cache_dir.into(), version.into(), None, 0)
401 }
402
403 pub fn new_remote(
405 cache_dir: impl Into<PathBuf>,
406 version: impl Into<Arc<str>>,
407 remote: AgentRemoteCache,
408 ) -> Self {
409 Self::build(
410 cache_dir.into(),
411 version.into(),
412 Some(remote),
413 DEFAULT_MAX_REMOTE_DOWNLOAD_BYTES,
414 )
415 }
416
417 pub fn new_remote_with_download_limit(
419 cache_dir: impl Into<PathBuf>,
420 version: impl Into<Arc<str>>,
421 remote: AgentRemoteCache,
422 max_remote_download_bytes: u64,
423 ) -> Self {
424 Self::build(
425 cache_dir.into(),
426 version.into(),
427 Some(remote),
428 max_remote_download_bytes,
429 )
430 }
431
432 fn build(
433 cache_dir: PathBuf,
434 version: Arc<str>,
435 remote: Option<AgentRemoteCache>,
436 remote_download_limit: u64,
437 ) -> Self {
438 let remote_mode = remote
439 .as_ref()
440 .map_or(RemoteCacheMode::ReadOnly, |remote| remote.mode);
441 let remote_staging_dir = remote.as_ref().map_or_else(
442 || cache_dir.join("remote"),
443 |remote| remote.staging_dir.clone(),
444 );
445 let remote = remote.map(|remote| Arc::new(remote.client));
446 let stats = Arc::new(AtomicAgentStats::default());
447 let remote_transfers = Arc::new(tokio::sync::Semaphore::new(MAX_REMOTE_TRANSFERS));
448 let uploads = remote
451 .clone()
452 .filter(|_| remote_mode.writes())
453 .map(|client| {
454 UploadQueue::new(
455 client,
456 Arc::new(AgentUploadSink {
457 stats: stats.clone(),
458 }),
459 remote_transfers.clone(),
460 )
461 });
462 Self {
463 cas: LocalCas::new(cache_dir.clone()),
464 actions: LocalActionCache::new(cache_dir.clone()),
465 verified_blobs: Arc::new(Mutex::new(BTreeMap::new())),
466 version,
467 write_locks: Arc::new(Mutex::new(BTreeMap::new())),
468 action_locks: Arc::new(Mutex::new(BTreeMap::new())),
469 stats,
470 observer: None,
471 observer_emission: Arc::new(Mutex::new(())),
472 executable_identities: Arc::new(Mutex::new(BTreeMap::new())),
473 manifest_dir: Arc::new(task_manifest_dir(&cache_dir)),
474 task_actions: Arc::new(Mutex::new(BTreeMap::new())),
475 next_task_run: Arc::new(AtomicU64::new(0)),
476 manifest_write_lock: Arc::new(Mutex::new(())),
477 remote,
478 remote_mode,
479 remote_staging_dir: Arc::new(remote_staging_dir),
480 remote_download_limit,
481 remote_download_bytes: Arc::new(AtomicU64::new(0)),
482 pending_remote_actions: Arc::new(Mutex::new(BTreeMap::new())),
483 remote_transfers,
484 prefetch_transfers: Arc::new(tokio::sync::Semaphore::new(MAX_PREFETCH_TRANSFERS)),
485 prefetch_tasks: Arc::new(Mutex::new(Vec::new())),
486 warnings: Arc::new(Mutex::new(BTreeSet::new())),
487 file_digests: Arc::new(Mutex::new(BTreeMap::new())),
488 uploads,
489 }
490 }
491
492 #[must_use]
494 pub fn with_observer(mut self, observer: Arc<dyn AgentEventObserver>) -> Self {
495 self.observer = Some(observer);
496 self
497 }
498
499 fn emit(&self, event: impl FnOnce() -> AgentEvent) {
500 if let Some(observer) = &self.observer {
501 let _emission = self.observer_emission.lock().unwrap();
502 observer.event(event());
503 }
504 }
505
506 fn emit_action(&self, diagnostic: Option<AgentEvent>, action: AgentEvent) {
507 if let Some(observer) = &self.observer {
508 let _emission = self.observer_emission.lock().unwrap();
509 if let Some(diagnostic) = diagnostic {
510 observer.event(diagnostic);
511 }
512 observer.event(action);
513 }
514 }
515
516 fn reserve_remote_download(&self, bytes: u64) -> Result<RemoteDownloadReservation> {
517 let mut current = self.remote_download_bytes.load(Ordering::Acquire);
518 loop {
519 let next = current
520 .checked_add(bytes)
521 .ok_or_else(|| eyre::eyre!("remote cache download budget overflowed"))?;
522 if next > self.remote_download_limit {
523 bail!(
524 "remote cache download budget exceeded: {} bytes requested with {} of {} bytes already used",
525 bytes,
526 current,
527 self.remote_download_limit
528 );
529 }
530 match self.remote_download_bytes.compare_exchange_weak(
531 current,
532 next,
533 Ordering::AcqRel,
534 Ordering::Acquire,
535 ) {
536 Ok(_) => {
537 return Ok(RemoteDownloadReservation {
538 counter: self.remote_download_bytes.clone(),
539 reserved: bytes,
540 committed: false,
541 });
542 }
543 Err(observed) => current = observed,
544 }
545 }
546 }
547
548 fn reserve_remote_download_up_to(&self, requested: u64) -> Result<RemoteDownloadReservation> {
549 let mut current = self.remote_download_bytes.load(Ordering::Acquire);
550 loop {
551 let reserved = requested.min(self.remote_download_limit.saturating_sub(current));
552 let next = current + reserved;
553 match self.remote_download_bytes.compare_exchange_weak(
554 current,
555 next,
556 Ordering::AcqRel,
557 Ordering::Acquire,
558 ) {
559 Ok(_) => {
560 return Ok(RemoteDownloadReservation {
561 counter: self.remote_download_bytes.clone(),
562 reserved,
563 committed: false,
564 });
565 }
566 Err(observed) => current = observed,
567 }
568 }
569 }
570
571 pub async fn begin_task(&self, task: &str) -> Result<String> {
573 self.begin_task_with_remote_errors(task, false, None).await
574 }
575
576 pub async fn begin_session_task(&self, task: &str) -> Result<()> {
582 self.begin_task_with_remote_errors(task, false, Some(task.to_string()))
583 .await?;
584 Ok(())
585 }
586
587 pub async fn prefetch_task(&self, task: &str) -> Result<String> {
589 let run = self.begin_task_with_remote_errors(task, true, None).await?;
590 self.wait_for_prefetches().await;
591 Ok(run)
592 }
593
594 async fn begin_task_with_remote_errors(
595 &self,
596 task: &str,
597 strict: bool,
598 run: Option<String>,
599 ) -> Result<String> {
600 validate_task_identity(task)?;
601 let early_predictions = {
607 let _write_guard = self.manifest_write_lock.lock().unwrap();
608 let _file_guard = self.lock_task_manifest(task)?;
609 self.load_task_manifest(task)?
610 .map(|manifest| manifest.predictions)
611 .unwrap_or_default()
612 };
613 let early_actions: BTreeSet<_> = early_predictions
614 .iter()
615 .map(|prediction| prediction.action.clone())
616 .collect();
617 self.spawn_prefetch_predictions(early_predictions);
618 let (remote_manifest, mut remote_etag) = if self.remote_mode.reads() {
619 match self.get_remote_task_manifest(task).await {
620 Ok(Some((manifest, etag))) => (Some(manifest), Some(etag)),
621 Ok(None) => (None, None),
622 Err(error) => {
623 if strict {
624 return Err(error).wrap_err_with(|| {
625 format!("remote task action manifest lookup failed for {task}")
626 });
627 }
628 self.note_remote_failure();
629 warn!("remote task action manifest lookup failed for {task}: {error}");
630 (None, None)
631 }
632 }
633 } else {
634 (None, None)
635 };
636 let manifest = {
637 let _write_guard = self.manifest_write_lock.lock().unwrap();
638 let _file_guard = self.lock_task_manifest(task)?;
639 let local_manifest = self.load_task_manifest(task)?;
640 let manifest = match (remote_manifest, local_manifest) {
641 (Some(remote), Some(local)) => {
642 let (manifest, merged) = merge_remote_task_manifest(task, remote, local);
643 if !merged {
644 remote_etag = None;
645 }
646 Some(manifest)
647 }
648 (Some(remote), None) => Some(remote),
649 (None, local) => local,
650 };
651 if let Some(manifest) = &manifest {
652 self.persist_task_manifest(manifest)?;
653 }
654 manifest
655 };
656 let state = if let Some(manifest) = manifest {
657 TaskActionState {
658 manifest: task.to_string(),
659 baseline_loaded: true,
660 predictions: manifest
661 .predictions
662 .into_iter()
663 .map(|prediction| (prediction.invocation.clone(), prediction))
664 .collect(),
665 pending_predictions: BTreeMap::new(),
666 remote_etag,
667 }
668 } else {
669 TaskActionState {
670 manifest: task.to_string(),
671 baseline_loaded: true,
672 remote_etag,
673 ..TaskActionState::default()
674 }
675 };
676 let run = run.unwrap_or_else(|| {
677 let sequence = self.next_task_run.fetch_add(1, Ordering::Relaxed);
678 CacheDigest::blake3(format!("{task}\0{}\0{sequence}", std::process::id()).as_bytes())
679 .hash
680 });
681 self.stats.predictions_loaded.fetch_max(
685 state.predictions.len().try_into().unwrap_or(u64::MAX),
686 Ordering::Relaxed,
687 );
688 let predictions = state
692 .predictions
693 .values()
694 .filter(|prediction| !early_actions.contains(&prediction.action))
695 .cloned()
696 .collect();
697 self.task_actions.lock().unwrap().insert(run.clone(), state);
698 self.spawn_prefetch_predictions(predictions);
699 Ok(run)
700 }
701
702 pub async fn cancel_prefetches(&self) {
704 let tasks = std::mem::take(&mut *self.prefetch_tasks.lock().unwrap());
705 for task in &tasks {
706 task.abort();
707 }
708 for task in tasks {
709 if let Err(error) = task.await
710 && !error.is_cancelled()
711 {
712 warn!("remote action prefetch task failed: {error}");
713 }
714 }
715 }
716
717 pub async fn wait_for_uploads(&self) {
724 let Some(uploads) = &self.uploads else {
725 return;
726 };
727 let _timer = AtomicDurationTimer::start(&self.stats.upload_drain_duration_ns);
728 uploads.drain().await;
729 }
730
731 async fn wait_for_prefetches(&self) {
732 let tasks = std::mem::take(&mut *self.prefetch_tasks.lock().unwrap());
733 for task in tasks {
734 if let Err(error) = task.await {
735 warn!("remote action prefetch task failed: {error}");
736 }
737 }
738 }
739
740 pub async fn commit_task(&self, run: &str) -> Result<()> {
742 self.commit_task_actions(run).await.map(|_| ())
743 }
744
745 pub async fn commit_task_actions(&self, run: &str) -> Result<Vec<ActionPrediction>> {
751 validate_task_identity(run)?;
752 let state = self
753 .task_actions
754 .lock()
755 .unwrap()
756 .get(run)
757 .cloned()
758 .ok_or_else(|| eyre::eyre!("task action manifest baseline was not loaded"))?;
759 if !state.baseline_loaded {
760 bail!("task action manifest baseline was not loaded");
761 }
762 let task = state.manifest;
763 validate_task_identity(&task)?;
764 let completed = state
765 .pending_predictions
766 .values()
767 .cloned()
768 .collect::<Vec<_>>();
769 let (manifest, introduced) = {
770 let _write_guard = self.manifest_write_lock.lock().unwrap();
771 let _file_guard = self.lock_task_manifest(&task)?;
772 let mut predictions = self
773 .load_task_manifest(&task)?
774 .map(|manifest| {
775 manifest
776 .predictions
777 .into_iter()
778 .map(|prediction| (prediction.invocation.clone(), prediction))
779 .collect::<BTreeMap<_, _>>()
780 })
781 .unwrap_or_default();
782 let introduced: BTreeSet<CacheDigest> = state
787 .pending_predictions
788 .keys()
789 .filter(|invocation| !predictions.contains_key(*invocation))
790 .cloned()
791 .collect();
792 predictions.extend(state.pending_predictions);
797 let manifest = TaskActionManifest {
798 version: TASK_ACTION_MANIFEST_VERSION,
799 task: task.clone(),
800 predictions: predictions.into_values().collect(),
801 };
802 validate_task_manifest(&manifest, &task)?;
803 self.persist_task_manifest(&manifest)?;
804 (manifest, introduced)
805 };
806 self.task_actions.lock().unwrap().remove(run);
807 if self.remote_mode.writes() {
808 let mut manifest = manifest;
812 if let Some(uploads) = &self.uploads {
813 let actions: Vec<CacheDigest> = manifest
814 .predictions
815 .iter()
816 .map(|prediction| prediction.action.clone())
817 .collect();
818 let unpublished = uploads.wait_for_actions(&actions).await;
819 let withheld = manifest
820 .predictions
821 .iter()
822 .filter(|prediction| {
823 introduced.contains(&prediction.invocation)
824 && unpublished.contains(&prediction.action)
825 })
826 .count();
827 if withheld > 0 {
828 warn!(
831 "{withheld} of {} predicted actions were not published, so the remote task action manifest omits them",
832 manifest.predictions.len()
833 );
834 manifest.predictions.retain(|prediction| {
835 !(introduced.contains(&prediction.invocation)
836 && unpublished.contains(&prediction.action))
837 });
838 }
839 }
840 match self
841 .put_remote_task_manifest(&task, manifest, state.remote_etag)
842 .await
843 {
844 Ok(remote_manifest) => {
845 let _write_guard = self.manifest_write_lock.lock().unwrap();
846 let reconciliation = (|| {
847 let _file_guard = self.lock_task_manifest(&task)?;
848 let manifest = match self.load_task_manifest(&task)? {
849 Some(local) => {
850 merge_remote_task_manifest(&task, remote_manifest, local).0
851 }
852 None => remote_manifest,
853 };
854 self.persist_task_manifest(&manifest)
855 })();
856 if let Err(error) = reconciliation {
857 warn!(
858 "remote task action manifest reconciliation failed for {task}: {error}"
859 );
860 }
861 }
862 Err(error) => {
863 self.note_remote_failure();
864 warn!("remote task action manifest upload failed for {task}: {error}");
865 }
866 }
867 }
868 Ok(completed)
869 }
870
871 fn task_manifest_path(&self, task: &str) -> PathBuf {
872 self.manifest_dir.join(format!("{task}.json"))
873 }
874
875 fn task_manifest_lock_path(&self, task: &str) -> PathBuf {
876 self.manifest_dir.join("locks").join(format!("{task}.lock"))
877 }
878
879 fn lock_task_manifest(&self, task: &str) -> Result<fslock::LockFile> {
880 let path = self.task_manifest_lock_path(task);
881 fs::create_dir_all(path.parent().expect("task manifest lock has a parent"))?;
882 let mut lock = fslock::LockFile::open(&path)?;
883 lock.lock()?;
884 Ok(lock)
885 }
886
887 fn load_task_manifest(&self, task: &str) -> Result<Option<TaskActionManifest>> {
888 match fs::read(self.task_manifest_path(task)) {
889 Ok(contents) => Ok(Some(self.parse_task_manifest(task, &contents, false)?)),
890 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
891 Err(error) => Err(error.into()),
892 }
893 }
894
895 fn parse_task_manifest(
896 &self,
897 task: &str,
898 contents: &[u8],
899 require_canonical: bool,
900 ) -> Result<TaskActionManifest> {
901 let manifest: TaskActionManifest = serde_json::from_slice(contents)?;
902 validate_task_manifest(&manifest, task)?;
903 if require_canonical && canonical_json(&manifest)? != contents {
904 bail!("task action manifest is not canonical JSON");
905 }
906 Ok(manifest)
907 }
908
909 fn task_manifest_selector(task: &str) -> Result<(Vec<u8>, CacheDigest)> {
910 TaskActionManifest::selector(task)
911 }
912
913 fn persist_task_manifest(&self, manifest: &TaskActionManifest) -> Result<()> {
914 let bytes = canonical_json(manifest)?;
915 fs::create_dir_all(self.manifest_dir.as_path())?;
916 let mut temporary = tempfile::NamedTempFile::new_in(self.manifest_dir.as_path())?;
917 std::io::Write::write_all(temporary.as_file_mut(), &bytes)?;
918 temporary.as_file_mut().sync_all()?;
919 temporary
920 .persist(self.task_manifest_path(&manifest.task))
921 .map_err(|error| error.error)?;
922 Ok(())
923 }
924
925 async fn get_remote_task_manifest(
926 &self,
927 task: &str,
928 ) -> Result<Option<(TaskActionManifest, String)>> {
929 let Some(remote) = &self.remote else {
930 return Ok(None);
931 };
932 let (_, selector) = Self::task_manifest_selector(task)?;
933 let _permit = self.remote_transfers.acquire().await?;
934 self.stats
935 .remote_manifest_lookups
936 .fetch_add(1, Ordering::Relaxed);
937 let _timer = AtomicDurationTimer::start(&self.stats.remote_manifest_lookup_duration_ns);
938 let Some(remote_manifest) = remote.get_action_manifest(&selector).await? else {
939 return Ok(None);
940 };
941 let manifest = self.parse_task_manifest(task, &remote_manifest.bytes, true)?;
942 Ok(Some((manifest, remote_manifest.etag)))
943 }
944
945 async fn put_remote_task_manifest(
946 &self,
947 task: &str,
948 mut manifest: TaskActionManifest,
949 mut expected_etag: Option<String>,
950 ) -> Result<TaskActionManifest> {
951 let Some(remote) = &self.remote else {
952 return Ok(manifest);
953 };
954 let (_, selector) = Self::task_manifest_selector(task)?;
955 for _ in 0..4 {
956 let bytes = canonical_json(&manifest)?;
957 let outcome = {
958 let _permit = self.remote_transfers.acquire().await?;
959 remote
960 .put_action_manifest(&selector, &bytes, expected_etag.as_deref())
961 .await?
962 };
963 match outcome {
964 ManifestPutOutcome::Stored => return Ok(manifest),
965 ManifestPutOutcome::PreconditionFailed => {
966 let Some((remote_manifest, etag)) = self.get_remote_task_manifest(task).await?
967 else {
968 expected_etag = None;
969 continue;
970 };
971 manifest = merge_task_manifests(task, Some(remote_manifest), manifest)?;
972 expected_etag = Some(etag);
973 }
974 }
975 }
976 bail!("remote task action manifest changed too frequently")
977 }
978
979 pub fn stats(&self) -> AgentStats {
981 AgentStats {
982 session_duration_ns: 0,
983 lookups: self.stats.lookups.load(Ordering::Relaxed),
984 unconsulted: self.stats.unconsulted.load(Ordering::Relaxed),
985 hits: self.stats.hits.load(Ordering::Relaxed),
986 stores: self.stats.stores.load(Ordering::Relaxed),
987 stored_bytes: self.stats.stored_bytes.load(Ordering::Relaxed),
988 verifications: self.stats.verifications.load(Ordering::Relaxed),
989 divergences: self.stats.divergences.load(Ordering::Relaxed),
990 downloaded_bytes: self.stats.downloaded_bytes.load(Ordering::Relaxed),
991 uploaded_bytes: self.stats.uploaded_bytes.load(Ordering::Relaxed),
992 background_uploads: self.stats.background_uploads.load(Ordering::Relaxed),
993 background_upload_failures: self
994 .stats
995 .background_upload_failures
996 .load(Ordering::Relaxed),
997 remote_blob_pack_uploads: self.stats.remote_blob_pack_uploads.load(Ordering::Relaxed),
998 remote_blob_pack_upload_blobs: self
999 .stats
1000 .remote_blob_pack_upload_blobs
1001 .load(Ordering::Relaxed),
1002 upload_drain_duration_ns: self.stats.upload_drain_duration_ns.load(Ordering::Relaxed),
1003 prefetched_actions: self.stats.prefetched_actions.load(Ordering::Relaxed),
1004 predictions_loaded: self.stats.predictions_loaded.load(Ordering::Relaxed),
1005 bypasses: self.stats.bypasses.lock().unwrap().clone(),
1006 avoided_compiler_duration_ns: self
1007 .stats
1008 .avoided_compiler_duration_ns
1009 .load(Ordering::Relaxed),
1010 compiler: self.stats.compiler.lock().unwrap().clone(),
1011 slow_compilations: self.stats.slow_compilations.lock().unwrap().clone(),
1012 remote_failures: self.stats.remote_failures.load(Ordering::Relaxed),
1013 remote_manifest_lookups: self.stats.remote_manifest_lookups.load(Ordering::Relaxed),
1014 remote_manifest_lookup_duration_ns: self
1015 .stats
1016 .remote_manifest_lookup_duration_ns
1017 .load(Ordering::Relaxed),
1018 remote_action_lookups: self.stats.remote_action_lookups.load(Ordering::Relaxed),
1019 remote_action_lookup_duration_ns: self
1020 .stats
1021 .remote_action_lookup_duration_ns
1022 .load(Ordering::Relaxed),
1023 remote_blob_requests: self.stats.remote_blob_requests.load(Ordering::Relaxed),
1024 remote_blob_pack_requests: self.stats.remote_blob_pack_requests.load(Ordering::Relaxed),
1025 remote_blob_pack_blobs: self.stats.remote_blob_pack_blobs.load(Ordering::Relaxed),
1026 remote_blob_transfer_duration_ns: self
1027 .stats
1028 .remote_blob_transfer_duration_ns
1029 .load(Ordering::Relaxed),
1030 local_cas_write_duration_ns: self
1031 .stats
1032 .local_cas_write_duration_ns
1033 .load(Ordering::Relaxed),
1034 prefetch_runs: self.stats.prefetch_runs.load(Ordering::Relaxed),
1035 prefetch_duration_ns: self.stats.prefetch_duration_ns.load(Ordering::Relaxed),
1036 materialization_duration_ns: self
1037 .stats
1038 .materialization_duration_ns
1039 .load(Ordering::Relaxed),
1040 restored_output_files: self.stats.restored_output_files.load(Ordering::Relaxed),
1041 restored_output_bytes: self.stats.restored_output_bytes.load(Ordering::Relaxed),
1042 reflinked_output_files: self.stats.reflinked_output_files.load(Ordering::Relaxed),
1043 reflinked_output_bytes: self.stats.reflinked_output_bytes.load(Ordering::Relaxed),
1044 copied_output_files: self.stats.copied_output_files.load(Ordering::Relaxed),
1045 copied_output_bytes: self.stats.copied_output_bytes.load(Ordering::Relaxed),
1046 reused_output_files: self.stats.reused_output_files.load(Ordering::Relaxed),
1047 reused_output_bytes: self.stats.reused_output_bytes.load(Ordering::Relaxed),
1048 }
1049 }
1050
1051 pub async fn handle_requests(
1058 &self,
1059 requests: impl IntoIterator<Item = AgentRequest>,
1060 ) -> Vec<AgentResponse> {
1061 let mut connection = ConnectionUploads::default();
1062 let mut responses = Vec::new();
1063 for request in requests {
1064 responses.push(self.respond_on(request, &mut connection).await);
1065 }
1066 responses
1067 }
1068
1069 fn write_lock(&self, digest: &CacheDigest) -> Arc<tokio::sync::Mutex<()>> {
1070 Self::digest_lock(&self.write_locks, digest)
1071 }
1072
1073 fn action_lock(&self, digest: &CacheDigest) -> Arc<tokio::sync::Mutex<()>> {
1074 Self::digest_lock(&self.action_locks, digest)
1075 }
1076
1077 fn digest_lock(
1078 locks: &Mutex<BTreeMap<CacheDigest, Weak<tokio::sync::Mutex<()>>>>,
1079 digest: &CacheDigest,
1080 ) -> Arc<tokio::sync::Mutex<()>> {
1081 let mut locks = locks.lock().unwrap();
1082 locks.retain(|_, lock| lock.strong_count() > 0);
1083 if let Some(lock) = locks.get(digest).and_then(Weak::upgrade) {
1084 return lock;
1085 }
1086 let lock = Arc::new(tokio::sync::Mutex::new(()));
1087 locks.insert(digest.clone(), Arc::downgrade(&lock));
1088 lock
1089 }
1090
1091 #[cfg(test)]
1096 async fn respond(&self, request: AgentRequest) -> AgentResponse {
1097 self.respond_on(request, &mut ConnectionUploads::default())
1098 .await
1099 }
1100
1101 async fn respond_on(
1102 &self,
1103 request: AgentRequest,
1104 connection: &mut ConnectionUploads,
1105 ) -> AgentResponse {
1106 let result = match request {
1107 AgentRequest::BeginTask { task } => self
1108 .begin_task(&task)
1109 .await
1110 .map(|run| AgentResponse::TaskBegun { run }),
1111 AgentRequest::CommitTask { run } => self
1112 .commit_task(&run)
1113 .await
1114 .map(|()| AgentResponse::TaskCommitted),
1115 AgentRequest::FindBlob { digest } => self.find_blob(&digest).await,
1116 AgentRequest::FindBlobs { digests } => self.find_blobs(digests).await,
1117 AgentRequest::StoreBlob { digest, source } => {
1118 self.store_blob(&digest, &source, connection).await
1119 }
1120 AgentRequest::FindActionResult { action } => {
1121 self.stats.lookups.fetch_add(1, Ordering::Relaxed);
1122 self.find_action_result(&action).await
1123 }
1124 AgentRequest::RecordActionHit {
1125 action,
1126 restore,
1127 crate_name,
1128 } => {
1129 let diagnostic = connection
1130 .take_action_diagnostic("hit", crate_name.as_deref())
1131 .map(|diagnostic| AgentEvent::ActionDiagnostic {
1132 outcome: "hit".into(),
1133 crate_name: crate_name.clone(),
1134 diagnostic,
1135 });
1136 self.record_action_hit(&action, restore, crate_name, diagnostic)
1137 }
1138 AgentRequest::RecordBypass { kind } => {
1139 *self
1140 .stats
1141 .bypasses
1142 .lock()
1143 .unwrap()
1144 .entry(kind.clone())
1145 .or_insert(0) += 1;
1146 self.emit(|| AgentEvent::Bypass { kind });
1147 Ok(AgentResponse::BypassRecorded)
1148 }
1149 AgentRequest::RecordUnconsulted => {
1150 self.stats.unconsulted.fetch_add(1, Ordering::Relaxed);
1151 self.emit(|| AgentEvent::Unconsulted);
1152 Ok(AgentResponse::UnconsultedRecorded)
1153 }
1154 AgentRequest::RecordWarning { message } => match parse_action_diagnostic(&message) {
1155 Some(Ok((outcome, crate_name, diagnostic))) => {
1156 connection.record_action_diagnostic(outcome, crate_name, diagnostic);
1157 Ok(AgentResponse::WarningRecorded)
1158 }
1159 Some(Err(error)) => Err(error),
1160 None => self.record_warning(message),
1161 },
1162 AgentRequest::FindFileDigests { scope, files } => self.find_file_digests(scope, files),
1163 AgentRequest::JoinActionPromise {
1164 adapter,
1165 invocation,
1166 } => self.join_action_promise(&adapter, &invocation).await,
1167 AgentRequest::CompleteActionPromise { claim, prediction } => {
1168 self.complete_action_promise(&claim, &prediction).await
1169 }
1170 AgentRequest::RecordFileDigests { scope, entries } => {
1171 self.record_file_digests(scope, entries)
1172 }
1173 AgentRequest::RecordCompilerInvocation {
1174 outcome,
1175 crate_name,
1176 duration_ns,
1177 } => {
1178 let diagnostic = connection
1179 .take_action_diagnostic(&outcome, crate_name.as_deref())
1180 .map(|diagnostic| AgentEvent::ActionDiagnostic {
1181 outcome: outcome.clone(),
1182 crate_name: crate_name.clone(),
1183 diagnostic,
1184 });
1185 self.record_compiler_invocation(
1186 &outcome,
1187 crate_name.as_deref(),
1188 duration_ns,
1189 diagnostic,
1190 )
1191 }
1192 AgentRequest::RecordActionVerification { matched, restore } => {
1193 self.record_materialization(restore);
1194 self.stats.verifications.fetch_add(1, Ordering::Relaxed);
1195 if !matched {
1196 self.stats.divergences.fetch_add(1, Ordering::Relaxed);
1197 }
1198 self.emit(|| AgentEvent::Verification { matched, restore });
1199 Ok(AgentResponse::ActionVerificationRecorded)
1200 }
1201 AgentRequest::StoreActionResult { result } => {
1202 self.store_action_result(&result, connection).await
1203 }
1204 AgentRequest::FindActionPrediction { task, invocation } => {
1205 self.find_action_prediction(&task, &invocation)
1206 }
1207 AgentRequest::RecordActionPrediction { task, prediction } => {
1208 self.record_action_prediction(&task, prediction)
1209 }
1210 AgentRequest::FindExecutableIdentity {
1211 executable,
1212 environment,
1213 } => self.find_executable_identity(executable, environment),
1214 AgentRequest::StoreExecutableIdentity {
1215 executable,
1216 environment,
1217 stdout,
1218 } => self.store_executable_identity(executable, environment, stdout),
1219 AgentRequest::Hello { .. } => {
1220 Err(eyre::eyre!("hello is only valid as the first request"))
1221 }
1222 };
1223 result.unwrap_or_else(|error| AgentResponse::Error {
1224 message: error.to_string(),
1225 })
1226 }
1227
1228 async fn find_blob(&self, digest: &CacheDigest) -> Result<AgentResponse> {
1229 if let Some(path) = self.find_verified_blob(digest)? {
1230 return Ok(AgentResponse::Blob { path: Some(path) });
1231 }
1232 if !self.remote_mode.reads() {
1233 return Ok(AgentResponse::Blob { path: None });
1234 }
1235 let Some(remote) = &self.remote else {
1236 return Ok(AgentResponse::Blob { path: None });
1237 };
1238 match self.fetch_remote_blob(remote, digest).await {
1239 Ok(path) => Ok(AgentResponse::Blob { path: Some(path) }),
1240 Err(error) => {
1241 warn!(
1242 "remote cache blob lookup failed for {}: {error}",
1243 digest.hash
1244 );
1245 Ok(AgentResponse::Blob { path: None })
1246 }
1247 }
1248 }
1249
1250 async fn find_blobs(&self, digests: Vec<CacheDigest>) -> Result<AgentResponse> {
1251 let mut paths = BTreeMap::new();
1252 let mut missing = Vec::new();
1253 for digest in &digests {
1254 match self.find_verified_blob(digest)? {
1255 Some(path) => {
1256 paths.insert(digest.clone(), path);
1257 }
1258 None => {
1259 missing.push(digest.clone());
1260 }
1261 }
1262 }
1263
1264 if !missing.is_empty()
1265 && self.remote_mode.reads()
1266 && let Some(remote) = &self.remote
1267 {
1268 paths.extend(self.fetch_remote_blobs(remote, missing, None).await);
1269 }
1270
1271 Ok(AgentResponse::Blobs {
1272 paths: digests
1273 .into_iter()
1274 .map(|digest| paths.get(&digest).cloned())
1275 .collect(),
1276 })
1277 }
1278
1279 async fn store_blob(
1280 &self,
1281 digest: &CacheDigest,
1282 source: &Path,
1283 connection: &mut ConnectionUploads,
1284 ) -> Result<AgentResponse> {
1285 let path = {
1286 let lock = self.write_lock(digest);
1287 let _guard = lock.lock().await;
1288 if let Some(path) = self.find_verified_blob(digest)? {
1289 path
1290 } else {
1291 let path = self.cas.store_file(digest, source)?;
1292 self.remember_verified_blob(digest, &path);
1293 self.stats.stores.fetch_add(1, Ordering::Relaxed);
1294 self.stats
1295 .stored_bytes
1296 .fetch_add(digest.size, Ordering::Relaxed);
1297 path
1298 }
1299 };
1300 if let Some(uploads) = &self.uploads {
1303 uploads.queue_blob(digest, path.clone(), connection);
1304 }
1305 Ok(AgentResponse::Stored { path })
1306 }
1307
1308 fn find_verified_blob(&self, digest: &CacheDigest) -> Result<Option<PathBuf>> {
1309 let remembered = self.verified_blobs.lock().unwrap().get(digest).cloned();
1310 if let Some(remembered) = remembered {
1311 if remembered.is_unchanged() {
1322 return Ok(Some(remembered.path));
1323 }
1324 self.verified_blobs.lock().unwrap().remove(digest);
1325 }
1326 let path = self.cas.find(digest)?;
1327 if let Some(path) = &path {
1328 self.remember_verified_blob(digest, path);
1329 }
1330 Ok(path)
1331 }
1332
1333 fn remember_verified_blob(&self, digest: &CacheDigest, path: &Path) {
1334 let Some(verified) = VerifiedBlob::describe(path) else {
1335 return;
1339 };
1340 self.verified_blobs
1341 .lock()
1342 .unwrap()
1343 .insert(digest.clone(), verified);
1344 }
1345
1346 async fn find_action_result(&self, action: &CacheDigest) -> Result<AgentResponse> {
1347 if let Some(result) = self.actions.find(action)? {
1348 return Ok(AgentResponse::ActionResult {
1349 result: Some(result),
1350 });
1351 }
1352 if !self.remote_mode.reads() {
1353 return Ok(AgentResponse::ActionResult { result: None });
1354 }
1355 let Some(remote) = &self.remote else {
1356 return Ok(AgentResponse::ActionResult { result: None });
1357 };
1358 let lock = self.action_lock(action);
1359 let _guard = lock.lock().await;
1360 if let Some(result) = self.actions.find(action)? {
1361 return Ok(AgentResponse::ActionResult {
1362 result: Some(result),
1363 });
1364 }
1365 if let Some(result) = self
1366 .pending_remote_actions
1367 .lock()
1368 .unwrap()
1369 .get(action)
1370 .cloned()
1371 {
1372 return Ok(AgentResponse::ActionResult {
1373 result: Some(result),
1374 });
1375 }
1376 let _permit = self.remote_transfers.acquire().await?;
1377 match self.get_remote_action_result(remote, action).await {
1378 Ok(Some(result)) => {
1379 self.pending_remote_actions
1380 .lock()
1381 .unwrap()
1382 .insert(action.clone(), result.clone());
1383 Ok(AgentResponse::ActionResult {
1384 result: Some(result),
1385 })
1386 }
1387 Ok(None) => Ok(AgentResponse::ActionResult { result: None }),
1388 Err(error) => {
1389 self.note_remote_failure();
1390 warn!(
1391 "remote cache action lookup failed for {}: {error}",
1392 action.hash
1393 );
1394 Ok(AgentResponse::ActionResult { result: None })
1395 }
1396 }
1397 }
1398
1399 async fn store_action_result(
1400 &self,
1401 result: &RemoteActionResult,
1402 connection: &ConnectionUploads,
1403 ) -> Result<AgentResponse> {
1404 let path = self.actions.store(result)?;
1405 if let Some(uploads) = &self.uploads {
1406 uploads.queue_action_result(result, connection);
1407 }
1408 Ok(AgentResponse::ActionStored { path })
1409 }
1410
1411 async fn join_action_promise(
1412 &self,
1413 adapter: &str,
1414 invocation: &CacheDigest,
1415 ) -> Result<AgentResponse> {
1416 if !self.remote_mode.reads() || !self.remote_mode.writes() {
1417 return Ok(AgentResponse::ActionPromise {
1418 claim: None,
1419 prediction: None,
1420 });
1421 }
1422 let Some(remote) = &self.remote else {
1423 return Ok(AgentResponse::ActionPromise {
1424 claim: None,
1425 prediction: None,
1426 });
1427 };
1428 let deadline = Instant::now() + ACTION_PROMISE_WAIT;
1429 loop {
1430 let _permit = self.remote_transfers.acquire().await?;
1431 let state = remote.join_action_promise(invocation, adapter).await;
1432 drop(_permit);
1433 match state {
1434 Ok(Some(ActionPromiseState::Claimed { claim })) => {
1435 return Ok(AgentResponse::ActionPromise {
1436 claim: Some(claim),
1437 prediction: None,
1438 });
1439 }
1440 Ok(Some(ActionPromiseState::Complete { prediction })) => {
1441 return Ok(AgentResponse::ActionPromise {
1442 claim: None,
1443 prediction: Some(prediction),
1444 });
1445 }
1446 Ok(Some(ActionPromiseState::Pending { retry_after_ms }))
1447 if Instant::now() < deadline =>
1448 {
1449 tokio::time::sleep(Duration::from_millis(retry_after_ms.clamp(10, 5_000)))
1450 .await;
1451 }
1452 Ok(Some(ActionPromiseState::Pending { .. }) | None) => {
1453 return Ok(AgentResponse::ActionPromise {
1454 claim: None,
1455 prediction: None,
1456 });
1457 }
1458 Ok(Some(_)) => {
1459 return Ok(AgentResponse::ActionPromise {
1460 claim: None,
1461 prediction: None,
1462 });
1463 }
1464 Err(error) => {
1465 self.note_remote_failure();
1466 warn!(
1467 "remote cache action promise failed for {}: {error}",
1468 invocation.hash
1469 );
1470 return Ok(AgentResponse::ActionPromise {
1471 claim: None,
1472 prediction: None,
1473 });
1474 }
1475 }
1476 }
1477 }
1478
1479 async fn complete_action_promise(
1480 &self,
1481 claim: &str,
1482 prediction: &ActionPrediction,
1483 ) -> Result<AgentResponse> {
1484 prediction.validate()?;
1485 let Some(remote) = &self.remote else {
1486 return Ok(AgentResponse::ActionPromiseCompleted);
1487 };
1488 let Some(uploads) = &self.uploads else {
1489 return Ok(AgentResponse::ActionPromiseCompleted);
1490 };
1491 if uploads
1492 .wait_for_actions(std::slice::from_ref(&prediction.action))
1493 .await
1494 .contains(&prediction.action)
1495 {
1496 return Ok(AgentResponse::ActionPromiseCompleted);
1499 }
1500 let completion = ActionPromiseCompletion {
1501 claim: claim.to_string(),
1502 prediction: prediction.clone(),
1503 };
1504 let _permit = self.remote_transfers.acquire().await?;
1505 if let Err(error) = remote
1506 .complete_action_promise(&prediction.invocation, &completion)
1507 .await
1508 {
1509 self.note_remote_failure();
1510 warn!(
1511 "remote cache action promise completion failed for {}: {error}",
1512 prediction.invocation.hash
1513 );
1514 }
1515 Ok(AgentResponse::ActionPromiseCompleted)
1516 }
1517
1518 fn note_remote_failure(&self) {
1520 self.stats.remote_failures.fetch_add(1, Ordering::Relaxed);
1521 }
1522
1523 async fn get_remote_action_result(
1524 &self,
1525 remote: &RemoteCacheClient,
1526 action: &CacheDigest,
1527 ) -> Result<Option<RemoteActionResult>> {
1528 self.stats
1529 .remote_action_lookups
1530 .fetch_add(1, Ordering::Relaxed);
1531 let _timer = AtomicDurationTimer::start(&self.stats.remote_action_lookup_duration_ns);
1532 remote.get_action_result(action).await
1533 }
1534
1535 fn record_action_hit(
1536 &self,
1537 action: &CacheDigest,
1538 restore: RestoreStats,
1539 crate_name: Option<String>,
1540 diagnostic: Option<AgentEvent>,
1541 ) -> Result<AgentResponse> {
1542 validate_crate_name(crate_name.as_deref())?;
1543 if self.actions.find(action)?.is_none() {
1544 let pending = self.pending_remote_actions.lock().unwrap().remove(action);
1545 if let Some(result) = pending {
1546 self.actions.store(&result)?;
1547 } else {
1548 bail!("cannot record a hit for a missing action result");
1549 }
1550 }
1551 self.record_restore(restore);
1552 self.stats.hits.fetch_add(1, Ordering::Relaxed);
1553 self.emit_action(
1554 diagnostic,
1555 AgentEvent::ActionHit {
1556 crate_name,
1557 restore,
1558 },
1559 );
1560 Ok(AgentResponse::ActionHitRecorded)
1561 }
1562
1563 fn record_restore(&self, restore: RestoreStats) {
1564 self.record_materialization(restore);
1565 atomic_saturating_add(
1566 &self.stats.avoided_compiler_duration_ns,
1567 restore.avoided_compiler_duration_ns,
1568 );
1569 atomic_saturating_add(&self.stats.restored_output_files, restore.output_files);
1570 atomic_saturating_add(&self.stats.restored_output_bytes, restore.output_bytes);
1571 atomic_saturating_add(
1572 &self.stats.reflinked_output_files,
1573 restore.reflinked_output_files,
1574 );
1575 atomic_saturating_add(
1576 &self.stats.reflinked_output_bytes,
1577 restore.reflinked_output_bytes,
1578 );
1579 atomic_saturating_add(&self.stats.copied_output_files, restore.copied_output_files);
1580 atomic_saturating_add(&self.stats.copied_output_bytes, restore.copied_output_bytes);
1581 atomic_saturating_add(&self.stats.reused_output_files, restore.reused_output_files);
1582 atomic_saturating_add(&self.stats.reused_output_bytes, restore.reused_output_bytes);
1583 }
1584
1585 fn record_compiler_invocation(
1586 &self,
1587 outcome: &str,
1588 crate_name: Option<&str>,
1589 duration_ns: u64,
1590 diagnostic: Option<AgentEvent>,
1591 ) -> Result<AgentResponse> {
1592 if !matches!(
1593 outcome,
1594 "miss" | "unconsulted" | "bypass" | "verification" | "incremental"
1595 ) {
1596 bail!("invalid compiler invocation outcome");
1597 }
1598 validate_crate_name(crate_name)?;
1599 let mut compiler = self.stats.compiler.lock().unwrap();
1600 let stats = compiler.entry(outcome.to_string()).or_default();
1601 stats.invocations = stats.invocations.saturating_add(1);
1602 stats.duration_ns = stats.duration_ns.saturating_add(duration_ns);
1603 drop(compiler);
1604 if outcome != "verification"
1605 && let Some(crate_name) = crate_name.filter(|name| !name.is_empty())
1606 {
1607 let mut slow = self.stats.slow_compilations.lock().unwrap();
1608 let duration = slow.entry(crate_name.to_string()).or_default();
1609 *duration = duration.saturating_add(duration_ns);
1610 }
1611 self.emit_action(
1612 diagnostic,
1613 AgentEvent::CompilerInvocation {
1614 outcome: outcome.to_string(),
1615 crate_name: crate_name.map(str::to_string),
1616 duration_ns,
1617 },
1618 );
1619 Ok(AgentResponse::CompilerInvocationRecorded)
1620 }
1621
1622 fn record_materialization(&self, restore: RestoreStats) {
1623 atomic_saturating_add(&self.stats.materialization_duration_ns, restore.duration_ns);
1624 }
1625
1626 fn record_warning(&self, message: String) -> Result<AgentResponse> {
1635 if message.is_empty()
1636 || message.len() > MAX_WARNING_BYTES
1637 || message.contains(['\n', '\r', '\0'])
1638 {
1639 bail!("invalid shim warning");
1640 }
1641 let mut warnings = self.warnings.lock().unwrap();
1642 if !warnings.contains(&message) && warnings.len() < MAX_WARNINGS {
1643 eprintln!("mbx[warning]: {message}");
1644 self.emit(|| AgentEvent::Warning {
1645 message: message.clone(),
1646 });
1647 warnings.insert(message);
1648 }
1649 Ok(AgentResponse::WarningRecorded)
1650 }
1651
1652 fn find_file_digests(
1659 &self,
1660 scope: FileDigestScope,
1661 files: Vec<FileIdentity>,
1662 ) -> Result<AgentResponse> {
1663 if files.len() > MAX_FILE_DIGEST_BATCH {
1664 bail!("too many file-digest lookups in one request");
1665 }
1666 let ledger = self.file_digests.lock().unwrap();
1667 let digests = files
1668 .into_iter()
1669 .map(|file| {
1670 let recorded = ledger.get(&(scope, file.path.clone()))?;
1671 (recorded.file == file).then(|| recorded.digest.clone())
1672 })
1673 .collect();
1674 Ok(AgentResponse::FileDigests { digests })
1675 }
1676
1677 fn record_file_digests(
1683 &self,
1684 scope: FileDigestScope,
1685 entries: Vec<RecordedFileDigest>,
1686 ) -> Result<AgentResponse> {
1687 if entries.len() > MAX_FILE_DIGEST_BATCH {
1688 bail!("too many file-digest records in one request");
1689 }
1690 for entry in &entries {
1691 if !entry.file.path.is_absolute() {
1692 bail!("file-digest records need absolute paths");
1693 }
1694 entry.digest.validate()?;
1695 if entry.file.len != entry.digest.size {
1696 bail!("file-digest record length does not match its digest");
1697 }
1698 }
1699 let mut ledger = self.file_digests.lock().unwrap();
1700 for entry in entries {
1701 if ledger.len() >= MAX_FILE_DIGEST_ENTRIES
1702 && !ledger.contains_key(&(scope, entry.file.path.clone()))
1703 {
1704 break;
1705 }
1706 ledger.insert((scope, entry.file.path.clone()), entry);
1707 }
1708 Ok(AgentResponse::FileDigestsRecorded)
1709 }
1710
1711 fn find_action_prediction(
1712 &self,
1713 task: &str,
1714 invocation: &CacheDigest,
1715 ) -> Result<AgentResponse> {
1716 validate_task_identity(task)?;
1717 invocation.validate()?;
1718 let prediction = self
1719 .task_actions
1720 .lock()
1721 .unwrap()
1722 .get(task)
1723 .and_then(|state| state.predictions.get(invocation))
1724 .cloned();
1725 Ok(AgentResponse::ActionPrediction { prediction })
1726 }
1727
1728 fn record_action_prediction(
1729 &self,
1730 task: &str,
1731 prediction: ActionPrediction,
1732 ) -> Result<AgentResponse> {
1733 validate_task_identity(task)?;
1734 prediction.validate()?;
1735 let mut tasks = self.task_actions.lock().unwrap();
1736 let state = tasks.entry(task.to_string()).or_default();
1737 if !state.predictions.contains_key(&prediction.invocation)
1738 && state.predictions.len() >= MAX_TASK_ACTION_PREDICTIONS
1739 {
1740 bail!("task action manifest contains too many predictions");
1741 }
1742 state
1743 .predictions
1744 .insert(prediction.invocation.clone(), prediction.clone());
1745 state
1746 .pending_predictions
1747 .insert(prediction.invocation.clone(), prediction);
1748 Ok(AgentResponse::ActionPredictionRecorded)
1749 }
1750
1751 fn executable_identity_key(
1752 &self,
1753 executable: PathBuf,
1754 environment: BTreeMap<String, Option<String>>,
1755 ) -> Result<ExecutableIdentityKey> {
1756 if !environment.keys().all(|name| {
1761 matches!(
1762 name.as_str(),
1763 "MBX_FUSE_LD"
1764 | "RUSTUP_HOME"
1765 | "RUSTUP_TOOLCHAIN"
1766 | "SDKROOT"
1767 | "MACOSX_DEPLOYMENT_TARGET"
1768 | "LIB"
1769 | "UCRTVersion"
1770 | "UniversalCRTSdkDir"
1771 | "VCToolsInstallDir"
1772 | "VCToolsVersion"
1773 | "WindowsSdkDir"
1774 | "WindowsSDKVersion"
1775 )
1776 }) {
1777 bail!("executable identity contains an unsupported environment variable");
1778 }
1779 Ok(ExecutableIdentityKey {
1780 executable,
1781 environment,
1782 })
1783 }
1784
1785 fn find_executable_identity(
1786 &self,
1787 executable: PathBuf,
1788 environment: BTreeMap<String, Option<String>>,
1789 ) -> Result<AgentResponse> {
1790 let key = self.executable_identity_key(executable, environment)?;
1791 let stdout = self
1792 .executable_identities
1793 .lock()
1794 .unwrap()
1795 .get(&key)
1796 .cloned();
1797 Ok(AgentResponse::ExecutableIdentity { stdout })
1798 }
1799
1800 fn store_executable_identity(
1801 &self,
1802 executable: PathBuf,
1803 environment: BTreeMap<String, Option<String>>,
1804 stdout: Vec<u8>,
1805 ) -> Result<AgentResponse> {
1806 if stdout.len() > MAX_EXECUTABLE_IDENTITY_SIZE {
1807 bail!("executable identity exceeds {MAX_EXECUTABLE_IDENTITY_SIZE} bytes");
1808 }
1809 let key = self.executable_identity_key(executable, environment)?;
1810 let mut identities = self.executable_identities.lock().unwrap();
1811 let is_new = !identities.contains_key(&key);
1812 let previous_size = identities.get(&key).map_or(0, Vec::len);
1813 if is_new && identities.len() >= MAX_EXECUTABLE_IDENTITIES {
1814 bail!("executable identity cache contains too many entries");
1815 }
1816 let retained_bytes = identities.values().map(Vec::len).sum::<usize>();
1817 if retained_bytes - previous_size + stdout.len() > MAX_EXECUTABLE_IDENTITY_BYTES {
1818 bail!("executable identity cache contains too many bytes");
1819 }
1820 identities.insert(key, stdout.clone());
1821 Ok(AgentResponse::ExecutableIdentity {
1822 stdout: Some(stdout),
1823 })
1824 }
1825
1826 pub async fn handle_connection<S>(&self, stream: S) -> Result<()>
1828 where
1829 S: AsyncRead + AsyncWrite + Unpin,
1830 {
1831 let (reader, mut writer) = tokio::io::split(stream);
1832 let mut reader = BufReader::new(reader);
1833 let hello = read_request(&mut reader)
1834 .await?
1835 .ok_or_else(|| eyre::eyre!("connection closed before the agent handshake"))?;
1836 let request: AgentRequest = serde_json::from_str(&hello)?;
1837 match request {
1838 AgentRequest::Hello {
1839 protocol,
1840 client_version,
1841 } if protocol == AGENT_PROTOCOL_VERSION && client_version == self.version.as_ref() => {}
1842 AgentRequest::Hello { protocol, .. } if protocol != AGENT_PROTOCOL_VERSION => {
1843 send_response(
1844 &mut writer,
1845 &AgentResponse::Error {
1846 message: format!(
1847 "unsupported agent protocol {protocol}; expected {AGENT_PROTOCOL_VERSION}"
1848 ),
1849 },
1850 )
1851 .await?;
1852 return Ok(());
1853 }
1854 AgentRequest::Hello { client_version, .. } => {
1855 send_response(
1856 &mut writer,
1857 &AgentResponse::Error {
1858 message: format!(
1859 "cache client {client_version} does not match agent {}",
1860 self.version
1861 ),
1862 },
1863 )
1864 .await?;
1865 return Ok(());
1866 }
1867 _ => bail!("the first agent request must be hello"),
1868 }
1869 send_response(
1870 &mut writer,
1871 &AgentResponse::Hello {
1872 protocol: AGENT_PROTOCOL_VERSION,
1873 agent_version: self.version.to_string(),
1874 },
1875 )
1876 .await?;
1877
1878 let mut connection = ConnectionUploads::default();
1882 while let Some(line) = read_request(&mut reader).await? {
1883 let response = match serde_json::from_str(&line) {
1884 Ok(request) => self.respond_on(request, &mut connection).await,
1885 Err(error) => AgentResponse::Error {
1886 message: format!("invalid agent request: {error}"),
1887 },
1888 };
1889 send_response(&mut writer, &response).await?;
1890 }
1891 Ok(())
1892 }
1893}
1894
1895async fn read_request<R>(reader: &mut R) -> Result<Option<String>>
1901where
1902 R: AsyncBufRead + Unpin,
1903{
1904 let mut line = Vec::new();
1905 loop {
1906 let available = reader.fill_buf().await?;
1907 if available.is_empty() {
1908 break;
1909 }
1910 let (consumed, complete) = match available.iter().position(|byte| *byte == b'\n') {
1911 Some(index) => (index, true),
1912 None => (available.len(), false),
1913 };
1914 if line.len() + consumed > MAX_REQUEST_BYTES {
1915 bail!("agent request exceeded {MAX_REQUEST_BYTES} bytes");
1916 }
1917 line.extend_from_slice(&available[..consumed]);
1918 reader.consume(consumed + usize::from(complete));
1920 if complete {
1921 return Ok(Some(String::from_utf8(line)?));
1922 }
1923 }
1924 if line.is_empty() {
1925 Ok(None)
1926 } else {
1927 Ok(Some(String::from_utf8(line)?))
1928 }
1929}
1930
1931async fn send_response(
1932 writer: &mut (impl AsyncWrite + Unpin),
1933 response: &AgentResponse,
1934) -> Result<()> {
1935 let mut encoded = serde_json::to_vec(response)?;
1936 encoded.push(b'\n');
1937 writer.write_all(&encoded).await?;
1938 writer.flush().await?;
1939 Ok(())
1940}
1941
1942#[cfg(test)]
1943#[path = "agent_tests.rs"]
1944mod tests;