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