1use crate::uploads::{ConnectionUploads, UploadQueue, UploadSink};
2use crate::{
3 ActionPrediction, ActionPromiseCompletion, ActionPromiseState, CacheDigest, CacheDirectory,
4 LocalActionCache, LocalCas, MAX_STAGED_BLOB_PACK_BYTES, ManifestPutOutcome, RemoteActionResult,
5 RemoteCacheClient, RemoteCacheMode, RustcMetadata, TaskActionManifest, blob_pack_chunk,
6 canonical_json,
7};
8use eyre::{Context, Result, bail};
9use futures_util::{FutureExt, StreamExt, future::BoxFuture, stream};
10use log::{info, 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_CONCURRENT_BLOB_PACKS: usize = 8;
77const MAX_PREFETCH_ACTIONS: usize = 1024;
88const MAX_PREFETCH_ACTION_WAVE: usize = 32;
93const MAX_PREFETCH_BATCH_LOOKUPS: usize = 1;
99const PREFETCH_ACTION_BATCH_DELAY: Duration = Duration::from_millis(5);
100const MAX_PREFETCH_DIRECTORY_OBJECTS: usize = 100_000;
101const MAX_PREFETCH_OBJECTS_PER_WAVE: usize = 100_000;
102const DEFAULT_MAX_REMOTE_DOWNLOAD_BYTES: u64 = 5 * 1024 * 1024 * 1024;
103
104pub type TaskFallbacks = Arc<dyn Fn() -> Vec<String> + Send + Sync>;
107
108const NEWEST_MANIFEST_CANDIDATES: usize = 8;
120
121pub struct AgentRemoteCache {
123 pub client: RemoteCacheClient,
125 pub mode: RemoteCacheMode,
127 pub staging_dir: PathBuf,
129}
130
131#[derive(Default)]
132struct AtomicAgentStats {
133 lookups: AtomicU64,
134 unconsulted: AtomicU64,
135 hits: AtomicU64,
136 stores: AtomicU64,
137 stored_bytes: AtomicU64,
138 verifications: AtomicU64,
139 divergences: AtomicU64,
140 downloaded_bytes: AtomicU64,
141 uploaded_bytes: AtomicU64,
142 background_uploads: AtomicU64,
143 background_upload_failures: AtomicU64,
144 remote_blob_pack_uploads: AtomicU64,
145 remote_blob_pack_upload_blobs: AtomicU64,
146 upload_drain_duration_ns: AtomicU64,
147 prefetched_actions: AtomicU64,
148 predictions_loaded: AtomicU64,
149 remote_failures: AtomicU64,
150 remote_manifest_lookups: AtomicU64,
151 remote_manifest_lookup_duration_ns: AtomicU64,
152 remote_action_lookups: AtomicU64,
153 remote_action_lookup_duration_ns: AtomicU64,
154 remote_blob_requests: AtomicU64,
155 remote_blob_pack_requests: AtomicU64,
156 remote_blob_pack_blobs: AtomicU64,
157 remote_blob_transfer_duration_ns: AtomicU64,
158 local_cas_write_duration_ns: AtomicU64,
159 prefetch_runs: AtomicU64,
160 prefetch_duration_ns: AtomicU64,
161 materialization_duration_ns: AtomicU64,
162 bypasses: Mutex<BTreeMap<String, u64>>,
163 avoided_compiler_duration_ns: AtomicU64,
164 compiler: Mutex<BTreeMap<String, CompilerStats>>,
165 slow_compilations: Mutex<BTreeMap<String, u64>>,
166 restored_output_files: AtomicU64,
167 restored_output_bytes: AtomicU64,
168 reflinked_output_files: AtomicU64,
169 reflinked_output_bytes: AtomicU64,
170 copied_output_files: AtomicU64,
171 copied_output_bytes: AtomicU64,
172 reused_output_files: AtomicU64,
173 reused_output_bytes: AtomicU64,
174}
175
176struct AtomicDurationTimer<'a> {
177 started: Instant,
178 target: &'a AtomicU64,
179}
180
181impl<'a> AtomicDurationTimer<'a> {
182 fn start(target: &'a AtomicU64) -> Self {
183 Self {
184 started: Instant::now(),
185 target,
186 }
187 }
188}
189
190impl Drop for AtomicDurationTimer<'_> {
191 fn drop(&mut self) {
192 atomic_saturating_add(self.target, duration_ns(self.started));
193 }
194}
195
196fn duration_ns(started: Instant) -> u64 {
197 started.elapsed().as_nanos().try_into().unwrap_or(u64::MAX)
198}
199
200fn validate_crate_name(crate_name: Option<&str>) -> Result<()> {
201 if let Some(crate_name) = crate_name
202 && (crate_name.len() > 256 || crate_name.contains(['\0', '\n', '\r']))
203 {
204 bail!("invalid compiler crate name");
205 }
206 Ok(())
207}
208
209#[derive(serde::Deserialize)]
210struct ActionDiagnosticEnvelope {
211 outcome: String,
212 crate_name: Option<String>,
213 diagnostic: ActionDiagnostic,
214}
215
216fn parse_action_diagnostic(
217 message: &str,
218) -> Option<Result<(String, Option<String>, ActionDiagnostic)>> {
219 let payload = message.strip_prefix(ACTION_DIAGNOSTIC_PREFIX)?;
220 Some((|| {
221 let envelope: ActionDiagnosticEnvelope = serde_json::from_str(payload)?;
222 if !matches!(envelope.outcome.as_str(), "hit" | "miss") {
223 bail!("invalid action diagnostic outcome");
224 }
225 validate_crate_name(envelope.crate_name.as_deref())?;
226 Ok((envelope.outcome, envelope.crate_name, envelope.diagnostic))
227 })())
228}
229
230fn atomic_saturating_add(target: &AtomicU64, value: u64) {
231 let _ = target.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
232 Some(current.saturating_add(value))
233 });
234}
235
236fn queue_prefetch_digest(
237 verified: &BTreeMap<CacheDigest, PathBuf>,
238 pending: &mut BTreeMap<CacheDigest, ()>,
239 digest: CacheDigest,
240) {
241 if verified.contains_key(&digest) || pending.contains_key(&digest) {
242 return;
243 }
244 pending.insert(digest, ());
245}
246
247fn queue_prefetch_directory(
248 seen: &BTreeMap<CacheDigest, ()>,
249 pending: &mut BTreeMap<CacheDigest, ()>,
250 digest: CacheDigest,
251 limit: usize,
252) -> bool {
253 if seen.contains_key(&digest) || pending.contains_key(&digest) {
254 return true;
255 }
256 if seen.len().saturating_add(pending.len()) >= limit {
257 return false;
258 }
259 pending.insert(digest, ());
260 true
261}
262
263pub type TaskLoader = dyn for<'a> Fn(&'a CacheAgent, &'a str) -> BoxFuture<'a, ()> + Send + Sync;
265
266#[derive(Clone)]
272pub struct CacheAgent {
273 cas: LocalCas,
274 actions: LocalActionCache,
275 verified_blobs: Arc<Mutex<BTreeMap<CacheDigest, VerifiedBlob>>>,
276 version: Arc<str>,
277 write_locks: Arc<Mutex<BTreeMap<CacheDigest, Weak<tokio::sync::Mutex<()>>>>>,
278 action_locks: Arc<Mutex<BTreeMap<CacheDigest, Weak<tokio::sync::Mutex<()>>>>>,
279 stats: Arc<AtomicAgentStats>,
280 observer: Option<Arc<dyn AgentEventObserver>>,
281 observer_emission: Arc<Mutex<()>>,
282 task_loader: Option<Arc<TaskLoader>>,
285 executable_identities: Arc<Mutex<BTreeMap<ExecutableIdentityKey, Vec<u8>>>>,
286 manifest_dir: Arc<PathBuf>,
287 task_actions: Arc<Mutex<BTreeMap<String, TaskActionState>>>,
288 task_fallbacks: Arc<Mutex<BTreeMap<String, TaskFallbacks>>>,
290 next_task_run: Arc<AtomicU64>,
291 manifest_write_lock: Arc<Mutex<()>>,
292 remote: Option<Arc<RemoteCacheClient>>,
293 remote_mode: RemoteCacheMode,
294 remote_staging_dir: Arc<PathBuf>,
295 remote_download_limit: u64,
296 remote_download_bytes: Arc<AtomicU64>,
297 pending_remote_actions: Arc<Mutex<BTreeMap<CacheDigest, RemoteActionResult>>>,
298 remote_transfers: Arc<tokio::sync::Semaphore>,
299 prefetch_transfers: Arc<tokio::sync::Semaphore>,
300 prefetch_tasks: Arc<Mutex<Vec<tokio::task::JoinHandle<()>>>>,
301 warnings: Arc<Mutex<BTreeSet<String>>>,
304 file_digests: Arc<Mutex<BTreeMap<(FileDigestScope, PathBuf), RecordedFileDigest>>>,
307 uploads: Option<UploadQueue>,
309}
310
311fn validate_file_digest_record(entry: &RecordedFileDigest) -> Result<()> {
314 if !entry.file.path.is_absolute() {
315 bail!("file-digest records need absolute paths");
316 }
317 entry.digest.validate()?;
318 if entry.file.len != entry.digest.size {
319 bail!("file-digest record length does not match its digest");
320 }
321 Ok(())
322}
323
324struct AgentUploadSink {
326 stats: Arc<AtomicAgentStats>,
327}
328
329impl UploadSink for AgentUploadSink {
330 fn record_blob_uploaded(&self, bytes: u64) {
331 self.stats
332 .background_uploads
333 .fetch_add(1, Ordering::Relaxed);
334 self.stats
335 .uploaded_bytes
336 .fetch_add(bytes, Ordering::Relaxed);
337 }
338
339 fn record_action_uploaded(&self) {
340 self.stats
341 .background_uploads
342 .fetch_add(1, Ordering::Relaxed);
343 }
344
345 fn record_blob_pack_uploaded(&self, blobs: u64) {
346 self.stats
347 .remote_blob_pack_uploads
348 .fetch_add(1, Ordering::Relaxed);
349 self.stats
350 .remote_blob_pack_upload_blobs
351 .fetch_add(blobs, Ordering::Relaxed);
352 }
353
354 fn record_upload_failure(&self) {
355 self.stats
356 .background_upload_failures
357 .fetch_add(1, Ordering::Relaxed);
358 self.stats.remote_failures.fetch_add(1, Ordering::Relaxed);
359 }
360}
361
362#[derive(Debug, Clone)]
368struct VerifiedBlob {
369 path: PathBuf,
370 len: u64,
371 modified: SystemTime,
372}
373
374impl VerifiedBlob {
375 fn describe(path: &Path) -> Option<Self> {
378 let metadata = std::fs::metadata(path).ok()?;
379 Some(Self {
380 path: path.to_path_buf(),
381 len: metadata.len(),
382 modified: metadata.modified().ok()?,
383 })
384 }
385
386 fn is_unchanged(&self) -> bool {
388 let Ok(metadata) = std::fs::metadata(&self.path) else {
389 return false;
390 };
391 metadata.len() == self.len && metadata.modified().is_ok_and(|now| now == self.modified)
392 }
393}
394
395#[derive(Debug, Clone, Default)]
396struct TaskActionState {
397 manifest: String,
398 baseline_loaded: bool,
399 predictions: BTreeMap<CacheDigest, ActionPrediction>,
400 pending_predictions: BTreeMap<CacheDigest, ActionPrediction>,
401 prefetched_adapters: BTreeSet<String>,
402 remote_etag: Option<String>,
403}
404
405fn activate_prediction_adapter(
407 state: &mut TaskActionState,
408 invocation: &CacheDigest,
409) -> (Option<ActionPrediction>, Option<Vec<ActionPrediction>>) {
410 let prediction = state.predictions.get(invocation).cloned();
411 let prefetch = prediction.as_ref().and_then(|prediction| {
412 if !state.prefetched_adapters.insert(prediction.adapter.clone()) {
413 return None;
414 }
415 Some(
416 state
417 .predictions
418 .values()
419 .filter(|candidate| candidate.adapter == prediction.adapter)
420 .cloned()
421 .collect(),
422 )
423 });
424 (prediction, prefetch)
425}
426
427struct PrefetchedAction {
428 adapter: String,
429 result: RemoteActionResult,
430}
431
432struct RemoteDownloadReservation {
433 counter: Arc<AtomicU64>,
434 reserved: u64,
435 committed: bool,
436}
437
438impl RemoteDownloadReservation {
439 fn bytes(&self) -> u64 {
440 self.reserved
441 }
442
443 fn commit(mut self, bytes: u64) {
444 debug_assert!(bytes <= self.reserved);
445 self.counter
446 .fetch_sub(self.reserved.saturating_sub(bytes), Ordering::AcqRel);
447 self.committed = true;
448 }
449}
450
451impl Drop for RemoteDownloadReservation {
452 fn drop(&mut self) {
453 if !self.committed {
454 self.counter.fetch_sub(self.reserved, Ordering::AcqRel);
455 }
456 }
457}
458
459#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
460struct ExecutableIdentityKey {
461 executable: PathBuf,
462 environment: BTreeMap<String, Option<String>>,
463}
464
465impl CacheAgent {
466 pub fn new(cache_dir: impl Into<PathBuf>, version: impl Into<Arc<str>>) -> Self {
468 Self::build(cache_dir.into(), version.into(), None, 0)
469 }
470
471 pub fn new_remote(
473 cache_dir: impl Into<PathBuf>,
474 version: impl Into<Arc<str>>,
475 remote: AgentRemoteCache,
476 ) -> Self {
477 Self::build(
478 cache_dir.into(),
479 version.into(),
480 Some(remote),
481 DEFAULT_MAX_REMOTE_DOWNLOAD_BYTES,
482 )
483 }
484
485 pub fn new_remote_with_download_limit(
487 cache_dir: impl Into<PathBuf>,
488 version: impl Into<Arc<str>>,
489 remote: AgentRemoteCache,
490 max_remote_download_bytes: u64,
491 ) -> Self {
492 Self::build(
493 cache_dir.into(),
494 version.into(),
495 Some(remote),
496 max_remote_download_bytes,
497 )
498 }
499
500 fn build(
501 cache_dir: PathBuf,
502 version: Arc<str>,
503 remote: Option<AgentRemoteCache>,
504 remote_download_limit: u64,
505 ) -> Self {
506 let remote_mode = remote
507 .as_ref()
508 .map_or(RemoteCacheMode::ReadOnly, |remote| remote.mode);
509 let remote_staging_dir = remote.as_ref().map_or_else(
510 || cache_dir.join("remote"),
511 |remote| remote.staging_dir.clone(),
512 );
513 let remote = remote.map(|remote| Arc::new(remote.client));
514 let stats = Arc::new(AtomicAgentStats::default());
515 let remote_transfers = Arc::new(tokio::sync::Semaphore::new(MAX_REMOTE_TRANSFERS));
516 let uploads = remote
519 .clone()
520 .filter(|_| remote_mode.writes())
521 .map(|client| {
522 UploadQueue::new(
523 client,
524 Arc::new(AgentUploadSink {
525 stats: stats.clone(),
526 }),
527 remote_transfers.clone(),
528 )
529 });
530 Self {
531 cas: LocalCas::new(cache_dir.clone()),
532 actions: LocalActionCache::new(cache_dir.clone()),
533 verified_blobs: Arc::new(Mutex::new(BTreeMap::new())),
534 version,
535 write_locks: Arc::new(Mutex::new(BTreeMap::new())),
536 action_locks: Arc::new(Mutex::new(BTreeMap::new())),
537 stats,
538 observer: None,
539 observer_emission: Arc::new(Mutex::new(())),
540 task_loader: None,
541 executable_identities: Arc::new(Mutex::new(BTreeMap::new())),
542 manifest_dir: Arc::new(task_manifest_dir(&cache_dir)),
543 task_actions: Arc::new(Mutex::new(BTreeMap::new())),
544 task_fallbacks: Arc::new(Mutex::new(BTreeMap::new())),
545 next_task_run: Arc::new(AtomicU64::new(0)),
546 manifest_write_lock: Arc::new(Mutex::new(())),
547 remote,
548 remote_mode,
549 remote_staging_dir: Arc::new(remote_staging_dir),
550 remote_download_limit,
551 remote_download_bytes: Arc::new(AtomicU64::new(0)),
552 pending_remote_actions: Arc::new(Mutex::new(BTreeMap::new())),
553 remote_transfers,
554 prefetch_transfers: Arc::new(tokio::sync::Semaphore::new(MAX_PREFETCH_TRANSFERS)),
555 prefetch_tasks: Arc::new(Mutex::new(Vec::new())),
556 warnings: Arc::new(Mutex::new(BTreeSet::new())),
557 file_digests: Arc::new(Mutex::new(BTreeMap::new())),
558 uploads,
559 }
560 }
561
562 #[must_use]
564 pub fn with_observer(mut self, observer: Arc<dyn AgentEventObserver>) -> Self {
565 self.observer = Some(observer);
566 self
567 }
568
569 pub fn with_task_loader(mut self, loader: Arc<TaskLoader>) -> Self {
576 self.task_loader = Some(loader);
577 self
578 }
579
580 async fn ensure_task_loaded(&self, task: &str) {
583 let Some(loader) = &self.task_loader else {
584 return;
585 };
586 if self.task_actions.lock().unwrap().contains_key(task) {
587 return;
588 }
589 loader(self, task).await;
590 }
591
592 fn emit(&self, event: impl FnOnce() -> AgentEvent) {
593 if let Some(observer) = &self.observer {
594 let _emission = self.observer_emission.lock().unwrap();
595 observer.event(event());
596 }
597 }
598
599 fn emit_action(&self, diagnostic: Option<AgentEvent>, action: AgentEvent) {
600 if let Some(observer) = &self.observer {
601 let _emission = self.observer_emission.lock().unwrap();
602 if let Some(diagnostic) = diagnostic {
603 observer.event(diagnostic);
604 }
605 observer.event(action);
606 }
607 }
608
609 fn reserve_remote_download(&self, bytes: u64) -> Result<RemoteDownloadReservation> {
610 let mut current = self.remote_download_bytes.load(Ordering::Acquire);
611 loop {
612 let next = current
613 .checked_add(bytes)
614 .ok_or_else(|| eyre::eyre!("remote cache download budget overflowed"))?;
615 if next > self.remote_download_limit {
616 bail!(
617 "remote cache download budget exceeded: {} bytes requested with {} of {} bytes already used",
618 bytes,
619 current,
620 self.remote_download_limit
621 );
622 }
623 match self.remote_download_bytes.compare_exchange_weak(
624 current,
625 next,
626 Ordering::AcqRel,
627 Ordering::Acquire,
628 ) {
629 Ok(_) => {
630 return Ok(RemoteDownloadReservation {
631 counter: self.remote_download_bytes.clone(),
632 reserved: bytes,
633 committed: false,
634 });
635 }
636 Err(observed) => current = observed,
637 }
638 }
639 }
640
641 fn reserve_remote_download_up_to(&self, requested: u64) -> Result<RemoteDownloadReservation> {
642 let mut current = self.remote_download_bytes.load(Ordering::Acquire);
643 loop {
644 let reserved = requested.min(self.remote_download_limit.saturating_sub(current));
645 let next = current + reserved;
646 match self.remote_download_bytes.compare_exchange_weak(
647 current,
648 next,
649 Ordering::AcqRel,
650 Ordering::Acquire,
651 ) {
652 Ok(_) => {
653 return Ok(RemoteDownloadReservation {
654 counter: self.remote_download_bytes.clone(),
655 reserved,
656 committed: false,
657 });
658 }
659 Err(observed) => current = observed,
660 }
661 }
662 }
663
664 pub async fn begin_task(&self, task: &str) -> Result<String> {
666 self.begin_task_with_remote_errors(task, false, None, true)
667 .await
668 }
669
670 pub async fn begin_task_on_prediction(&self, task: &str) -> Result<String> {
673 self.begin_task_with_remote_errors(task, false, None, false)
674 .await
675 }
676
677 pub async fn begin_session_task(&self, task: &str) -> Result<()> {
683 self.begin_task_with_remote_errors(task, false, Some(task.to_string()), false)
684 .await?;
685 Ok(())
686 }
687
688 pub fn register_task_fallbacks(
699 &self,
700 task: &str,
701 fallbacks: impl Fn() -> Vec<String> + Send + Sync + 'static,
702 ) -> Result<()> {
703 validate_task_identity(task)?;
704 self.task_fallbacks
705 .lock()
706 .unwrap()
707 .insert(task.to_string(), Arc::new(fallbacks));
708 Ok(())
709 }
710
711 pub async fn prefetch_task(&self, task: &str) -> Result<String> {
713 let run = self
714 .begin_task_with_remote_errors(task, true, None, true)
715 .await?;
716 self.wait_for_prefetches().await;
717 Ok(run)
718 }
719
720 async fn begin_task_with_remote_errors(
721 &self,
722 task: &str,
723 strict: bool,
724 run: Option<String>,
725 eager_prefetch: bool,
726 ) -> Result<String> {
727 validate_task_identity(task)?;
728 let early_manifest = {
734 let _write_guard = self.manifest_write_lock.lock().unwrap();
735 let _file_guard = self.lock_task_manifest(task)?;
736 self.load_task_manifest(task)?
737 };
738 let early_actions: BTreeSet<_> = early_manifest
739 .iter()
740 .flat_map(|manifest| manifest.predictions.iter())
741 .map(|prediction| prediction.action.clone())
742 .collect();
743 if eager_prefetch {
744 self.spawn_prefetch_predictions(
745 early_manifest
746 .as_ref()
747 .map(|manifest| manifest.predictions.clone())
748 .unwrap_or_default(),
749 );
750 }
751 let (remote_manifest, mut remote_etag) = if self.remote_mode.reads() {
752 match self.get_remote_task_manifest(task).await {
753 Ok(Some((manifest, etag))) => (Some(manifest), Some(etag)),
754 Ok(None) => (None, None),
755 Err(error) => {
756 if strict {
757 return Err(error).wrap_err_with(|| {
758 format!("remote task action manifest lookup failed for {task}")
759 });
760 }
761 self.note_remote_failure();
762 warn!("remote task action manifest lookup failed for {task}: {error}");
763 (None, None)
764 }
765 }
766 } else {
767 (None, None)
768 };
769 let manifest = if self.remote.is_some() && self.remote_mode.reads() {
773 let _write_guard = self.manifest_write_lock.lock().unwrap();
774 let _file_guard = self.lock_task_manifest(task)?;
775 let local_manifest = self.load_task_manifest(task)?;
776 let manifest = match (remote_manifest, local_manifest) {
777 (Some(remote), Some(local)) => {
778 let (manifest, merged) = merge_remote_task_manifest(task, remote, local);
779 if !merged {
780 remote_etag = None;
781 }
782 Some(manifest)
783 }
784 (Some(remote), None) => Some(remote),
785 (None, local) => local,
786 };
787 if let Some(manifest) = &manifest {
788 self.persist_task_manifest(manifest)?;
789 }
790 manifest
791 } else {
792 early_manifest
793 };
794 let (manifest, remote_etag) = match manifest {
795 Some(manifest) => (Some(manifest), remote_etag),
796 None => (self.inherit_task_manifest(task, strict).await?, None),
799 };
800 let mut state = if let Some(manifest) = manifest {
801 TaskActionState {
802 manifest: task.to_string(),
803 baseline_loaded: true,
804 predictions: manifest
805 .predictions
806 .into_iter()
807 .map(|prediction| (prediction.invocation.clone(), prediction))
808 .collect(),
809 pending_predictions: BTreeMap::new(),
810 prefetched_adapters: BTreeSet::new(),
811 remote_etag,
812 }
813 } else {
814 TaskActionState {
815 manifest: task.to_string(),
816 baseline_loaded: true,
817 remote_etag,
818 ..TaskActionState::default()
819 }
820 };
821 let run = run.unwrap_or_else(|| {
822 let sequence = self.next_task_run.fetch_add(1, Ordering::Relaxed);
823 CacheDigest::blake3(format!("{task}\0{}\0{sequence}", std::process::id()).as_bytes())
824 .hash
825 });
826 self.stats.predictions_loaded.fetch_max(
830 state.predictions.len().try_into().unwrap_or(u64::MAX),
831 Ordering::Relaxed,
832 );
833 if eager_prefetch {
834 state.prefetched_adapters.extend(
835 state
836 .predictions
837 .values()
838 .map(|prediction| prediction.adapter.clone()),
839 );
840 }
841 let predictions = state
845 .predictions
846 .values()
847 .filter(|prediction| !early_actions.contains(&prediction.action))
848 .cloned()
849 .collect();
850 self.task_actions.lock().unwrap().insert(run.clone(), state);
851 if eager_prefetch {
852 self.spawn_prefetch_predictions(predictions);
853 }
854 Ok(run)
855 }
856
857 async fn inherit_task_manifest(
865 &self,
866 task: &str,
867 strict: bool,
868 ) -> Result<Option<TaskActionManifest>> {
869 let fallbacks = self.task_fallbacks.lock().unwrap().get(task).cloned();
870 let Some(fallbacks) = fallbacks else {
871 return Ok(None);
872 };
873 let named = tokio::task::spawn_blocking(move || fallbacks()).await?;
874 let mut tried = BTreeSet::from([task.to_string()]);
875 let candidates = named.into_iter().map(|identity| (identity, true)).chain(
876 self.newest_task_identities()
877 .into_iter()
878 .map(|identity| (identity, false)),
879 );
880 for (identity, named) in candidates {
881 if !tried.insert(identity.clone()) || validate_task_identity(&identity).is_err() {
882 continue;
883 }
884 let mut found = self.load_task_manifest(&identity)?;
885 if found.is_none() && named && self.remote_mode.reads() {
886 found = match self.get_remote_task_manifest(&identity).await {
887 Ok(manifest) => manifest.map(|(manifest, _)| manifest),
888 Err(error) => {
889 if strict {
890 return Err(error).wrap_err_with(|| {
891 format!("remote task action manifest lookup failed for {identity}")
892 });
893 }
894 self.note_remote_failure();
895 warn!("remote task action manifest lookup failed for {identity}: {error}");
896 None
897 }
898 };
899 }
900 let Some(mut manifest) = found else {
901 continue;
902 };
903 if manifest.predictions.is_empty()
904 || (!named && manifest.predictions.len() > MAX_TASK_ACTION_PREDICTIONS / 2)
905 {
906 continue;
907 }
908 info!(
909 "no predictions were recorded for {task}; inheriting {} from {identity}",
910 manifest.predictions.len()
911 );
912 manifest.task = task.to_string();
913 validate_task_manifest(&manifest, task)?;
914 let _write_guard = self.manifest_write_lock.lock().unwrap();
915 let _file_guard = self.lock_task_manifest(task)?;
916 if let Some(recorded) = self.load_task_manifest(task)? {
920 return Ok(Some(recorded));
921 }
922 self.persist_task_manifest(&manifest)?;
923 return Ok(Some(manifest));
924 }
925 Ok(None)
926 }
927
928 fn newest_task_identities(&self) -> Vec<String> {
930 let Ok(entries) = fs::read_dir(self.manifest_dir.as_path()) else {
931 return Vec::new();
932 };
933 let mut manifests: Vec<(std::time::SystemTime, String)> = entries
934 .filter_map(|entry| {
935 let entry = entry.ok()?;
936 let name = entry.file_name();
937 let identity = name.to_str()?.strip_suffix(".json")?.to_string();
938 validate_task_identity(&identity).ok()?;
939 let modified = entry.metadata().ok()?.modified().ok()?;
940 Some((modified, identity))
941 })
942 .collect();
943 manifests.sort_by(|left, right| right.cmp(left));
944 manifests
945 .into_iter()
946 .take(NEWEST_MANIFEST_CANDIDATES)
947 .map(|(_, identity)| identity)
948 .collect()
949 }
950
951 pub async fn cancel_prefetches(&self) {
953 let tasks = std::mem::take(&mut *self.prefetch_tasks.lock().unwrap());
954 for task in &tasks {
955 task.abort();
956 }
957 for task in tasks {
958 if let Err(error) = task.await
959 && !error.is_cancelled()
960 {
961 warn!("remote action prefetch task failed: {error}");
962 }
963 }
964 }
965
966 pub async fn wait_for_uploads(&self) {
973 let Some(uploads) = &self.uploads else {
974 return;
975 };
976 let _timer = AtomicDurationTimer::start(&self.stats.upload_drain_duration_ns);
977 uploads.drain().await;
978 }
979
980 async fn wait_for_prefetches(&self) {
981 let tasks = std::mem::take(&mut *self.prefetch_tasks.lock().unwrap());
982 for task in tasks {
983 if let Err(error) = task.await {
984 warn!("remote action prefetch task failed: {error}");
985 }
986 }
987 }
988
989 pub async fn commit_task(&self, run: &str) -> Result<()> {
991 self.commit_task_actions(run).await.map(|_| ())
992 }
993
994 pub async fn commit_task_actions(&self, run: &str) -> Result<Vec<ActionPrediction>> {
1000 validate_task_identity(run)?;
1001 let state = {
1002 let mut runs = self.task_actions.lock().unwrap();
1003 let state = runs
1004 .get(run)
1005 .ok_or_else(|| eyre::eyre!("task action manifest baseline was not loaded"))?;
1006 if !state.baseline_loaded {
1007 bail!("task action manifest baseline was not loaded");
1008 }
1009 if state.pending_predictions.is_empty()
1015 && (self.remote.is_none() || !self.remote_mode.writes())
1016 {
1017 runs.remove(run);
1018 return Ok(Vec::new());
1019 }
1020 state.clone()
1021 };
1022 let task = state.manifest;
1023 validate_task_identity(&task)?;
1024 let completed = state
1025 .pending_predictions
1026 .values()
1027 .cloned()
1028 .collect::<Vec<_>>();
1029 let (manifest, introduced) = {
1030 let _write_guard = self.manifest_write_lock.lock().unwrap();
1031 let _file_guard = self.lock_task_manifest(&task)?;
1032 let mut predictions = self
1033 .load_task_manifest(&task)?
1034 .map(|manifest| {
1035 manifest
1036 .predictions
1037 .into_iter()
1038 .map(|prediction| (prediction.invocation.clone(), prediction))
1039 .collect::<BTreeMap<_, _>>()
1040 })
1041 .unwrap_or_default();
1042 let introduced: BTreeSet<CacheDigest> = state
1047 .pending_predictions
1048 .keys()
1049 .filter(|invocation| !predictions.contains_key(*invocation))
1050 .cloned()
1051 .collect();
1052 predictions.extend(state.pending_predictions);
1057 let manifest = TaskActionManifest {
1058 version: TASK_ACTION_MANIFEST_VERSION,
1059 task: task.clone(),
1060 predictions: predictions.into_values().collect(),
1061 };
1062 validate_task_manifest(&manifest, &task)?;
1063 self.persist_task_manifest(&manifest)?;
1064 (manifest, introduced)
1065 };
1066 self.task_actions.lock().unwrap().remove(run);
1067 if self.remote_mode.writes() {
1068 let mut manifest = manifest;
1072 if let Some(uploads) = &self.uploads {
1073 let actions: Vec<CacheDigest> = manifest
1074 .predictions
1075 .iter()
1076 .map(|prediction| prediction.action.clone())
1077 .collect();
1078 let unpublished = uploads.wait_for_actions(&actions).await;
1079 let withheld = manifest
1080 .predictions
1081 .iter()
1082 .filter(|prediction| {
1083 introduced.contains(&prediction.invocation)
1084 && unpublished.contains(&prediction.action)
1085 })
1086 .count();
1087 if withheld > 0 {
1088 warn!(
1091 "{withheld} of {} predicted actions were not published, so the remote task action manifest omits them",
1092 manifest.predictions.len()
1093 );
1094 manifest.predictions.retain(|prediction| {
1095 !(introduced.contains(&prediction.invocation)
1096 && unpublished.contains(&prediction.action))
1097 });
1098 }
1099 }
1100 match self
1101 .put_remote_task_manifest(&task, manifest, state.remote_etag)
1102 .await
1103 {
1104 Ok(remote_manifest) => {
1105 let _write_guard = self.manifest_write_lock.lock().unwrap();
1106 let reconciliation = (|| {
1107 let _file_guard = self.lock_task_manifest(&task)?;
1108 let manifest = match self.load_task_manifest(&task)? {
1109 Some(local) => {
1110 merge_remote_task_manifest(&task, remote_manifest, local).0
1111 }
1112 None => remote_manifest,
1113 };
1114 self.persist_task_manifest(&manifest)
1115 })();
1116 if let Err(error) = reconciliation {
1117 warn!(
1118 "remote task action manifest reconciliation failed for {task}: {error}"
1119 );
1120 }
1121 }
1122 Err(error) => {
1123 self.note_remote_failure();
1124 warn!("remote task action manifest upload failed for {task}: {error}");
1125 }
1126 }
1127 }
1128 Ok(completed)
1129 }
1130
1131 fn task_manifest_path(&self, task: &str) -> PathBuf {
1132 self.manifest_dir.join(format!("{task}.json"))
1133 }
1134
1135 fn task_manifest_lock_path(&self, task: &str) -> PathBuf {
1136 self.manifest_dir.join("locks").join(format!("{task}.lock"))
1137 }
1138
1139 fn lock_task_manifest(&self, task: &str) -> Result<fslock::LockFile> {
1140 let path = self.task_manifest_lock_path(task);
1141 fs::create_dir_all(path.parent().expect("task manifest lock has a parent"))?;
1142 let mut lock = fslock::LockFile::open(&path)?;
1143 lock.lock()?;
1144 Ok(lock)
1145 }
1146
1147 fn load_task_manifest(&self, task: &str) -> Result<Option<TaskActionManifest>> {
1148 match fs::read(self.task_manifest_path(task)) {
1149 Ok(contents) => Ok(Some(self.parse_task_manifest(task, &contents, false)?)),
1150 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
1151 Err(error) => Err(error.into()),
1152 }
1153 }
1154
1155 fn parse_task_manifest(
1156 &self,
1157 task: &str,
1158 contents: &[u8],
1159 require_canonical: bool,
1160 ) -> Result<TaskActionManifest> {
1161 let manifest: TaskActionManifest = serde_json::from_slice(contents)?;
1162 validate_task_manifest(&manifest, task)?;
1163 if require_canonical && canonical_json(&manifest)? != contents {
1164 bail!("task action manifest is not canonical JSON");
1165 }
1166 Ok(manifest)
1167 }
1168
1169 fn task_manifest_selector(task: &str) -> Result<(Vec<u8>, CacheDigest)> {
1170 TaskActionManifest::selector(task)
1171 }
1172
1173 fn persist_task_manifest(&self, manifest: &TaskActionManifest) -> Result<()> {
1174 let bytes = canonical_json(manifest)?;
1175 fs::create_dir_all(self.manifest_dir.as_path())?;
1176 let mut temporary = tempfile::NamedTempFile::new_in(self.manifest_dir.as_path())?;
1177 std::io::Write::write_all(temporary.as_file_mut(), &bytes)?;
1178 temporary.as_file_mut().sync_all()?;
1179 temporary
1180 .persist(self.task_manifest_path(&manifest.task))
1181 .map_err(|error| error.error)?;
1182 Ok(())
1183 }
1184
1185 async fn get_remote_task_manifest(
1186 &self,
1187 task: &str,
1188 ) -> Result<Option<(TaskActionManifest, String)>> {
1189 let Some(remote) = &self.remote else {
1190 return Ok(None);
1191 };
1192 let (_, selector) = Self::task_manifest_selector(task)?;
1193 let _permit = self.remote_transfers.acquire().await?;
1194 self.stats
1195 .remote_manifest_lookups
1196 .fetch_add(1, Ordering::Relaxed);
1197 let _timer = AtomicDurationTimer::start(&self.stats.remote_manifest_lookup_duration_ns);
1198 let Some(remote_manifest) = remote.get_action_manifest(&selector).await? else {
1199 return Ok(None);
1200 };
1201 let manifest = self.parse_task_manifest(task, &remote_manifest.bytes, true)?;
1202 Ok(Some((manifest, remote_manifest.etag)))
1203 }
1204
1205 async fn put_remote_task_manifest(
1206 &self,
1207 task: &str,
1208 mut manifest: TaskActionManifest,
1209 mut expected_etag: Option<String>,
1210 ) -> Result<TaskActionManifest> {
1211 let Some(remote) = &self.remote else {
1212 return Ok(manifest);
1213 };
1214 let (_, selector) = Self::task_manifest_selector(task)?;
1215 for _ in 0..4 {
1216 let bytes = canonical_json(&manifest)?;
1217 let outcome = {
1218 let _permit = self.remote_transfers.acquire().await?;
1219 remote
1220 .put_action_manifest(&selector, &bytes, expected_etag.as_deref())
1221 .await?
1222 };
1223 match outcome {
1224 ManifestPutOutcome::Stored => return Ok(manifest),
1225 ManifestPutOutcome::PreconditionFailed => {
1226 let Some((remote_manifest, etag)) = self.get_remote_task_manifest(task).await?
1227 else {
1228 expected_etag = None;
1229 continue;
1230 };
1231 manifest = merge_task_manifests(task, Some(remote_manifest), manifest)?;
1232 expected_etag = Some(etag);
1233 }
1234 }
1235 }
1236 bail!("remote task action manifest changed too frequently")
1237 }
1238
1239 pub fn stats(&self) -> AgentStats {
1241 AgentStats {
1242 session_duration_ns: 0,
1243 lookups: self.stats.lookups.load(Ordering::Relaxed),
1244 unconsulted: self.stats.unconsulted.load(Ordering::Relaxed),
1245 hits: self.stats.hits.load(Ordering::Relaxed),
1246 stores: self.stats.stores.load(Ordering::Relaxed),
1247 stored_bytes: self.stats.stored_bytes.load(Ordering::Relaxed),
1248 verifications: self.stats.verifications.load(Ordering::Relaxed),
1249 divergences: self.stats.divergences.load(Ordering::Relaxed),
1250 downloaded_bytes: self.stats.downloaded_bytes.load(Ordering::Relaxed),
1251 uploaded_bytes: self.stats.uploaded_bytes.load(Ordering::Relaxed),
1252 background_uploads: self.stats.background_uploads.load(Ordering::Relaxed),
1253 background_upload_failures: self
1254 .stats
1255 .background_upload_failures
1256 .load(Ordering::Relaxed),
1257 remote_blob_pack_uploads: self.stats.remote_blob_pack_uploads.load(Ordering::Relaxed),
1258 remote_blob_pack_upload_blobs: self
1259 .stats
1260 .remote_blob_pack_upload_blobs
1261 .load(Ordering::Relaxed),
1262 upload_drain_duration_ns: self.stats.upload_drain_duration_ns.load(Ordering::Relaxed),
1263 prefetched_actions: self.stats.prefetched_actions.load(Ordering::Relaxed),
1264 predictions_loaded: self.stats.predictions_loaded.load(Ordering::Relaxed),
1265 bypasses: self.stats.bypasses.lock().unwrap().clone(),
1266 avoided_compiler_duration_ns: self
1267 .stats
1268 .avoided_compiler_duration_ns
1269 .load(Ordering::Relaxed),
1270 compiler: self.stats.compiler.lock().unwrap().clone(),
1271 slow_compilations: self.stats.slow_compilations.lock().unwrap().clone(),
1272 remote_failures: self.stats.remote_failures.load(Ordering::Relaxed),
1273 remote_manifest_lookups: self.stats.remote_manifest_lookups.load(Ordering::Relaxed),
1274 remote_manifest_lookup_duration_ns: self
1275 .stats
1276 .remote_manifest_lookup_duration_ns
1277 .load(Ordering::Relaxed),
1278 remote_action_lookups: self.stats.remote_action_lookups.load(Ordering::Relaxed),
1279 remote_action_lookup_duration_ns: self
1280 .stats
1281 .remote_action_lookup_duration_ns
1282 .load(Ordering::Relaxed),
1283 remote_blob_requests: self.stats.remote_blob_requests.load(Ordering::Relaxed),
1284 remote_blob_pack_requests: self.stats.remote_blob_pack_requests.load(Ordering::Relaxed),
1285 remote_blob_pack_blobs: self.stats.remote_blob_pack_blobs.load(Ordering::Relaxed),
1286 remote_blob_transfer_duration_ns: self
1287 .stats
1288 .remote_blob_transfer_duration_ns
1289 .load(Ordering::Relaxed),
1290 local_cas_write_duration_ns: self
1291 .stats
1292 .local_cas_write_duration_ns
1293 .load(Ordering::Relaxed),
1294 prefetch_runs: self.stats.prefetch_runs.load(Ordering::Relaxed),
1295 prefetch_duration_ns: self.stats.prefetch_duration_ns.load(Ordering::Relaxed),
1296 materialization_duration_ns: self
1297 .stats
1298 .materialization_duration_ns
1299 .load(Ordering::Relaxed),
1300 restored_output_files: self.stats.restored_output_files.load(Ordering::Relaxed),
1301 restored_output_bytes: self.stats.restored_output_bytes.load(Ordering::Relaxed),
1302 reflinked_output_files: self.stats.reflinked_output_files.load(Ordering::Relaxed),
1303 reflinked_output_bytes: self.stats.reflinked_output_bytes.load(Ordering::Relaxed),
1304 copied_output_files: self.stats.copied_output_files.load(Ordering::Relaxed),
1305 copied_output_bytes: self.stats.copied_output_bytes.load(Ordering::Relaxed),
1306 reused_output_files: self.stats.reused_output_files.load(Ordering::Relaxed),
1307 reused_output_bytes: self.stats.reused_output_bytes.load(Ordering::Relaxed),
1308 }
1309 }
1310
1311 pub async fn handle_requests(
1318 &self,
1319 requests: impl IntoIterator<Item = AgentRequest>,
1320 ) -> Vec<AgentResponse> {
1321 let mut connection = ConnectionUploads::default();
1322 let mut responses = Vec::new();
1323 for request in requests {
1324 responses.push(self.respond_on(request, &mut connection).await);
1325 }
1326 responses
1327 }
1328
1329 fn write_lock(&self, digest: &CacheDigest) -> Arc<tokio::sync::Mutex<()>> {
1330 Self::digest_lock(&self.write_locks, digest)
1331 }
1332
1333 fn action_lock(&self, digest: &CacheDigest) -> Arc<tokio::sync::Mutex<()>> {
1334 Self::digest_lock(&self.action_locks, digest)
1335 }
1336
1337 fn digest_lock(
1338 locks: &Mutex<BTreeMap<CacheDigest, Weak<tokio::sync::Mutex<()>>>>,
1339 digest: &CacheDigest,
1340 ) -> Arc<tokio::sync::Mutex<()>> {
1341 let mut locks = locks.lock().unwrap();
1342 locks.retain(|_, lock| lock.strong_count() > 0);
1343 if let Some(lock) = locks.get(digest).and_then(Weak::upgrade) {
1344 return lock;
1345 }
1346 let lock = Arc::new(tokio::sync::Mutex::new(()));
1347 locks.insert(digest.clone(), Arc::downgrade(&lock));
1348 lock
1349 }
1350
1351 #[cfg(test)]
1356 async fn respond(&self, request: AgentRequest) -> AgentResponse {
1357 self.respond_on(request, &mut ConnectionUploads::default())
1358 .await
1359 }
1360
1361 async fn respond_on(
1362 &self,
1363 request: AgentRequest,
1364 connection: &mut ConnectionUploads,
1365 ) -> AgentResponse {
1366 match &request {
1367 AgentRequest::FindActionPrediction { task, .. }
1368 | AgentRequest::RecordActionPrediction { task, .. } => {
1369 self.ensure_task_loaded(task).await;
1370 }
1371 _ => {}
1372 }
1373 let result = match request {
1374 AgentRequest::BeginTask { task } => self
1375 .begin_task(&task)
1376 .await
1377 .map(|run| AgentResponse::TaskBegun { run }),
1378 AgentRequest::CommitTask { run } => self
1379 .commit_task(&run)
1380 .await
1381 .map(|()| AgentResponse::TaskCommitted),
1382 AgentRequest::FindBlob { digest } => self.find_blob(&digest).await,
1383 AgentRequest::FindBlobs { digests } => self.find_blobs(digests).await,
1384 AgentRequest::StoreBlob { digest, source } => {
1385 self.store_blob(&digest, &source, connection).await
1386 }
1387 AgentRequest::FindActionResult { action } => {
1388 self.stats.lookups.fetch_add(1, Ordering::Relaxed);
1389 self.find_action_result(&action).await
1390 }
1391 AgentRequest::RecordActionHit {
1392 action,
1393 restore,
1394 crate_name,
1395 } => {
1396 let diagnostic = connection
1397 .take_action_diagnostic("hit", crate_name.as_deref())
1398 .map(|diagnostic| AgentEvent::ActionDiagnostic {
1399 outcome: "hit".into(),
1400 crate_name: crate_name.clone(),
1401 diagnostic,
1402 });
1403 self.record_action_hit(&action, restore, crate_name, diagnostic)
1404 }
1405 AgentRequest::RecordBypass { kind } => {
1406 *self
1407 .stats
1408 .bypasses
1409 .lock()
1410 .unwrap()
1411 .entry(kind.clone())
1412 .or_insert(0) += 1;
1413 self.emit(|| AgentEvent::Bypass { kind });
1414 Ok(AgentResponse::BypassRecorded)
1415 }
1416 AgentRequest::RecordUnconsulted => {
1417 self.stats.unconsulted.fetch_add(1, Ordering::Relaxed);
1418 self.emit(|| AgentEvent::Unconsulted);
1419 Ok(AgentResponse::UnconsultedRecorded)
1420 }
1421 AgentRequest::RecordWarning { message } => match parse_action_diagnostic(&message) {
1422 Some(Ok((outcome, crate_name, diagnostic))) => {
1423 connection.record_action_diagnostic(outcome, crate_name, diagnostic);
1424 Ok(AgentResponse::WarningRecorded)
1425 }
1426 Some(Err(error)) => Err(error),
1427 None => self.record_warning(message),
1428 },
1429 AgentRequest::FindFileDigests { scope, files } => self.find_file_digests(scope, files),
1430 AgentRequest::JoinActionPromise {
1431 adapter,
1432 invocation,
1433 } => self.join_action_promise(&adapter, &invocation).await,
1434 AgentRequest::CompleteActionPromise { claim, prediction } => {
1435 self.complete_action_promise(&claim, &prediction).await
1436 }
1437 AgentRequest::RecordFileDigests { scope, entries } => {
1438 self.record_file_digests(scope, entries)
1439 }
1440 AgentRequest::RecordCompilerInvocation {
1441 outcome,
1442 crate_name,
1443 duration_ns,
1444 } => {
1445 let diagnostic = connection
1446 .take_action_diagnostic(&outcome, crate_name.as_deref())
1447 .map(|diagnostic| AgentEvent::ActionDiagnostic {
1448 outcome: outcome.clone(),
1449 crate_name: crate_name.clone(),
1450 diagnostic,
1451 });
1452 self.record_compiler_invocation(
1453 &outcome,
1454 crate_name.as_deref(),
1455 duration_ns,
1456 diagnostic,
1457 )
1458 }
1459 AgentRequest::RecordActionVerification { matched, restore } => {
1460 self.record_materialization(restore);
1461 self.stats.verifications.fetch_add(1, Ordering::Relaxed);
1462 if !matched {
1463 self.stats.divergences.fetch_add(1, Ordering::Relaxed);
1464 }
1465 self.emit(|| AgentEvent::Verification { matched, restore });
1466 Ok(AgentResponse::ActionVerificationRecorded)
1467 }
1468 AgentRequest::StoreActionResult { result } => {
1469 self.store_action_result(&result, connection).await
1470 }
1471 AgentRequest::FindActionPrediction { task, invocation } => {
1472 self.find_action_prediction(&task, &invocation)
1473 }
1474 AgentRequest::RecordActionPrediction { task, prediction } => {
1475 self.record_action_prediction(&task, prediction)
1476 }
1477 AgentRequest::FindExecutableIdentity {
1478 executable,
1479 environment,
1480 } => self.find_executable_identity(executable, environment),
1481 AgentRequest::StoreExecutableIdentity {
1482 executable,
1483 environment,
1484 stdout,
1485 } => self.store_executable_identity(executable, environment, stdout),
1486 AgentRequest::Hello { .. } => {
1487 Err(eyre::eyre!("hello is only valid as the first request"))
1488 }
1489 };
1490 result.unwrap_or_else(|error| AgentResponse::Error {
1491 message: error.to_string(),
1492 })
1493 }
1494
1495 async fn find_blob(&self, digest: &CacheDigest) -> Result<AgentResponse> {
1496 if let Some(path) = self.find_verified_blob(digest)? {
1497 return Ok(AgentResponse::Blob { path: Some(path) });
1498 }
1499 if !self.remote_mode.reads() {
1500 return Ok(AgentResponse::Blob { path: None });
1501 }
1502 let Some(remote) = &self.remote else {
1503 return Ok(AgentResponse::Blob { path: None });
1504 };
1505 match self.fetch_remote_blob(remote, digest).await {
1506 Ok(path) => Ok(AgentResponse::Blob { path: Some(path) }),
1507 Err(error) => {
1508 warn!(
1509 "remote cache blob lookup failed for {}: {error}",
1510 digest.hash
1511 );
1512 Ok(AgentResponse::Blob { path: None })
1513 }
1514 }
1515 }
1516
1517 async fn find_blobs(&self, digests: Vec<CacheDigest>) -> Result<AgentResponse> {
1518 let mut paths = BTreeMap::new();
1519 let mut missing = Vec::new();
1520 for digest in &digests {
1521 match self.find_verified_blob(digest)? {
1522 Some(path) => {
1523 paths.insert(digest.clone(), path);
1524 }
1525 None => {
1526 missing.push(digest.clone());
1527 }
1528 }
1529 }
1530
1531 if !missing.is_empty()
1532 && self.remote_mode.reads()
1533 && let Some(remote) = &self.remote
1534 {
1535 paths.extend(self.fetch_remote_blobs(remote, missing, None).await);
1536 }
1537
1538 Ok(AgentResponse::Blobs {
1539 paths: digests
1540 .into_iter()
1541 .map(|digest| paths.get(&digest).cloned())
1542 .collect(),
1543 })
1544 }
1545
1546 async fn store_blob(
1547 &self,
1548 digest: &CacheDigest,
1549 source: &Path,
1550 connection: &mut ConnectionUploads,
1551 ) -> Result<AgentResponse> {
1552 let path = {
1553 let lock = self.write_lock(digest);
1554 let _guard = lock.lock().await;
1555 if let Some(path) = self.find_verified_blob(digest)? {
1556 path
1557 } else {
1558 let path = self.cas.store_file(digest, source)?;
1559 self.remember_verified_blob(digest, &path);
1560 self.stats.stores.fetch_add(1, Ordering::Relaxed);
1561 self.stats
1562 .stored_bytes
1563 .fetch_add(digest.size, Ordering::Relaxed);
1564 path
1565 }
1566 };
1567 if let Some(uploads) = &self.uploads {
1570 uploads.queue_blob(digest, path.clone(), connection);
1571 }
1572 Ok(AgentResponse::Stored { path })
1573 }
1574
1575 fn find_verified_blob(&self, digest: &CacheDigest) -> Result<Option<PathBuf>> {
1576 let remembered = self.verified_blobs.lock().unwrap().get(digest).cloned();
1577 if let Some(remembered) = remembered {
1578 if remembered.is_unchanged() {
1589 return Ok(Some(remembered.path));
1590 }
1591 self.verified_blobs.lock().unwrap().remove(digest);
1592 }
1593 let path = self.cas.find(digest)?;
1594 if let Some(path) = &path {
1595 self.remember_verified_blob(digest, path);
1596 }
1597 Ok(path)
1598 }
1599
1600 fn remember_verified_blob(&self, digest: &CacheDigest, path: &Path) {
1601 let Some(verified) = VerifiedBlob::describe(path) else {
1602 return;
1606 };
1607 self.verified_blobs
1608 .lock()
1609 .unwrap()
1610 .insert(digest.clone(), verified);
1611 }
1612
1613 async fn find_action_result(&self, action: &CacheDigest) -> Result<AgentResponse> {
1614 if let Some(result) = self.actions.find(action)? {
1615 return Ok(AgentResponse::ActionResult {
1616 result: Some(result),
1617 });
1618 }
1619 if !self.remote_mode.reads() {
1620 return Ok(AgentResponse::ActionResult { result: None });
1621 }
1622 let Some(remote) = &self.remote else {
1623 return Ok(AgentResponse::ActionResult { result: None });
1624 };
1625 let lock = self.action_lock(action);
1626 let _guard = lock.lock().await;
1627 if let Some(result) = self.actions.find(action)? {
1628 return Ok(AgentResponse::ActionResult {
1629 result: Some(result),
1630 });
1631 }
1632 if let Some(result) = self
1633 .pending_remote_actions
1634 .lock()
1635 .unwrap()
1636 .get(action)
1637 .cloned()
1638 {
1639 return Ok(AgentResponse::ActionResult {
1640 result: Some(result),
1641 });
1642 }
1643 let _permit = self.remote_transfers.acquire().await?;
1644 match self.get_remote_action_result(remote, action).await {
1645 Ok(Some(result)) => {
1646 self.pending_remote_actions
1647 .lock()
1648 .unwrap()
1649 .insert(action.clone(), result.clone());
1650 Ok(AgentResponse::ActionResult {
1651 result: Some(result),
1652 })
1653 }
1654 Ok(None) => Ok(AgentResponse::ActionResult { result: None }),
1655 Err(error) => {
1656 self.note_remote_failure();
1657 warn!(
1658 "remote cache action lookup failed for {}: {error}",
1659 action.hash
1660 );
1661 Ok(AgentResponse::ActionResult { result: None })
1662 }
1663 }
1664 }
1665
1666 async fn store_action_result(
1667 &self,
1668 result: &RemoteActionResult,
1669 connection: &ConnectionUploads,
1670 ) -> Result<AgentResponse> {
1671 let path = self.actions.store(result)?;
1672 if let Some(uploads) = &self.uploads {
1673 uploads.queue_action_result(result, connection);
1674 }
1675 Ok(AgentResponse::ActionStored { path })
1676 }
1677
1678 async fn join_action_promise(
1679 &self,
1680 adapter: &str,
1681 invocation: &CacheDigest,
1682 ) -> Result<AgentResponse> {
1683 if !self.remote_mode.reads() || !self.remote_mode.writes() {
1684 return Ok(AgentResponse::ActionPromise {
1685 claim: None,
1686 prediction: None,
1687 });
1688 }
1689 let Some(remote) = &self.remote else {
1690 return Ok(AgentResponse::ActionPromise {
1691 claim: None,
1692 prediction: None,
1693 });
1694 };
1695 let deadline = Instant::now() + ACTION_PROMISE_WAIT;
1696 loop {
1697 let _permit = self.remote_transfers.acquire().await?;
1698 let state = remote.join_action_promise(invocation, adapter).await;
1699 drop(_permit);
1700 match state {
1701 Ok(Some(ActionPromiseState::Claimed { claim })) => {
1702 return Ok(AgentResponse::ActionPromise {
1703 claim: Some(claim),
1704 prediction: None,
1705 });
1706 }
1707 Ok(Some(ActionPromiseState::Complete { prediction })) => {
1708 return Ok(AgentResponse::ActionPromise {
1709 claim: None,
1710 prediction: Some(prediction),
1711 });
1712 }
1713 Ok(Some(ActionPromiseState::Pending { retry_after_ms }))
1714 if Instant::now() < deadline =>
1715 {
1716 tokio::time::sleep(Duration::from_millis(retry_after_ms.clamp(10, 5_000)))
1717 .await;
1718 }
1719 Ok(Some(ActionPromiseState::Pending { .. }) | None) => {
1720 return Ok(AgentResponse::ActionPromise {
1721 claim: None,
1722 prediction: None,
1723 });
1724 }
1725 Ok(Some(_)) => {
1726 return Ok(AgentResponse::ActionPromise {
1727 claim: None,
1728 prediction: None,
1729 });
1730 }
1731 Err(error) => {
1732 self.note_remote_failure();
1733 warn!(
1734 "remote cache action promise failed for {}: {error}",
1735 invocation.hash
1736 );
1737 return Ok(AgentResponse::ActionPromise {
1738 claim: None,
1739 prediction: None,
1740 });
1741 }
1742 }
1743 }
1744 }
1745
1746 async fn complete_action_promise(
1747 &self,
1748 claim: &str,
1749 prediction: &ActionPrediction,
1750 ) -> Result<AgentResponse> {
1751 prediction.validate()?;
1752 let Some(remote) = &self.remote else {
1753 return Ok(AgentResponse::ActionPromiseCompleted);
1754 };
1755 let Some(uploads) = &self.uploads else {
1756 return Ok(AgentResponse::ActionPromiseCompleted);
1757 };
1758 if uploads
1759 .wait_for_actions(std::slice::from_ref(&prediction.action))
1760 .await
1761 .contains(&prediction.action)
1762 {
1763 return Ok(AgentResponse::ActionPromiseCompleted);
1766 }
1767 let completion = ActionPromiseCompletion {
1768 claim: claim.to_string(),
1769 prediction: prediction.clone(),
1770 };
1771 let _permit = self.remote_transfers.acquire().await?;
1772 if let Err(error) = remote
1773 .complete_action_promise(&prediction.invocation, &completion)
1774 .await
1775 {
1776 self.note_remote_failure();
1777 warn!(
1778 "remote cache action promise completion failed for {}: {error}",
1779 prediction.invocation.hash
1780 );
1781 }
1782 Ok(AgentResponse::ActionPromiseCompleted)
1783 }
1784
1785 fn note_remote_failure(&self) {
1787 self.stats.remote_failures.fetch_add(1, Ordering::Relaxed);
1788 }
1789
1790 async fn get_remote_action_result(
1791 &self,
1792 remote: &RemoteCacheClient,
1793 action: &CacheDigest,
1794 ) -> Result<Option<RemoteActionResult>> {
1795 self.stats
1796 .remote_action_lookups
1797 .fetch_add(1, Ordering::Relaxed);
1798 let _timer = AtomicDurationTimer::start(&self.stats.remote_action_lookup_duration_ns);
1799 remote.get_action_result(action).await
1800 }
1801
1802 fn record_action_hit(
1803 &self,
1804 action: &CacheDigest,
1805 restore: RestoreStats,
1806 crate_name: Option<String>,
1807 diagnostic: Option<AgentEvent>,
1808 ) -> Result<AgentResponse> {
1809 validate_crate_name(crate_name.as_deref())?;
1810 if self.actions.find(action)?.is_none() {
1811 let pending = self.pending_remote_actions.lock().unwrap().remove(action);
1812 if let Some(result) = pending {
1813 self.actions.store(&result)?;
1814 } else {
1815 bail!("cannot record a hit for a missing action result");
1816 }
1817 }
1818 self.record_restore(restore);
1819 self.stats.hits.fetch_add(1, Ordering::Relaxed);
1820 self.emit_action(
1821 diagnostic,
1822 AgentEvent::ActionHit {
1823 crate_name,
1824 restore,
1825 },
1826 );
1827 Ok(AgentResponse::ActionHitRecorded)
1828 }
1829
1830 fn record_restore(&self, restore: RestoreStats) {
1831 self.record_materialization(restore);
1832 atomic_saturating_add(
1833 &self.stats.avoided_compiler_duration_ns,
1834 restore.avoided_compiler_duration_ns,
1835 );
1836 atomic_saturating_add(&self.stats.restored_output_files, restore.output_files);
1837 atomic_saturating_add(&self.stats.restored_output_bytes, restore.output_bytes);
1838 atomic_saturating_add(
1839 &self.stats.reflinked_output_files,
1840 restore.reflinked_output_files,
1841 );
1842 atomic_saturating_add(
1843 &self.stats.reflinked_output_bytes,
1844 restore.reflinked_output_bytes,
1845 );
1846 atomic_saturating_add(&self.stats.copied_output_files, restore.copied_output_files);
1847 atomic_saturating_add(&self.stats.copied_output_bytes, restore.copied_output_bytes);
1848 atomic_saturating_add(&self.stats.reused_output_files, restore.reused_output_files);
1849 atomic_saturating_add(&self.stats.reused_output_bytes, restore.reused_output_bytes);
1850 }
1851
1852 fn record_compiler_invocation(
1853 &self,
1854 outcome: &str,
1855 crate_name: Option<&str>,
1856 duration_ns: u64,
1857 diagnostic: Option<AgentEvent>,
1858 ) -> Result<AgentResponse> {
1859 if !matches!(
1860 outcome,
1861 "miss" | "unconsulted" | "bypass" | "verification" | "incremental"
1862 ) {
1863 bail!("invalid compiler invocation outcome");
1864 }
1865 validate_crate_name(crate_name)?;
1866 let mut compiler = self.stats.compiler.lock().unwrap();
1867 let stats = compiler.entry(outcome.to_string()).or_default();
1868 stats.invocations = stats.invocations.saturating_add(1);
1869 stats.duration_ns = stats.duration_ns.saturating_add(duration_ns);
1870 drop(compiler);
1871 if outcome != "verification"
1872 && let Some(crate_name) = crate_name.filter(|name| !name.is_empty())
1873 {
1874 let mut slow = self.stats.slow_compilations.lock().unwrap();
1875 let duration = slow.entry(crate_name.to_string()).or_default();
1876 *duration = duration.saturating_add(duration_ns);
1877 }
1878 self.emit_action(
1879 diagnostic,
1880 AgentEvent::CompilerInvocation {
1881 outcome: outcome.to_string(),
1882 crate_name: crate_name.map(str::to_string),
1883 duration_ns,
1884 },
1885 );
1886 Ok(AgentResponse::CompilerInvocationRecorded)
1887 }
1888
1889 fn record_materialization(&self, restore: RestoreStats) {
1890 atomic_saturating_add(&self.stats.materialization_duration_ns, restore.duration_ns);
1891 }
1892
1893 fn record_warning(&self, message: String) -> Result<AgentResponse> {
1902 if message.is_empty()
1903 || message.len() > MAX_WARNING_BYTES
1904 || message.contains(['\n', '\r', '\0'])
1905 {
1906 bail!("invalid shim warning");
1907 }
1908 let mut warnings = self.warnings.lock().unwrap();
1909 if !warnings.contains(&message) && warnings.len() < MAX_WARNINGS {
1910 eprintln!("mbx[warning]: {message}");
1911 self.emit(|| AgentEvent::Warning {
1912 message: message.clone(),
1913 });
1914 warnings.insert(message);
1915 }
1916 Ok(AgentResponse::WarningRecorded)
1917 }
1918
1919 fn find_file_digests(
1926 &self,
1927 scope: FileDigestScope,
1928 files: Vec<FileIdentity>,
1929 ) -> Result<AgentResponse> {
1930 if files.len() > MAX_FILE_DIGEST_BATCH {
1931 bail!("too many file-digest lookups in one request");
1932 }
1933 let ledger = self.file_digests.lock().unwrap();
1934 let digests = files
1935 .into_iter()
1936 .map(|file| {
1937 let recorded = ledger.get(&(scope, file.path.clone()))?;
1938 (recorded.file == file).then(|| recorded.digest.clone())
1939 })
1940 .collect();
1941 Ok(AgentResponse::FileDigests { digests })
1942 }
1943
1944 fn record_file_digests(
1950 &self,
1951 scope: FileDigestScope,
1952 entries: Vec<RecordedFileDigest>,
1953 ) -> Result<AgentResponse> {
1954 if entries.len() > MAX_FILE_DIGEST_BATCH {
1955 bail!("too many file-digest records in one request");
1956 }
1957 for entry in &entries {
1958 validate_file_digest_record(entry)?;
1959 }
1960 let mut ledger = self.file_digests.lock().unwrap();
1961 for entry in entries {
1962 if ledger.len() >= MAX_FILE_DIGEST_ENTRIES
1963 && !ledger.contains_key(&(scope, entry.file.path.clone()))
1964 {
1965 break;
1966 }
1967 ledger.insert((scope, entry.file.path.clone()), entry);
1968 }
1969 Ok(AgentResponse::FileDigestsRecorded)
1970 }
1971
1972 pub fn seed_file_digests(&self, entries: Vec<(FileDigestScope, RecordedFileDigest)>) -> usize {
1981 self.seed_file_digests_with(|| entries)
1982 }
1983
1984 pub fn seed_file_digests_with(
1990 &self,
1991 load: impl FnOnce() -> Vec<(FileDigestScope, RecordedFileDigest)>,
1992 ) -> usize {
1993 let mut ledger = self.file_digests.lock().unwrap();
1994 let entries = load();
1995 let mut seeded = 0;
1996 for (scope, entry) in entries {
1997 if ledger.len() >= MAX_FILE_DIGEST_ENTRIES {
1998 break;
1999 }
2000 if validate_file_digest_record(&entry).is_err() {
2001 continue;
2002 }
2003 if let std::collections::btree_map::Entry::Vacant(slot) =
2004 ledger.entry((scope, entry.file.path.clone()))
2005 {
2006 slot.insert(entry);
2007 seeded += 1;
2008 }
2009 }
2010 seeded
2011 }
2012
2013 pub fn file_digests(&self) -> Vec<(FileDigestScope, RecordedFileDigest)> {
2016 self.file_digests
2017 .lock()
2018 .unwrap()
2019 .iter()
2020 .map(|((scope, _), entry)| (*scope, entry.clone()))
2021 .collect()
2022 }
2023
2024 fn find_action_prediction(
2025 &self,
2026 task: &str,
2027 invocation: &CacheDigest,
2028 ) -> Result<AgentResponse> {
2029 validate_task_identity(task)?;
2030 invocation.validate()?;
2031 let (prediction, prefetch) = {
2032 let mut tasks = self.task_actions.lock().unwrap();
2033 let Some(state) = tasks.get_mut(task) else {
2034 return Ok(AgentResponse::ActionPrediction { prediction: None });
2035 };
2036 activate_prediction_adapter(state, invocation)
2037 };
2038 if let Some(prefetch) = prefetch {
2039 self.spawn_prefetch_predictions(prefetch);
2040 }
2041 Ok(AgentResponse::ActionPrediction { prediction })
2042 }
2043
2044 fn record_action_prediction(
2045 &self,
2046 task: &str,
2047 prediction: ActionPrediction,
2048 ) -> Result<AgentResponse> {
2049 validate_task_identity(task)?;
2050 prediction.validate()?;
2051 let mut tasks = self.task_actions.lock().unwrap();
2052 let state = tasks.entry(task.to_string()).or_default();
2053 if !state.predictions.contains_key(&prediction.invocation)
2054 && state.predictions.len() >= MAX_TASK_ACTION_PREDICTIONS
2055 {
2056 bail!("task action manifest contains too many predictions");
2057 }
2058 state
2059 .predictions
2060 .insert(prediction.invocation.clone(), prediction.clone());
2061 state
2062 .pending_predictions
2063 .insert(prediction.invocation.clone(), prediction);
2064 Ok(AgentResponse::ActionPredictionRecorded)
2065 }
2066
2067 fn executable_identity_key(
2068 &self,
2069 executable: PathBuf,
2070 environment: BTreeMap<String, Option<String>>,
2071 ) -> Result<ExecutableIdentityKey> {
2072 if !environment.keys().all(|name| {
2077 matches!(
2078 name.as_str(),
2079 "MBX_FUSE_LD"
2080 | "RUSTUP_HOME"
2081 | "RUSTUP_TOOLCHAIN"
2082 | "SDKROOT"
2083 | "MACOSX_DEPLOYMENT_TARGET"
2084 | "LIB"
2085 | "UCRTVersion"
2086 | "UniversalCRTSdkDir"
2087 | "VCToolsInstallDir"
2088 | "VCToolsVersion"
2089 | "WindowsSdkDir"
2090 | "WindowsSDKVersion"
2091 )
2092 }) {
2093 bail!("executable identity contains an unsupported environment variable");
2094 }
2095 Ok(ExecutableIdentityKey {
2096 executable,
2097 environment,
2098 })
2099 }
2100
2101 fn find_executable_identity(
2102 &self,
2103 executable: PathBuf,
2104 environment: BTreeMap<String, Option<String>>,
2105 ) -> Result<AgentResponse> {
2106 let key = self.executable_identity_key(executable, environment)?;
2107 let stdout = self
2108 .executable_identities
2109 .lock()
2110 .unwrap()
2111 .get(&key)
2112 .cloned();
2113 Ok(AgentResponse::ExecutableIdentity { stdout })
2114 }
2115
2116 fn store_executable_identity(
2117 &self,
2118 executable: PathBuf,
2119 environment: BTreeMap<String, Option<String>>,
2120 stdout: Vec<u8>,
2121 ) -> Result<AgentResponse> {
2122 if stdout.len() > MAX_EXECUTABLE_IDENTITY_SIZE {
2123 bail!("executable identity exceeds {MAX_EXECUTABLE_IDENTITY_SIZE} bytes");
2124 }
2125 let key = self.executable_identity_key(executable, environment)?;
2126 let mut identities = self.executable_identities.lock().unwrap();
2127 let is_new = !identities.contains_key(&key);
2128 let previous_size = identities.get(&key).map_or(0, Vec::len);
2129 if is_new && identities.len() >= MAX_EXECUTABLE_IDENTITIES {
2130 bail!("executable identity cache contains too many entries");
2131 }
2132 let retained_bytes = identities.values().map(Vec::len).sum::<usize>();
2133 if retained_bytes - previous_size + stdout.len() > MAX_EXECUTABLE_IDENTITY_BYTES {
2134 bail!("executable identity cache contains too many bytes");
2135 }
2136 identities.insert(key, stdout.clone());
2137 Ok(AgentResponse::ExecutableIdentity {
2138 stdout: Some(stdout),
2139 })
2140 }
2141
2142 pub async fn handle_connection<S>(&self, stream: S) -> Result<()>
2144 where
2145 S: AsyncRead + AsyncWrite + Unpin,
2146 {
2147 let (reader, mut writer) = tokio::io::split(stream);
2148 let mut reader = BufReader::new(reader);
2149 let hello = read_request(&mut reader)
2150 .await?
2151 .ok_or_else(|| eyre::eyre!("connection closed before the agent handshake"))?;
2152 let request: AgentRequest = serde_json::from_str(&hello)?;
2153 match request {
2154 AgentRequest::Hello {
2155 protocol,
2156 client_version,
2157 } if protocol == AGENT_PROTOCOL_VERSION && client_version == self.version.as_ref() => {}
2158 AgentRequest::Hello { protocol, .. } if protocol != AGENT_PROTOCOL_VERSION => {
2159 send_response(
2160 &mut writer,
2161 &AgentResponse::Error {
2162 message: format!(
2163 "unsupported agent protocol {protocol}; expected {AGENT_PROTOCOL_VERSION}"
2164 ),
2165 },
2166 )
2167 .await?;
2168 return Ok(());
2169 }
2170 AgentRequest::Hello { client_version, .. } => {
2171 send_response(
2172 &mut writer,
2173 &AgentResponse::Error {
2174 message: format!(
2175 "cache client {client_version} does not match agent {}",
2176 self.version
2177 ),
2178 },
2179 )
2180 .await?;
2181 return Ok(());
2182 }
2183 _ => bail!("the first agent request must be hello"),
2184 }
2185 send_response(
2186 &mut writer,
2187 &AgentResponse::Hello {
2188 protocol: AGENT_PROTOCOL_VERSION,
2189 agent_version: self.version.to_string(),
2190 },
2191 )
2192 .await?;
2193
2194 let mut connection = ConnectionUploads::default();
2198 while let Some(line) = read_request(&mut reader).await? {
2199 let response = match serde_json::from_str(&line) {
2200 Ok(request) => self.respond_on(request, &mut connection).await,
2201 Err(error) => AgentResponse::Error {
2202 message: format!("invalid agent request: {error}"),
2203 },
2204 };
2205 send_response(&mut writer, &response).await?;
2206 }
2207 Ok(())
2208 }
2209}
2210
2211async fn read_request<R>(reader: &mut R) -> Result<Option<String>>
2217where
2218 R: AsyncBufRead + Unpin,
2219{
2220 let mut line = Vec::new();
2221 loop {
2222 let available = reader.fill_buf().await?;
2223 if available.is_empty() {
2224 break;
2225 }
2226 let (consumed, complete) = match available.iter().position(|byte| *byte == b'\n') {
2227 Some(index) => (index, true),
2228 None => (available.len(), false),
2229 };
2230 if line.len() + consumed > MAX_REQUEST_BYTES {
2231 bail!("agent request exceeded {MAX_REQUEST_BYTES} bytes");
2232 }
2233 line.extend_from_slice(&available[..consumed]);
2234 reader.consume(consumed + usize::from(complete));
2236 if complete {
2237 return Ok(Some(String::from_utf8(line)?));
2238 }
2239 }
2240 if line.is_empty() {
2241 Ok(None)
2242 } else {
2243 Ok(Some(String::from_utf8(line)?))
2244 }
2245}
2246
2247async fn send_response(
2248 writer: &mut (impl AsyncWrite + Unpin),
2249 response: &AgentResponse,
2250) -> Result<()> {
2251 let mut encoded = serde_json::to_vec(response)?;
2252 encoded.push(b'\n');
2253 writer.write_all(&encoded).await?;
2254 writer.flush().await?;
2255 Ok(())
2256}
2257
2258#[cfg(test)]
2259#[path = "agent_tests.rs"]
2260mod tests;