Coverage Report

Created: 2026-11-13 23:29

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-worker/src/running_actions_manager.rs
Line
Count
Source
1
// Copyright 2024 Trace Machina, Inc. All rights reserved.
2
//
3
// Licensed under the Functional Source License, Version 1.1, Apache 2.0 Future License (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
//    See LICENSE file for details
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14
15
use core::cmp::min;
16
use core::convert::Into;
17
use core::fmt::Debug;
18
use core::pin::Pin;
19
use core::sync::atomic::{AtomicBool, AtomicU64, Ordering};
20
use core::time::Duration;
21
use std::borrow::Cow;
22
use std::collections::vec_deque::VecDeque;
23
use std::collections::{BTreeMap, HashMap, HashSet};
24
use std::env;
25
use std::ffi::{OsStr, OsString};
26
#[cfg(target_family = "unix")]
27
use std::fs::Permissions;
28
#[cfg(target_family = "unix")]
29
use std::os::unix::fs::{MetadataExt, PermissionsExt};
30
#[cfg(target_family = "unix")]
31
use std::os::unix::process::ExitStatusExt;
32
use std::path::{Path, PathBuf};
33
use std::process::Stdio;
34
use std::sync::{Arc, Weak};
35
use std::time::SystemTime;
36
37
use bytes::{Bytes, BytesMut};
38
use filetime::{FileTime, set_file_mtime};
39
use formatx::Template;
40
use futures::future::{
41
    BoxFuture, Fuse, Future, FutureExt, TryFutureExt, join_all, try_join, try_join_all,
42
};
43
use futures::stream::{FuturesUnordered, StreamExt, TryStreamExt};
44
use nativelink_config::cas_server::{
45
    Buck2FileCaptureConfig, DiskEnforcement, EnvironmentSource, MemoryEnforcement,
46
    ResourceEnforcementConfig, UploadActionResultConfig, UploadCacheResultsStrategy,
47
};
48
use nativelink_error::{Code, Error, ResultExt, make_err, make_input_err};
49
use nativelink_metric::MetricsComponent;
50
use nativelink_proto::build::bazel::remote::execution::v2::{
51
    Action, ActionResult as ProtoActionResult, Command as ProtoCommand,
52
    Directory as ProtoDirectory, Directory, DirectoryNode, ExecuteResponse, FileNode, SymlinkNode,
53
    Tree as ProtoTree, UpdateActionResultRequest,
54
};
55
use nativelink_proto::com::github::trace_machina::nativelink::remote_execution::{
56
    ActionResourceUsage, HistoricalExecuteResponse, Reservation, ResourceOutcome, StartExecute,
57
};
58
use nativelink_store::ac_utils::{
59
    ESTIMATED_DIGEST_SIZE, compute_buf_digest, get_and_decode_digest, serialize_and_upload_message,
60
};
61
use nativelink_store::cas_utils::is_zero_digest;
62
use nativelink_store::fast_slow_store::FastSlowStore;
63
use nativelink_store::filesystem_store::{FileEntry, FileEntryImpl, FilesystemStore};
64
use nativelink_store::grpc_store::GrpcStore;
65
use nativelink_util::action_messages::{
66
    ActionInfo, ActionResult, DirectoryInfo, ExecutionMetadata, FileInfo, NameOrPath, OperationId,
67
    STORE_UNREACHABLE_ERROR_TIP, SymlinkInfo, to_execute_response,
68
};
69
use nativelink_util::common::{DigestInfo, fs};
70
use nativelink_util::digest_hasher::{DigestHasher, DigestHasherFunc};
71
use nativelink_util::metrics::{INPUT_MOUNT_METRICS, record_input_relink, record_prepare_retry};
72
use nativelink_util::metrics_utils::{AsyncCounterWrapper, CounterWithTime};
73
use nativelink_util::store_trait::{Store, StoreLike, UploadSizeInfo};
74
use nativelink_util::task::JoinHandleDropGuard;
75
use nativelink_util::{background_spawn, spawn, spawn_blocking};
76
use parking_lot::Mutex;
77
use prost::Message;
78
use relative_path::RelativePath;
79
use scopeguard::{ScopeGuard, guard};
80
use serde::Deserialize;
81
use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt};
82
use tokio::process;
83
use tokio::sync::{Notify, Semaphore, SemaphorePermit, oneshot, watch};
84
use tokio::time::Instant;
85
use tokio_stream::wrappers::ReadDirStream;
86
use tonic::Request;
87
use tracing::{debug, error, info, trace, warn};
88
use uuid::Uuid;
89
90
use crate::buck2_file_capture::Buck2FileCapture;
91
use crate::input_mounts::InputMounts;
92
use crate::persistent_worker::{
93
    Input as PersistentWorkerInput, PersistentWorkerPool, PoolConfig, WireFormat, WorkRequest,
94
    WorkerKey,
95
};
96
use crate::process_table::{
97
    EXACT_MEMORY_READ_WALKS_PAGES, ProcessTableCache, live_process_table, process_exists,
98
    read_process_cpu_ticks, read_process_memory_bound_kb, read_process_memory_kb, ticks_to_millis,
99
    worker_pgid,
100
};
101
102
/// For simplicity we use a fixed exit code for cases when our program is terminated
103
/// due to a signal.
104
const EXIT_CODE_FOR_SIGNAL: i32 = 9;
105
106
const SUPPORTS_WORKERS_PROPERTY: &str = "supports-workers";
107
const REQUIRES_WORKER_PROTOCOL_PROPERTY: &str = "requires-worker-protocol";
108
109
/// Default strategy for uploading historical results.
110
/// Note: If this value changes the config documentation
111
/// should reflect it.
112
const DEFAULT_HISTORICAL_RESULTS_STRATEGY: UploadCacheResultsStrategy =
113
    UploadCacheResultsStrategy::FailuresOnly;
114
115
const RESOURCE_USAGE_SAMPLE_INTERVAL: Duration = Duration::from_millis(250);
116
117
/// How many output files are open for upload at once, across the whole
118
/// output tree: one semaphore, taken around each file from open to upload,
119
/// so nested directories cannot multiply the bound. An unbounded set has hit
120
/// the descriptor limit on actions with tens of thousands of outputs. The
121
/// same number bounds each stream's fan-out.
122
const UPLOAD_CONCURRENCY: usize = 64;
123
124
/// What the sampler observed over an action's lifetime.
125
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
126
struct SampledResourceUsage {
127
    peak_memory_kb: u64,
128
    cpu_time_ms: u64,
129
    /// The last reading before the group was gone, which is what a kernel
130
    /// kill is judged against.
131
    last_memory_kb: u64,
132
    /// The most the action's own files under its directory came to, when
133
    /// disk was measured; 0 otherwise.
134
    peak_disk_kb: u64,
135
}
136
137
/// Why the worker ended an action before the action ended itself.
138
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139
pub enum KillReason {
140
    Timeout,
141
    Memory,
142
    Disk,
143
    External,
144
}
145
146
const SIGKILL_NUMBER: i32 = 9;
147
148
pub use nativelink_util::action_messages::MISSING_INPUT_ERROR_TIP;
149
150
5
fn tag_missing_input(err: Error) -> Error {
151
5
    if err.code == Code::NotFound {
152
1
        err.append(MISSING_INPUT_ERROR_TIP)
153
    } else {
154
4
        err
155
    }
156
5
}
157
158
/// How long a killed action's output pipes, or those of one that left
159
/// stragglers behind, get to drain. They close when every process holding
160
/// them is gone, which the SIGKILL to the group sees to; a process that
161
/// left the group could still hold them, so that wait is bounded, and what
162
/// was read by then is kept. An action that exited cleanly with nothing
163
/// left behind is read to the end.
164
const OUTPUT_DRAIN_TIMEOUT: Duration = Duration::from_secs(5);
165
166
/// Classifies how an action ended. A kill the worker sent names itself; a
167
/// SIGKILL nobody here sent, with the last memory sample within 10% of the
168
/// limit, is the kernel's OOM killer; any other signal is external.
169
56
pub fn classify_outcome(
170
56
    kill_reason: Option<KillReason>,
171
56
    signal: Option<i32>,
172
56
    last_memory_kb: u64,
173
56
    memory_limit_kb: Option<u64>,
174
56
) -> (ResourceOutcome, bool) {
175
56
    match (kill_reason, signal) {
176
4
        (Some(KillReason::Memory), _) => (ResourceOutcome::KilledMemory, true),
177
1
        (Some(KillReason::Disk), _) => (ResourceOutcome::KilledDisk, true),
178
6
        (Some(KillReason::Timeout), _) => (ResourceOutcome::KilledTimeout, false),
179
37
        (None, None) => (ResourceOutcome::Completed, false),
180
        (None, Some(SIGKILL_NUMBER)) => {
181
3
            let near_limit = memory_limit_kb.is_some_and(|limit_kb| 
{2
182
2
                limit_kb > 0 && last_memory_kb.saturating_mul(10) >= limit_kb.saturating_mul(9)
183
2
            });
184
3
            if near_limit {
185
1
                (ResourceOutcome::KilledMemory, false)
186
            } else {
187
2
                (ResourceOutcome::KilledExternal, false)
188
            }
189
        }
190
        (Some(KillReason::External), _) | (None, Some(_)) => {
191
5
            (ResourceOutcome::KilledExternal, false)
192
        }
193
    }
194
56
}
195
196
#[cfg(target_os = "linux")]
197
type SpawnedChild = crate::namespace_utils::MaybeNamespacedChild;
198
#[cfg(not(target_os = "linux"))]
199
type SpawnedChild = process::Child;
200
201
/// SIGKILL to every process group holding the action's processes, then
202
/// through the child wrapper, which takes the action's own group with it;
203
/// logged if it fails.
204
11
async fn hard_kill(child: &mut SpawnedChild, watch: &ActionMemoryWatch, why: &str) {
205
    #[cfg(target_family = "unix")]
206
11
    if let Some(pgid) = child.id() {
207
11
        signal_action(pgid, watch, libc::SIGKILL);
208
11
    
}0
209
    #[cfg(not(target_family = "unix"))]
210
    let _ = watch;
211
11
    if let Err(
err0
) = child.kill().await {
212
0
        error!(?err, why, "Could not kill process in RunningActionsManager");
213
11
    }
214
11
}
215
216
/// Starts stopping an action. With a grace period the process group gets
217
/// SIGTERM and the returned future fires when SIGKILL is due; without one
218
/// SIGKILL goes out now and the future never fires.
219
0
async fn stop_action(
220
0
    child: &mut SpawnedChild,
221
0
    watch: &ActionMemoryWatch,
222
0
    grace: Duration,
223
0
    why: &str,
224
8
) -> Fuse<BoxFuture<'static, ()>> {
225
8
    debug!(grace_ms = grace.as_millis(), why, "Stopping action");
226
    #[cfg(target_os = "linux")]
227
8
    if !grace.is_zero()
228
1
        && let Some(pgid) = child.id()
229
    {
230
1
        signal_action(pgid, watch, libc::SIGTERM);
231
1
        return tokio::time::sleep(grace).boxed().fuse();
232
7
    }
233
7
    hard_kill(child, watch, why).await;
234
7
    Fuse::terminated()
235
8
}
236
237
/// The reservation the action was admitted with, read back from its
238
/// platform properties so the measurement travels with its yardstick.
239
48
fn reservation_of(
240
48
    properties: &HashMap<String, String>,
241
48
    memory_property: Option<&str>,
242
48
    disk_property: Option<&str>,
243
48
) -> Option<Reservation> {
244
144
    let 
read48
= |name: &str| {
245
144
        properties
246
144
            .get(name)
247
144
            .and_then(|v| 
v.trim()6
.
parse6
::<u64>().
ok6
())
248
144
    };
249
48
    let reservation = Reservation {
250
48
        cpu_count: read("cpu_count").unwrap_or(0),
251
48
        memory_kb: read(memory_property.unwrap_or("memory_kb")).unwrap_or(0),
252
48
        disk_kb: read(disk_property.unwrap_or("disk_kb")).unwrap_or(0),
253
48
    };
254
48
    (reservation.cpu_count > 0 || reservation.memory_kb > 0 || 
reservation.disk_kb > 043
)
255
48
        .then_some(reservation)
256
48
}
257
258
#[cfg(target_os = "linux")]
259
82
fn pod_memory_limit_kb() -> Option<u64> {
260
82
    crate::capacity::observe_cgroup().map(|observed| observed.memory_kb)
261
82
}
262
#[cfg(not(target_os = "linux"))]
263
const fn pod_memory_limit_kb() -> Option<u64> {
264
    None
265
}
266
267
struct ActionResourceUsageSampler {
268
    stop_tx: watch::Sender<bool>,
269
    handle: tokio::task::JoinHandle<SampledResourceUsage>,
270
}
271
272
/// What the sampler found over a ceiling, with the figure it saw.
273
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
274
enum OverLimit {
275
    Memory(u64),
276
    Disk(u64),
277
    /// The worker's cgroup stayed under memory pressure and this action
278
    /// held the most memory of those running; the figures are its last
279
    /// sample and the pressure that chose it.
280
    MemoryPressure {
281
        observed_kb: u64,
282
        full_avg10_percent: u64,
283
    },
284
}
285
286
/// What one running action publishes for the worker-wide memory pressure
287
/// monitor, and the kill it can ask of the action's sampler: the sampler
288
/// writes `last_memory_kb` every sample and 0 once it stops; the monitor
289
/// sets `kill_requested_percent` (the pressure it saw, never 0) and the
290
/// sampler ends the action on its next pass through the same channel a
291
/// ceiling breach uses.
292
#[derive(Debug, Default)]
293
pub struct ActionMemoryWatch {
294
    last_memory_kb: AtomicU64,
295
    /// The highest figure the sampler has read for the action; it only
296
    /// rises. Reported on the keepalive so a scheduler that loses this
297
    /// worker can tell which action grew past its reservation.
298
    peak_memory_kb: AtomicU64,
299
    /// The memory reservation the action runs under, 0 when it has none.
300
    reserved_memory_kb: AtomicU64,
301
    kill_requested_percent: AtomicU64,
302
    /// Every process group the sampler last found holding the action's
303
    /// processes. A test that `set -m` put in a group of its own keeps
304
    /// nothing linking it to the action once its wrapper exits; this is
305
    /// how the sampler still counts it and a kill still reaches it. Only
306
    /// the Linux sampler fills it.
307
    process_groups: Mutex<Vec<u32>>,
308
}
309
310
impl ActionMemoryWatch {
311
8
    pub fn last_memory_kb(&self) -> u64 {
312
8
        self.last_memory_kb.load(Ordering::Relaxed)
313
8
    }
314
315
6
    pub fn peak_memory_kb(&self) -> u64 {
316
6
        self.peak_memory_kb.load(Ordering::Relaxed)
317
6
    }
318
319
    /// Records a sample: the last figure, and the peak if it is higher.
320
101
    pub fn observe(&self, memory_kb: u64) {
321
101
        self.last_memory_kb.store(memory_kb, Ordering::Relaxed);
322
101
        self.peak_memory_kb.fetch_max(memory_kb, Ordering::Relaxed);
323
101
    }
324
325
0
    pub fn reserved_memory_kb(&self) -> u64 {
326
0
        self.reserved_memory_kb.load(Ordering::Relaxed)
327
0
    }
328
329
0
    pub fn kill_requested(&self) -> bool {
330
0
        self.kill_requested_percent.load(Ordering::Relaxed) > 0
331
0
    }
332
}
333
334
/// One running action as the memory pressure kill sees it.
335
#[derive(Debug, Clone, Copy)]
336
pub struct PressureCandidate<T> {
337
    pub item: T,
338
    /// The sampler's last figure, 0 before the first sample and after the
339
    /// sampler stopped.
340
    pub last_kb: u64,
341
    /// Its memory reservation, 0 when it has none.
342
    pub reserved_kb: u64,
343
    pub kill_requested: bool,
344
}
345
346
/// Picks the action a memory pressure kill lands on, among those with a
347
/// sample and no kill already asked: the one furthest over its
348
/// reservation, or, when none is over, the one whose last sample is
349
/// largest. An action that reserves `whole_worker_kb` or more is never
350
/// picked: it already has the worker to itself, and a memory kill there
351
/// would leave the scheduler nothing larger to requeue it on. `None` when
352
/// nothing qualifies, so a worker under pressure from something other than
353
/// its actions kills nothing.
354
8
pub fn pick_pressure_victim<T>(
355
8
    candidates: impl IntoIterator<Item = PressureCandidate<T>>,
356
8
    whole_worker_kb: Option<u64>,
357
8
) -> Option<T> {
358
8
    let eligible: Vec<PressureCandidate<T>> = candidates
359
8
        .into_iter()
360
12
        .
filter8
(|c| c.last_kb > 0 &&
!c.kill_requested10
)
361
9
        .
filter8
(|c| {
362
9
            !whole_worker_kb.is_some_and(|whole_kb| 
whole_kb > 05
&&
c.reserved_kb >= whole_kb5
)
363
9
        })
364
8
        .collect();
365
8
    let furthest_over = eligible
366
8
        .iter()
367
8
        .enumerate()
368
8
        .filter(|(_, c)| 
c.last_kb6
>
c.reserved_kb6
)
369
8
        .max_by_key(|(_, c)| 
c.last_kb3
-
c.reserved_kb3
)
370
8
        .map(|(index, _)| index);
371
8
    let 
index4
= furthest_over.or_else(||
{5
372
5
        eligible
373
5
            .iter()
374
5
            .enumerate()
375
5
            .max_by_key(|(_, c)| c.last_kb)
376
5
            .map(|(index, _)| index)
377
5
    })
?4
;
378
4
    eligible.into_iter().nth(index).map(|c| c.item)
379
8
}
380
381
/// Decides when sustained memory pressure becomes a kill: `observe` is
382
/// handed each `memory.pressure` `full avg10` reading and answers true
383
/// once the reading has stayed at or above the threshold for `after`.
384
/// A reading under the threshold starts over, and so does a kill, so the
385
/// next one waits a whole window again while the freed memory takes
386
/// effect.
387
#[derive(Debug, Clone, Copy)]
388
pub struct PressureWindow {
389
    threshold_percent: f64,
390
    after: Duration,
391
    over_since: Option<Instant>,
392
}
393
394
impl PressureWindow {
395
1
    pub const fn new(threshold_percent: u64, after: Duration) -> Self {
396
1
        Self {
397
1
            threshold_percent: threshold_percent as f64,
398
1
            after,
399
1
            over_since: None,
400
1
        }
401
1
    }
402
403
9
    pub fn observe(&mut self, full_avg10_percent: f64, now: Instant) -> bool {
404
9
        if full_avg10_percent < self.threshold_percent {
405
1
            self.over_since = None;
406
1
            return false;
407
8
        }
408
8
        let since = *self.over_since.get_or_insert(now);
409
8
        if now.saturating_duration_since(since) >= self.after {
410
2
            self.over_since = None;
411
2
            return true;
412
6
        }
413
6
        false
414
9
    }
415
}
416
417
/// How often the worker reads its cgroup's `memory.pressure`.
418
#[cfg(target_os = "linux")]
419
const MEMORY_PRESSURE_SAMPLE_INTERVAL: Duration = Duration::from_secs(2);
420
421
/// A PSI percentage as a whole number for the kill's message: clamped to
422
/// 0 to 100 first, so the cast cannot truncate or lose a sign.
423
#[cfg(target_os = "linux")]
424
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
425
0
const fn whole_percent(value: f64) -> u64 {
426
0
    value.clamp(0.0, 100.0).round() as u64
427
0
}
428
429
/// The ceilings the sampler enforces. Memory: two consecutive samples over
430
/// `memory_limit_kb`, or one at twice it. Disk: one sample of the action's
431
/// own files over `disk_limit_kb`. The first breach is sent on
432
/// `over_limit_tx` and the caller kills the action; nothing is sent when no
433
/// ceiling is set, and the caller keeps the sender alive so the receiver
434
/// stays pending.
435
struct Ceilings {
436
    memory_limit_kb: Option<u64>,
437
    disk_limit_kb: Option<u64>,
438
    over_limit_tx: Option<oneshot::Sender<OverLimit>>,
439
    /// Where each sample is published and where a pressure kill arrives.
440
    watch: Arc<ActionMemoryWatch>,
441
}
442
443
/// One disk sample every this many memory samples: a walk of the action
444
/// directory is a stat per file, so it runs every ten seconds, not four
445
/// times a second.
446
const DISK_SAMPLE_EVERY: u32 = 40;
447
448
/// `oom_limit_kb` is what a SIGKILL nobody here sent is judged against (see
449
/// [`classify_outcome`]): the enforced ceiling, or the pod's limit without
450
/// one. It is not enforced; the sampler keeps its figure exact near it.
451
48
fn start_action_resource_usage_sampler(
452
48
    pgid: u32,
453
48
    ceilings: Ceilings,
454
48
    oom_limit_kb: Option<u64>,
455
48
    disk_directory: Option<(PathBuf, SystemTime)>,
456
48
    process_table: Arc<ProcessTableCache>,
457
48
) -> ActionResourceUsageSampler {
458
48
    let (stop_tx, stop_rx) = watch::channel(false);
459
48
    let handle = background_spawn!(
460
        "action_resource_usage_sampler",
461
48
        sample_action_resource_usage(
462
48
            pgid,
463
48
            stop_rx,
464
48
            ceilings,
465
48
            oom_limit_kb,
466
48
            disk_directory,
467
48
            process_table
468
        )
469
    );
470
48
    ActionResourceUsageSampler { stop_tx, handle }
471
48
}
472
473
/// KiB on disk of the files under `directory` that the action wrote: regular
474
/// files modified at or after `since`, the directory's stamp from just
475
/// before its command started (see [`directory_stamp`]). Its
476
/// inputs were materialized before that, as hard links into the CAS store
477
/// or private copies, and keep their earlier times, so they are told apart
478
/// by time rather than by link count: a hard link drops to one link the
479
/// moment the store evicts the blob, and would then pass for the action's
480
/// own. Symlinks and the directories' own blocks are not counted. Blocking;
481
/// run it on the blocking pool.
482
#[cfg(target_family = "unix")]
483
5
pub fn directory_private_kb(directory: &Path, since: SystemTime) -> u64 {
484
5
    let mut total_blocks: u64 = 0;
485
5
    let mut pending = vec![directory.to_path_buf()];
486
15
    while let Some(
dir10
) = pending.pop() {
487
10
        let Ok(entries) = std::fs::read_dir(&dir) else {
488
0
            continue;
489
        };
490
13
        for entry in 
entries10
.
flatten10
() {
491
13
            let Ok(metadata) = entry.metadata() else {
492
0
                continue;
493
            };
494
13
            let file_type = metadata.file_type();
495
13
            if file_type.is_dir() {
496
5
                pending.push(entry.path());
497
8
            } else if file_type.is_file()
498
7
                && metadata.modified().is_ok_and(|modified| modified >= since)
499
4
            {
500
4
                total_blocks = total_blocks.saturating_add(metadata.blocks());
501
4
            }
502
        }
503
    }
504
    // st_blocks are 512-byte units whatever the filesystem's block size.
505
5
    total_blocks / 2
506
5
}
507
508
#[cfg(not(target_family = "unix"))]
509
pub fn directory_private_kb(_directory: &Path, _since: SystemTime) -> u64 {
510
    0
511
}
512
513
/// Touches `directory` and returns the time the filesystem recorded for it.
514
/// File mtimes come from the kernel's coarse clock, which on Linux can lag
515
/// the wall clock by a tick, so a file written right after
516
/// `SystemTime::now()` can look older than that moment. The walk compares
517
/// mtimes against this stamp, taken from the same clock, so a file the
518
/// action writes afterwards is never older than it.
519
#[cfg(target_family = "unix")]
520
3
pub fn directory_stamp(directory: &Path) -> Result<SystemTime, Error> {
521
    use std::os::unix::io::AsRawFd;
522
3
    let dir = std::fs::File::open(directory)
523
3
        .err_tip(|| 
format!0
("Opening {} to stamp it",
directory0
.
display0
()))
?0
;
524
    // SAFETY: a zeroed timespec is valid; only tv_nsec is set.
525
3
    let mut now: libc::timespec = unsafe { core::mem::zeroed() };
526
3
    now.tv_nsec = libc::UTIME_NOW;
527
3
    let times = [now, now];
528
    // SAFETY: futimens takes an open fd and a pointer to two timespecs that
529
    // outlive the call.
530
3
    if unsafe { libc::futimens(dir.as_raw_fd(), times.as_ptr()) } != 0 {
531
0
        return Err(std::io::Error::last_os_error())
532
0
            .err_tip(|| format!("Stamping {}", directory.display()));
533
3
    }
534
3
    dir.metadata()
535
3
        .and_then(|metadata| metadata.modified())
536
3
        .err_tip(|| 
format!0
("Reading the stamp on {}",
directory0
.
display0
()))
537
3
}
538
539
#[cfg(not(target_family = "unix"))]
540
pub fn directory_stamp(_directory: &Path) -> Result<SystemTime, Error> {
541
    Ok(SystemTime::now())
542
}
543
544
/// Starts a disk sample: the action's own files, walked on the blocking
545
/// pool as a task of its own, so the memory samples carry on while a large
546
/// tree is being walked. `None` when there is no directory to measure.
547
50
fn start_disk_sample(
548
50
    directory: Option<&(PathBuf, SystemTime)>,
549
50
) -> Option<JoinHandleDropGuard<u64>> {
550
50
    let (
directory4
,
since4
) = directory
?46
.
clone4
();
551
4
    Some(spawn_blocking!("action_disk_sample", move || {
552
4
        directory_private_kb(&directory, since)
553
4
    }))
554
50
}
555
556
/// A signal to every process in the action's group. The action is its own
557
/// group leader, so this reaches the children a shell forked. The SIGKILL
558
/// at the end of a kill goes through the child wrapper, which takes the
559
/// group with it (or tells the stub, namespaced); this is for the SIGTERM
560
/// that gives an action its grace, and for the stragglers left once the
561
/// action's own process has exited. Returns whether any process got it.
562
#[cfg(target_family = "unix")]
563
59
fn signal_process_group(pgid: u32, signal: i32) -> bool {
564
59
    let Ok(pgid) = i32::try_from(pgid) else {
565
0
        return false;
566
    };
567
    // SAFETY: killpg only takes integers and has no memory safety
568
    // considerations; a stale group id is reported as ESRCH, not acted on.
569
59
    unsafe { libc::killpg(pgid, signal) == 0 }
570
59
}
571
572
/// A signal to every process group holding the action's processes: its
573
/// own group, the groups the sampler last found, and whatever those reach
574
/// now (see [`action_membership`]). The action's own group alone misses a
575
/// test that `set -m` moved to a group of its own, which would survive the
576
/// kill and keep its memory. Returns whether any process got it.
577
#[cfg(target_family = "unix")]
578
58
fn signal_action(action_pgid: u32, watch: &ActionMemoryWatch, signal: i32) -> bool {
579
58
    let mut seeds = watch.process_groups.lock().clone();
580
58
    seeds.push(action_pgid);
581
58
    let mut groups = action_membership_live(&seeds).groups;
582
58
    groups.insert(action_pgid);
583
58
    let mut delivered = false;
584
59
    for pgid in 
groups58
{
585
59
        delivered |= signal_process_group(pgid, signal);
586
59
    }
587
58
    delivered
588
58
}
589
590
48
async fn finish_action_resource_usage_sampler(
591
48
    sampler: ActionResourceUsageSampler,
592
48
) -> Option<SampledResourceUsage> {
593
48
    let _ = sampler.stop_tx.send(true);
594
48
    sampler.handle.await.ok()
595
48
}
596
597
48
async fn sample_action_resource_usage(
598
48
    pgid: u32,
599
48
    mut stop_rx: watch::Receiver<bool>,
600
48
    mut ceilings: Ceilings,
601
48
    oom_limit_kb: Option<u64>,
602
48
    disk_directory: Option<(PathBuf, SystemTime)>,
603
48
    process_table: Arc<ProcessTableCache>,
604
48
) -> SampledResourceUsage {
605
48
    let mut peak_memory_kb = 0;
606
48
    let mut last_memory_kb = 0;
607
48
    let mut peak_disk_kb = 0;
608
48
    let mut samples_over_limit = 0u32;
609
48
    let mut samples_since_disk = DISK_SAMPLE_EVERY;
610
    // The walk in flight, if one is; polled, never awaited, from the loop.
611
48
    let mut disk_sample: Option<JoinHandleDropGuard<u64>> = None;
612
    // CPU time is cumulative per process and a process that exits stops
613
    // appearing, so summing the live group at the end would lose everything
614
    // short-lived. Keep the last figure seen for each pid and total them at
615
    // the end instead.
616
48
    let mut cpu_ticks_by_pid = HashMap::new();
617
    // What the group had already spent when sampling began. A one-shot
618
    // process starts at nothing; a pooled process carries every request it
619
    // served before, which is not this action's.
620
48
    let mut baseline_ticks: Option<u64> = None;
621
622
48
    let mut memory_gate = ExactMemoryGate::new(oom_limit_kb);
623
624
48
    let watch = ceilings.watch.clone();
625
48
    let sample = async |peak_memory_kb: &mut u64,
626
                        cpu_ticks_by_pid: &mut HashMap<u32, u64>,
627
                        memory_gate: &mut ExactMemoryGate,
628
316
                        exact: bool| {
629
158
        let table = process_table.current().await;
630
158
        let observed =
631
158
            sample_process_group(pgid, &table, &watch, cpu_ticks_by_pid, memory_gate, exact);
632
158
        if let Some(
memory_kb98
) = observed {
633
98
            *peak_memory_kb = (*peak_memory_kb).max(memory_kb);
634
98
        
}60
635
158
        observed
636
316
    };
637
638
    loop {
639
110
        let observed = sample(
640
110
            &mut peak_memory_kb,
641
110
            &mut cpu_ticks_by_pid,
642
110
            &mut memory_gate,
643
110
            false,
644
110
        )
645
110
        .await;
646
110
        if baseline_ticks.is_none() {
647
48
            baseline_ticks = Some(cpu_ticks_by_pid.values().sum());
648
62
        }
649
110
        if let Some(
memory_kb96
) = observed {
650
96
            last_memory_kb = memory_kb;
651
96
            ceilings.watch.observe(memory_kb);
652
96
        
}14
653
110
        if observed.is_none() && 
!process_exists(pgid)14
{
654
            // The group leader has been reaped and no member process remains,
655
            // so the action is finished.
656
0
            break;
657
110
        }
658
        // Two consecutive samples over the ceiling, so one spike between
659
        // reads is not a kill; one sample at twice the ceiling is, because
660
        // an allocation that fast reaches the pod's limit before the next
661
        // read (observed: twelve 20 GiB allocators filled a 52 GiB pod in
662
        // under a second).
663
110
        if let (Some(
observed_kb10
), Some(
limit_kb10
)) = (observed, ceilings.memory_limit_kb) {
664
10
            if observed_kb > limit_kb {
665
2
                samples_over_limit += 1;
666
8
            } else {
667
8
                samples_over_limit = 0;
668
8
            }
669
10
            let gross = observed_kb >= limit_kb.saturating_mul(2);
670
10
            if (samples_over_limit >= 2 || 
gross9
)
671
1
                && let Some(over_limit_tx) = ceilings.over_limit_tx.take()
672
1
                && over_limit_tx.send(OverLimit::Memory(observed_kb)).is_err()
673
            {
674
                // The receiver is gone only when the action already ended.
675
0
                debug!(observed_kb, "Memory ceiling breached, action already ended");
676
10
            }
677
100
        }
678
        // The worker-wide monitor chose this action under sustained memory
679
        // pressure; the kill goes the way a ceiling breach does.
680
110
        let full_avg10_percent = ceilings
681
110
            .watch
682
110
            .kill_requested_percent
683
110
            .load(Ordering::Relaxed);
684
110
        if full_avg10_percent > 0
685
2
            && let Some(over_limit_tx) = ceilings.over_limit_tx.take()
686
2
            && over_limit_tx
687
2
                .send(OverLimit::MemoryPressure {
688
2
                    observed_kb: last_memory_kb,
689
2
                    full_avg10_percent,
690
2
                })
691
2
                .is_err()
692
        {
693
0
            debug!(
694
                last_memory_kb,
695
                "Memory pressure kill asked, action already ended"
696
            );
697
110
        }
698
699
        // A walk that finished since the last sample; the memory samples
700
        // above never waited for it.
701
110
        if let Some(
handle2
) = disk_sample.as_mut()
702
2
            && let core::task::Poll::Ready(finished) = futures::poll!(handle)
703
        {
704
2
            disk_sample = None;
705
2
            if let Ok(disk_kb) = finished {
706
2
                peak_disk_kb = peak_disk_kb.max(disk_kb);
707
2
                if let Some(limit_kb) = ceilings.disk_limit_kb
708
2
                    && disk_kb > limit_kb
709
1
                    && let Some(over_limit_tx) = ceilings.over_limit_tx.take()
710
1
                    && over_limit_tx.send(OverLimit::Disk(disk_kb)).is_err()
711
                {
712
0
                    debug!(disk_kb, "Disk ceiling breached, action already ended");
713
2
                }
714
0
            }
715
108
        }
716
        // The periodic walk belongs to the soft limit; the guard alone
717
        // measures once, at the end, and pays no walk while running.
718
110
        samples_since_disk += 1;
719
110
        if ceilings.disk_limit_kb.is_some()
720
42
            && samples_since_disk >= DISK_SAMPLE_EVERY
721
2
            && disk_sample.is_none()
722
2
        {
723
2
            samples_since_disk = 0;
724
2
            disk_sample = start_disk_sample(disk_directory.as_ref());
725
108
        }
726
727
110
        if *stop_rx.borrow() {
728
0
            break;
729
110
        }
730
731
110
        tokio::select! {
732
110
            
changed48
= stop_rx.changed() => {
733
48
                if changed.is_ok() && *stop_rx.borrow() {
734
48
                    break;
735
0
                }
736
            }
737
110
            () = tokio::time::sleep(RESOURCE_USAGE_SAMPLE_INTERVAL) => 
{}62
738
        }
739
    }
740
    // The final figure settles the peak and how the action ended, so every
741
    // way out takes it exact. A pooled process is still there to read; a
742
    // finished group has nothing left and changes nothing.
743
48
    if let Some(
memory_kb2
) = sample(
744
48
        &mut peak_memory_kb,
745
48
        &mut cpu_ticks_by_pid,
746
48
        &mut memory_gate,
747
48
        true,
748
48
    )
749
48
    .await
750
2
    {
751
2
        last_memory_kb = memory_kb;
752
46
    }
753
    // What the action left on disk at the end is the figure that matters
754
    // for sizing; this walk, one stat per entry under the action directory,
755
    // is the only one the guard mode pays.
756
    // Nothing is sampled from here on, so the pressure monitor must not
757
    // pick this action on a stale figure while its outputs upload.
758
48
    ceilings.watch.last_memory_kb.store(0, Ordering::Relaxed);
759
48
    drop(disk_sample.take());
760
48
    if let Some(
handle2
) = start_disk_sample(disk_directory.as_ref())
761
2
        && let Ok(disk_kb) = handle.await
762
2
    {
763
2
        peak_disk_kb = peak_disk_kb.max(disk_kb);
764
46
    }
765
766
48
    SampledResourceUsage {
767
48
        peak_memory_kb,
768
48
        cpu_time_ms: ticks_to_millis(
769
48
            cpu_ticks_by_pid
770
48
                .values()
771
48
                .sum::<u64>()
772
48
                .saturating_sub(baseline_ticks.unwrap_or(0)),
773
48
        ),
774
48
        last_memory_kb,
775
48
        peak_disk_kb,
776
48
    }
777
48
}
778
779
/// One process as `/proc/<pid>/stat` describes it: its parent and its
780
/// process group.
781
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
782
pub struct ProcEntry {
783
    pub pid: u32,
784
    pub ppid: u32,
785
    pub pgrp: u32,
786
}
787
788
/// The pids that belong to the action, to a fixpoint: every process in the
789
/// action's process group, every child of a member, and every process in a
790
/// member's process group. The action is spawned as its own group leader
791
/// (`process_group(0)` in `inner_execute`), so `action_pgid` is the spawned
792
/// child's pid and its direct descendants inherit the group.
793
///
794
/// Neither link alone is enough. An intermediate shell often exits and
795
/// reparents the real workload to the worker (pid 1 in a pod), so a walk by
796
/// parent from the spawned pid sees a childless zombie; the group survives
797
/// that. Bazel's `test-setup.sh` runs under `set -m`, which puts the test
798
/// binary in a process group of its own, so the group alone sees the few MB
799
/// of the wrapper shell and never the test; the parent link reaches it, and
800
/// its group then covers whatever it reparents. Nothing in the worker's own
801
/// group is ever a member.
802
2
pub fn action_member_pids(action_pgid: u32, worker_pgid: u32, procs: &[ProcEntry]) -> HashSet<u32> {
803
2
    action_membership(&[action_pgid], worker_pgid, procs).pids
804
2
}
805
806
/// The processes that belong to an action and the process groups they are
807
/// in.
808
#[derive(Debug, Default, Clone, PartialEq, Eq)]
809
pub struct ActionMembers {
810
    pub pids: HashSet<u32>,
811
    /// Every group holding a member. A seed group with no process left is
812
    /// not here, so a group id the kernel reuses later is never signalled.
813
    pub groups: HashSet<u32>,
814
}
815
816
/// [`action_member_pids`] from several starting groups: the action's own
817
/// and the groups found on earlier samples, which still hold a `set -m`
818
/// test after its wrapper exited and nothing else links it to the action.
819
222
pub fn action_membership(
820
222
    seed_groups: &[u32],
821
222
    worker_pgid: u32,
822
222
    procs: &[ProcEntry],
823
222
) -> ActionMembers {
824
222
    let mut members = HashSet::new();
825
222
    let mut groups: HashSet<u32> = seed_groups
826
222
        .iter()
827
222
        .copied()
828
389
        .
filter222
(|pgid| *pgid != worker_pgid)
829
222
        .collect();
830
    loop {
831
387
        let mut grew = false;
832
2.29k
        for proc_entry in 
procs387
{
833
2.29k
            if proc_entry.pgrp == worker_pgid || 
members707
.
contains707
(
&proc_entry.pid707
) {
834
1.92k
                continue;
835
373
            }
836
373
            if groups.contains(&proc_entry.pgrp) || 
members43
.
contains43
(
&proc_entry.ppid43
) {
837
334
                members.insert(proc_entry.pid);
838
334
                groups.insert(proc_entry.pgrp);
839
334
                grew = true;
840
334
            
}39
841
        }
842
387
        if !grew {
843
222
            let groups = procs
844
222
                .iter()
845
1.28k
                .
filter222
(|proc_entry| members.contains(&proc_entry.pid))
846
222
                .map(|proc_entry| proc_entry.pgrp)
847
222
                .collect();
848
222
            return ActionMembers {
849
222
                pids: members,
850
222
                groups,
851
222
            };
852
165
        }
853
    }
854
222
}
855
856
/// The action's members on the live system; see [`action_membership`].
857
/// The kills resolve the groups to signal through this.
858
60
pub fn action_membership_live(seed_groups: &[u32]) -> ActionMembers {
859
60
    let table: Vec<ProcEntry> = live_process_table()
860
60
        .into_iter()
861
60
        .map(|(entry, _)| entry)
862
60
        .collect();
863
60
    action_membership(seed_groups, worker_pgid(), &table)
864
60
}
865
866
/// Sums the resident memory of every process that belongs to the action
867
/// (see [`action_membership`] and [`members_memory_kb`]) and records their
868
/// CPU ticks, and the groups they are in in `watch`. Returns `None` when no
869
/// member process can be read (the action has no processes left).
870
158
fn sample_process_group(
871
158
    pgid: u32,
872
158
    table: &[(ProcEntry, Option<u64>)],
873
158
    watch: &ActionMemoryWatch,
874
158
    cpu_ticks_by_pid: &mut HashMap<u32, u64>,
875
158
    memory_gate: &mut ExactMemoryGate,
876
158
    exact: bool,
877
158
) -> Option<u64> {
878
158
    let entries: Vec<ProcEntry> = table.iter().map(|(entry, _)| *entry).collect();
879
158
    let mut seeds = watch.process_groups.lock().clone();
880
158
    seeds.push(pgid);
881
    let ActionMembers {
882
158
        pids: members,
883
158
        groups,
884
158
    } = action_membership(&seeds, worker_pgid(), &entries);
885
158
    *watch.process_groups.lock() = groups.into_iter().collect();
886
158
    let mut member_pids = Vec::new();
887
983
    for &(entry, ticks) in 
table158
{
888
983
        if !members.contains(&entry.pid) {
889
684
            continue;
890
299
        }
891
299
        member_pids.push(entry.pid);
892
        // Read now, not from the shared snapshot: a request to a pooled
893
        // process can start and end within one snapshot's lifetime, and
894
        // would then measure nothing.
895
299
        if let Some(ticks) = read_process_cpu_ticks(entry.pid).or(ticks) {
896
299
            // Monotonic per process, but a pid could in principle be reused
897
299
            // within one action, so never let the figure go backwards.
898
299
            let slot = cpu_ticks_by_pid.entry(entry.pid).or_insert(0);
899
299
            *slot = (*slot).max(ticks);
900
299
        
}0
901
    }
902
158
    members_memory_kb(&member_pids, memory_gate, exact)
903
158
}
904
905
/// How long the sampler may reuse an exact memory figure; see
906
/// [`ExactMemoryGate`].
907
const EXACT_MEMORY_MAX_AGE: Duration = Duration::from_secs(2);
908
909
/// How far, in percent, the members' RSS may move from where it was when
910
/// the exact memory figure was read before the sampler reads it again; see
911
/// [`ExactMemoryGate`].
912
const EXACT_MEMORY_MAX_DRIFT_PERCENT: u64 = 10;
913
914
/// Decides which samples read the exact memory figure. On Linux that is
915
/// `Pss` from `smaps_rollup`, a walk of the page tables that costs several
916
/// milliseconds per resident GiB, four times a second for every running
917
/// action. The members' RSS is a counter read that `Pss` never exceeds, so
918
/// a sample reuses the last exact figure, capped at the RSS, only while:
919
/// - the RSS is under 90% of the limit a kill is judged against, so no
920
///   ceiling kill and no near-limit OOM classification (see
921
///   [`classify_outcome`]) can follow from the figure;
922
/// - the RSS is within [`EXACT_MEMORY_MAX_DRIFT_PERCENT`] of where it was
923
///   at the last exact read, so an action is read again as it grows, and
924
///   one that ends before the next refresh still reports its peak;
925
/// - that read is under [`EXACT_MEMORY_MAX_AGE`] old. `Pss` can rise while
926
///   the RSS stays flat, when another process sharing the pages exits, so
927
///   this age alone bounds how far the peak and the figure the pressure
928
///   monitor compares can trail.
929
#[derive(Debug, Clone, Copy)]
930
pub struct ExactMemoryGate {
931
    oom_limit_kb: Option<u64>,
932
    last: Option<ExactMemoryReading>,
933
}
934
935
#[derive(Debug, Clone, Copy)]
936
struct ExactMemoryReading {
937
    read_at: Instant,
938
    bound_kb: u64,
939
    exact_kb: u64,
940
}
941
942
impl ExactMemoryGate {
943
    /// `oom_limit_kb` is what a SIGKILL nobody here sent is judged against.
944
50
    pub const fn new(oom_limit_kb: Option<u64>) -> Self {
945
50
        Self {
946
50
            oom_limit_kb,
947
50
            last: None,
948
50
        }
949
50
    }
950
951
    /// The figure for a sample whose members' RSS is `bound_kb`, when the
952
    /// last exact one serves; `None` when the sample must read it.
953
117
    pub fn reuse(&self, bound_kb: u64, now: Instant) -> Option<u64> {
954
117
        let 
last73
= self.last
?44
;
955
73
        let under_ceiling = self
956
73
            .oom_limit_kb
957
73
            .is_none_or(|limit_kb| 
bound_kb16
.
saturating_mul16
(10) <
limit_kb16
.
saturating_mul16
(9));
958
73
        let fresh = now.saturating_duration_since(last.read_at) < EXACT_MEMORY_MAX_AGE;
959
73
        let steady = bound_kb.abs_diff(last.bound_kb).saturating_mul(100)
960
73
            <= last.bound_kb.saturating_mul(EXACT_MEMORY_MAX_DRIFT_PERCENT);
961
73
        (under_ceiling && 
fresh71
&&
steady65
).then(||
last.exact_kb52
.
min52
(
bound_kb52
))
962
117
    }
963
964
56
    pub const fn record(&mut self, bound_kb: u64, exact_kb: u64, now: Instant) {
965
56
        self.last = Some(ExactMemoryReading {
966
56
            read_at: now,
967
56
            bound_kb,
968
56
            exact_kb,
969
56
        });
970
56
    }
971
}
972
973
/// The members' memory summed, `None` when none of them can be read. Where
974
/// the exact figure walks pages (see [`EXACT_MEMORY_READ_WALKS_PAGES`]) it
975
/// is read only when `exact` asks for it or `memory_gate` cannot reuse the
976
/// last one.
977
158
fn members_memory_kb(pids: &[u32], memory_gate: &mut ExactMemoryGate, exact: bool) -> Option<u64> {
978
221
    let 
sum158
= |read: fn(u32) -> Option<u64>| {
979
221
        pids.iter()
980
425
            .
filter_map221
(|&pid| read(pid))
981
221
            .reduce(|total_kb, kb| 
total_kb150
+
kb150
)
982
221
    };
983
158
    if !EXACT_MEMORY_READ_WALKS_PAGES {
984
0
        return sum(read_process_memory_kb);
985
158
    }
986
158
    let 
bound_kb108
= sum(read_process_memory_bound_kb)
?50
;
987
108
    let now = Instant::now();
988
108
    if !exact && let Some(
memory_kb45
) =
memory_gate105
.
reuse105
(bound_kb, now) {
989
45
        return Some(memory_kb);
990
63
    }
991
63
    let 
exact_kb53
= sum(read_process_memory_kb)
?10
;
992
53
    memory_gate.record(bound_kb, exact_kb, now);
993
53
    Some(exact_kb)
994
158
}
995
996
/// The value of a `Field:    1234 kB` line in `/proc` text, in KiB.
997
120
pub fn parse_kb_field(text: &str, field: &str) -> Option<u64> {
998
859
    
text.lines()120
.
find_map120
(|line| {
999
859
        let 
rest106
= line.strip_prefix(field)
?753
;
1000
106
        rest.split_whitespace().next()
?0
.parse().ok()
1001
859
    })
1002
120
}
1003
1004
/// Parses the process group id (`pgrp`, field 5) from the contents of a
1005
/// `/proc/<pid>/stat` line.
1006
///
1007
/// `comm` (field 2) can contain spaces and parentheses, so fields are parsed
1008
/// relative to the final `)` to avoid miscounting. Returns `None` when the
1009
/// line is malformed.
1010
#[cfg(target_os = "linux")]
1011
6
pub fn parse_pgid_from_stat(stat: &str) -> Option<u32> {
1012
6
    parse_links_from_stat(stat).map(|(_, pgrp)| pgrp)
1013
6
}
1014
1015
/// Parses the parent pid (`ppid`, field 4) and the process group id
1016
/// (`pgrp`, field 5) from the contents of a `/proc/<pid>/stat` line, located
1017
/// relative to the final `)` as [`parse_pgid_from_stat`] does.
1018
967
pub fn parse_links_from_stat(stat: &str) -> Option<(u32, u32)> {
1019
967
    let 
after_comm965
= stat.rsplit_once(')')
?2
.1;
1020
    // Fields after the final ')': state(0) ppid(1) pgrp(2) ...
1021
965
    let mut fields = after_comm.split_whitespace().skip(1);
1022
965
    let 
ppid964
= fields.next()
?1
.
parse964
().
ok964
()
?0
;
1023
964
    let pgrp = fields.next()
?0
.parse().ok()
?0
;
1024
964
    Some((ppid, pgrp))
1025
967
}
1026
1027
/// Sums user and system CPU ticks (`utime` + `stime`, fields 14 and 15) from
1028
/// the contents of a `/proc/<pid>/stat` line.
1029
///
1030
/// Parsed relative to the final `)` for the same reason as the pgid above:
1031
/// `comm` can contain spaces and parentheses. Returns `None` when the line is
1032
/// malformed.
1033
///
1034
/// `cutime`/`cstime` are deliberately not included. They only cover children
1035
/// this process has already reaped, so adding them here would double count
1036
/// every child that is still its own entry in the group.
1037
#[cfg(target_os = "linux")]
1038
1.18k
pub fn parse_cpu_ticks_from_stat(stat: &str) -> Option<u64> {
1039
1.18k
    let 
after_comm1.17k
= stat.rsplit_once(')')
?2
.1;
1040
1.17k
    let mut fields = after_comm.split_whitespace();
1041
    // Fields after the final ')': state(0) ppid(1) pgrp(2) session(3) tty(4)
1042
    // tpgid(5) flags(6) minflt(7) cminflt(8) majflt(9) cmajflt(10)
1043
    // utime(11) stime(12) ...
1044
1.17k
    let 
utime1.17k
:
u641.17k
= fields.nth(11)
?1
.
parse1.17k
().
ok1.17k
()
?0
;
1045
1.17k
    let stime: u64 = fields.next()
?0
.parse().ok()
?0
;
1046
1.17k
    Some(utime.saturating_add(stime))
1047
1.18k
}
1048
1049
/// Valid string reasons for a failure.
1050
/// Note: If these change, the documentation should be updated.
1051
#[derive(Debug, Deserialize)]
1052
#[serde(rename_all = "snake_case")]
1053
enum SideChannelFailureReason {
1054
    /// Task should be considered timed out.
1055
    Timeout,
1056
}
1057
1058
/// This represents the json data that can be passed from the running process
1059
/// to the parent via the `SideChannelFile`. See:
1060
/// `config::EnvironmentSource::sidechannelfile` for more details.
1061
/// Note: Any fields added here must be added to the documentation.
1062
#[derive(Debug, Deserialize, Default)]
1063
struct SideChannelInfo {
1064
    /// If the task should be considered a failure and why.
1065
    failure: Option<SideChannelFailureReason>,
1066
}
1067
1068
56
fn action_supports_persistent_workers(
1069
56
    action_info: &ActionInfo,
1070
56
) -> Option<Result<WireFormat, Error>> {
1071
56
    if action_info
1072
56
        .platform_properties
1073
56
        .get(SUPPORTS_WORKERS_PROPERTY)
1074
56
        .is_none_or(|value| 
value2
!=
"1"2
)
1075
    {
1076
54
        return None;
1077
2
    }
1078
1079
2
    let protocol = action_info
1080
2
        .platform_properties
1081
2
        .get(REQUIRES_WORKER_PROTOCOL_PROPERTY)
1082
2
        .map_or("proto", String::as_str);
1083
2
    Some(WireFormat::parse(protocol))
1084
56
}
1085
1086
2
fn os_args_to_strings(args: &[&OsStr]) -> Result<Vec<String>, Error> {
1087
2
    args.iter()
1088
4
        .
map2
(|arg| {
1089
4
            arg.to_str().map(str::to_owned).ok_or_else(|| 
{0
1090
0
                make_err!(
1091
0
                    Code::InvalidArgument,
1092
                    "Persistent worker command arguments must be valid UTF-8: {arg:?}"
1093
                )
1094
0
            })
1095
4
        })
1096
2
        .collect()
1097
2
}
1098
1099
2
fn persistent_worker_request_arguments(argv: &[String]) -> Vec<String> {
1100
2
    argv.iter()
1101
2
        .skip(1)
1102
2
        .skip_while(|arg| !arg.starts_with('@'))
1103
2
        .cloned()
1104
2
        .collect()
1105
2
}
1106
/// Maximum number of file-materialization (hardlink) or subdirectory
1107
/// recursion futures polled concurrently per directory level. Higher values
1108
/// drown APFS's per-volume metadata lock with `hardlink(2)` syscalls and
1109
/// regress overall throughput vs lower-contention concurrency.
1110
///
1111
/// 64 is well above the inflection point on any modern Linux filesystem,
1112
/// so this is also a no-op on Linux beyond replacing tokio scheduling
1113
/// overhead.
1114
const DOWNLOAD_TO_DIRECTORY_CONCURRENCY: usize = 64;
1115
1116
/// One permit per blob fetched into the worker's store, from the pool the
1117
/// worker shares across every action it is staging. The pool is never
1118
/// closed, so a refusal here is a bug, not a condition.
1119
295
async fn acquire_download_permit(downloads: &Semaphore) -> Result<SemaphorePermit<'_>, Error> {
1120
295
    downloads
1121
295
        .acquire()
1122
295
        .await
1123
295
        .map_err(|e| 
make_err!0
(
Code::Internal0
, "Input download pool closed: {e:?}"))
1124
295
}
1125
1126
/// The pool `max_concurrent_input_downloads` asks for. At 0, the default,
1127
/// the pool is too large to bound anything: each action then fetches its
1128
/// inputs on its own, limited only by the per-level fan-out above.
1129
95
fn input_download_pool(max_concurrent_input_downloads: usize) -> Semaphore {
1130
95
    Semaphore::new(match max_concurrent_input_downloads {
1131
91
        0 => Semaphore::MAX_PERMITS,
1132
4
        n => n.min(Semaphore::MAX_PERMITS),
1133
    })
1134
95
}
1135
1136
/// How often a worker without `max_concurrent_input_downloads` may suggest
1137
/// setting it. The signs that call for it arrive in storms; one line per
1138
/// storm is advice, one per failed fetch would bury it.
1139
const INPUT_POOL_HINT_INTERVAL: Duration = Duration::from_mins(10);
1140
1141
/// Suggests a shared input pool to a worker running without one, at most
1142
/// once per `INPUT_POOL_HINT_INTERVAL`.
1143
#[derive(Debug, Default)]
1144
struct InputPoolHint {
1145
    last: Mutex<Option<Instant>>,
1146
}
1147
1148
impl InputPoolHint {
1149
    /// Whether the hint is due at `now`. When it is, the next one is not
1150
    /// due until a full interval has passed.
1151
19
    fn due(&self, now: Instant) -> bool {
1152
19
        let mut last = self.last.lock();
1153
19
        if last.is_some_and(|last| 
now11
.saturating_duration_since(last) <
INPUT_POOL_HINT_INTERVAL11
) {
1154
10
            return false;
1155
9
        }
1156
9
        *last = Some(now);
1157
9
        true
1158
19
    }
1159
}
1160
1161
/// Keeps every digest used by an action present in locally eviction-managed
1162
/// CAS tiers.
1163
///
1164
/// A lease is registered before a digest is populated. This is important for
1165
/// blobs that are not yet in the fast tier: reserving the key first prevents
1166
/// the insertion that follows from immediately evicting the blob under pressure.
1167
/// Every directly discoverable filesystem tier of the worker's
1168
/// `FastSlowStore` is leased when present (including a `RefStore` target).
1169
/// Transforming wrappers remain opaque by design. The reference count lets
1170
/// concurrent actions share a digest safely.
1171
#[derive(Debug)]
1172
struct ActionInputLease {
1173
    filesystem_stores: Vec<Arc<FilesystemStore>>,
1174
    digests: Mutex<Option<HashSet<DigestInfo>>>,
1175
}
1176
1177
impl ActionInputLease {
1178
9
    fn new(
1179
9
        cas_store: &FastSlowStore,
1180
9
        command_digest: DigestInfo,
1181
9
        input_root_digest: DigestInfo,
1182
9
    ) -> Arc<Self> {
1183
9
        let lease = Arc::new(Self {
1184
9
            filesystem_stores: cas_store.get_filesystem_stores(),
1185
9
            digests: Mutex::new(Some(HashSet::new())),
1186
9
        });
1187
9
        lease.lease_digest(&command_digest);
1188
9
        lease.lease_digest(&input_root_digest);
1189
9
        lease
1190
9
    }
1191
1192
80
    fn lease_digest(&self, digest: &DigestInfo) {
1193
80
        let mut digests = self.digests.lock();
1194
80
        let Some(digests) = digests.as_mut() else {
1195
0
            return;
1196
        };
1197
80
        if digests.insert(*digest) {
1198
38
            for filesystem_store in &self.filesystem_stores {
1199
38
                filesystem_store.lease_digest(digest);
1200
38
            }
1201
42
        }
1202
80
    }
1203
1204
25
    fn take_release_work(&self) -> Option<(Vec<Arc<FilesystemStore>>, Vec<DigestInfo>)> {
1205
25
        let 
digests9
= self.digests.lock().take()
?16
.
into_iter9
().
collect9
();
1206
9
        Some((self.filesystem_stores.clone(), digests))
1207
25
    }
1208
1209
25
    fn spawn_release(&self) -> Option<tokio::task::JoinHandle<()>> {
1210
25
        let (
filesystem_stores9
,
digests9
) = self.take_release_work()
?16
;
1211
9
        Some(background_spawn!(
1212
            "action_input_lease_release",
1213
9
            release_filesystem_stores(filesystem_stores, digests)
1214
        ))
1215
25
    }
1216
1217
17
    async fn release(self: Arc<Self>) 
{16
1218
16
        let Some(
handle9
) = self.spawn_release() else {
1219
7
            return;
1220
        };
1221
        // Detach the actual release before awaiting it so cancellation of the
1222
        // action cleanup future cannot strand leases in a later filesystem
1223
        // tier.
1224
9
        let _result = handle.await;
1225
16
    }
1226
}
1227
1228
impl crate::directory_cache::DigestLease for ActionInputLease {
1229
21
    fn acquire(&self, digest: &DigestInfo) {
1230
21
        self.lease_digest(digest);
1231
21
    }
1232
}
1233
1234
impl Drop for ActionInputLease {
1235
9
    fn drop(&mut self) {
1236
9
        drop(self.spawn_release());
1237
9
    }
1238
}
1239
1240
9
async fn release_filesystem_stores(
1241
9
    filesystem_stores: Vec<Arc<FilesystemStore>>,
1242
9
    digests: Vec<DigestInfo>,
1243
9
) {
1244
9
    join_all(
1245
9
        filesystem_stores
1246
9
            .iter()
1247
9
            .map(|filesystem_store| filesystem_store.release_digests(&digests)),
1248
    )
1249
9
    .await;
1250
9
}
1251
1252
/// Input bytes an action's fetch hardlinked, and how many of them the local
1253
/// store already held. Counted per fetch attempt: [`INPUT_FETCH_STATS`] is
1254
/// scoped around one attempt's download, so a retried attempt starts again
1255
/// from zero instead of counting the tree twice. What a failed attempt
1256
/// fetched is carried over by `fetch_with_retry`.
1257
#[derive(Debug, Default)]
1258
pub struct InputFetchStats {
1259
    total_bytes: AtomicU64,
1260
    cached_bytes: AtomicU64,
1261
}
1262
1263
impl InputFetchStats {
1264
17
    fn record(&self, digest: &DigestInfo, cached: bool) {
1265
17
        let bytes = digest.size_bytes();
1266
17
        self.total_bytes.fetch_add(bytes, Ordering::Relaxed);
1267
17
        if cached {
1268
7
            self.cached_bytes.fetch_add(bytes, Ordering::Relaxed);
1269
10
        }
1270
17
    }
1271
1272
    /// Total input bytes and the cached part of them.
1273
86
    pub fn bytes(&self) -> (u64, u64) {
1274
86
        (
1275
86
            self.total_bytes.load(Ordering::Relaxed),
1276
86
            self.cached_bytes.load(Ordering::Relaxed),
1277
86
        )
1278
86
    }
1279
}
1280
1281
tokio::task_local! {
1282
    /// The counters of the fetch attempt running on this task, if any. A
1283
    /// task local rather than a parameter: the download functions are
1284
    /// public and recursive, and the counts are a report, not an input.
1285
    pub static INPUT_FETCH_STATS: Arc<InputFetchStats>;
1286
}
1287
1288
/// Runs `fut`, one input fetch attempt, with fresh counters in scope and
1289
/// returns them with its output.
1290
2
pub async fn with_input_fetch_stats<F: Future>(fut: F) -> (F::Output, Arc<InputFetchStats>) {
1291
2
    let stats = Arc::new(InputFetchStats::default());
1292
2
    let out = INPUT_FETCH_STATS.scope(stats.clone(), fut).await;
1293
2
    (out, stats)
1294
2
}
1295
1296
/// Counts one resolved input file against the attempt in scope; a no-op
1297
/// outside one, as in the download functions' own tests.
1298
149
fn record_input_file(digest: &DigestInfo, cached: bool) {
1299
149
    let _ = INPUT_FETCH_STATS.try_with(|stats| 
stats17
.
record17
(
digest17
,
cached17
));
1300
149
}
1301
1302
/// Milliseconds from `start` to `end`; zero when either was never set or
1303
/// the clock ran backwards between them.
1304
4
fn phase_ms(start: SystemTime, end: SystemTime) -> u64 {
1305
4
    if start == SystemTime::UNIX_EPOCH || 
end == SystemTime::UNIX_EPOCH2
{
1306
2
        return 0;
1307
2
    }
1308
2
    end.duration_since(start)
1309
2
        .map_or(0, |d| u64::try_from(d.as_millis()).unwrap_or(u64::MAX))
1310
4
}
1311
1312
/// Fills the parts of an action's usage report that are known only once its
1313
/// result is uploaded: how long the input fetch and the output upload took,
1314
/// from the result's execution metadata, and how many bytes it produced.
1315
2
pub fn with_phases(usage: &mut ActionResourceUsage, result: &ActionResult) {
1316
2
    let m = &result.execution_metadata;
1317
2
    usage.input_fetch_ms = phase_ms(
1318
2
        m.input_fetch_start_timestamp,
1319
2
        m.input_fetch_completed_timestamp,
1320
2
    );
1321
2
    usage.output_upload_ms = phase_ms(
1322
2
        m.output_upload_start_timestamp,
1323
2
        m.output_upload_completed_timestamp,
1324
2
    );
1325
2
    usage.output_bytes = result
1326
2
        .output_files
1327
2
        .iter()
1328
2
        .map(|file| 
file.digest1
.
size_bytes1
())
1329
2
        .chain(
1330
2
            result
1331
2
                .output_folders
1332
2
                .iter()
1333
2
                .map(|dir| 
dir.tree_digest0
.
size_bytes0
()),
1334
        )
1335
2
        .chain([
1336
2
            result.stdout_digest.size_bytes(),
1337
2
            result.stderr_digest.size_bytes(),
1338
2
        ])
1339
2
        .sum();
1340
2
}
1341
1342
/// Aggressively download the digests of files and make a local folder from it. This function
1343
/// gates each directory level to at most `DOWNLOAD_TO_DIRECTORY_CONCURRENCY`
1344
/// concurrent in-flight materialization futures.
1345
/// We require the `FilesystemStore` to be the `fast` store of `FastSlowStore`. This is for
1346
/// efficiency reasons. We will request the `FastSlowStore` to populate the entry then we will
1347
/// assume the `FilesystemStore` has the file available immediately after and hardlink the file
1348
/// to a new location.
1349
// Sadly we cannot use `async fn` here because the rust compiler cannot determine the auto traits
1350
// of the future. So we need to force this function to return a dynamic future instead.
1351
// see: https://github.com/rust-lang/rust/issues/78649
1352
10
pub fn download_to_directory<'a>(
1353
10
    cas_store: &'a FastSlowStore,
1354
10
    filesystem_store: Pin<&'a FilesystemStore>,
1355
10
    digest: &'a DigestInfo,
1356
10
    current_directory: &'a str,
1357
10
) -> BoxFuture<'a, Result<(), Error>> {
1358
10
    async move {
1359
        // No shared pool, as for a worker without
1360
        // `max_concurrent_input_downloads`; the worker's own arrives through
1361
        // `prepare_action_inputs`.
1362
10
        let downloads = input_download_pool(0);
1363
10
        download_to_directory_bounded(
1364
10
            cas_store,
1365
10
            filesystem_store,
1366
10
            digest,
1367
10
            current_directory,
1368
10
            &downloads,
1369
10
        )
1370
10
        .await
1371
10
    }
1372
10
    .boxed()
1373
10
}
1374
1375
/// `download_to_directory` drawing its fetches from `downloads`, the pool
1376
/// the worker shares across the actions it stages.
1377
13
pub fn download_to_directory_bounded<'a>(
1378
13
    cas_store: &'a FastSlowStore,
1379
13
    filesystem_store: Pin<&'a FilesystemStore>,
1380
13
    digest: &'a DigestInfo,
1381
13
    current_directory: &'a str,
1382
13
    downloads: &'a Semaphore,
1383
13
) -> BoxFuture<'a, Result<(), Error>> {
1384
13
    download_to_directory_with_mounts(
1385
13
        cas_store,
1386
13
        filesystem_store,
1387
13
        digest,
1388
13
        current_directory,
1389
13
        None,
1390
13
        None,
1391
13
        downloads,
1392
13
    )
1393
13
}
1394
1395
97
fn download_to_directory_with_mounts<'a>(
1396
97
    cas_store: &'a FastSlowStore,
1397
97
    filesystem_store: Pin<&'a FilesystemStore>,
1398
97
    digest: &'a DigestInfo,
1399
97
    current_directory: &'a str,
1400
97
    input_lease: Option<Arc<ActionInputLease>>,
1401
97
    input_mounts: Option<Arc<InputMounts>>,
1402
97
    downloads: &'a Semaphore,
1403
97
) -> BoxFuture<'a, Result<(), Error>> {
1404
97
    async move {
1405
97
        let (
dirs96
,
files96
,
inline_nodes96
) = collect_download_links(
1406
97
            cas_store,
1407
97
            digest,
1408
97
            current_directory,
1409
97
            input_lease,
1410
97
            input_mounts,
1411
97
            downloads,
1412
97
        )
1413
97
        .await
?1
;
1414
1415
96
        let (exec_files, plain_files): (Vec<_>, Vec<_>) =
1416
96
            files.into_iter().partition(|file| file.executable);
1417
73
        let mut links =
1418
96
            resolve_plain_files(cas_store, filesystem_store, plain_files, downloads).await
?21
;
1419
73
        links.extend(resolve_exec_files(cas_store, filesystem_store, exec_files, downloads).await
?0
);
1420
1421
        // Every directory exists before anything is written into one, so no
1422
        // later pass has to create a parent it happens to need.
1423
73
        fs::create_dir_many(dirs).await
?0
;
1424
73
        materialize_inline_nodes(cas_store, filesystem_store, inline_nodes, downloads).await
?0
;
1425
73
        materialize_links(cas_store, filesystem_store, links, downloads).await
1426
95
    }
1427
97
    .boxed()
1428
97
}
1429
1430
/// A hardlink deferred to the end of the input-tree walk, so the whole tree is
1431
/// materialized by one batched [`fs::hard_link_many`] pass instead of one
1432
/// `spawn_blocking` dispatch per file.
1433
struct PendingLink {
1434
    src: PathBuf,
1435
    dst: PathBuf,
1436
    /// What `src` was resolved from, so a link whose source is gone by the
1437
    /// time the batch runs can be resolved and made again.
1438
    digest: DigestInfo,
1439
    executable: bool,
1440
    /// Keeps the CAS blob's `FileEntry` alive until the link is made, so the
1441
    /// inode cannot be dropped out from under the batch. `None` for an
1442
    /// executable variant, which is a private file outside the entry map and so
1443
    /// has no entry to hold.
1444
    ///
1445
    /// A held entry pins the inode but does not stop an eviction from renaming
1446
    /// the blob, and a variant is deleted outright by the eviction callback, so
1447
    /// a link in the batch can still find its source gone; [`relink_evicted_input`]
1448
    /// then fetches the blob back and links it at once. An operator who sees
1449
    /// those repairs often is running the store at its ceiling and should enable
1450
    /// `experimental_active_input_leases`, since a leased digest is not evicted,
1451
    /// or raise the store's `max_bytes`.
1452
    _keepalive: Option<Arc<FileEntryImpl>>,
1453
}
1454
1455
/// A file whose CAS blob has not been resolved to a path yet.
1456
struct PendingFile {
1457
    digest: DigestInfo,
1458
    dst: String,
1459
    executable: bool,
1460
}
1461
1462
/// The directories to create, the files to hardlink, and the nodes to
1463
/// materialize one at a time, for one input subtree.
1464
type CollectedTree = (Vec<PathBuf>, Vec<PendingFile>, Vec<InlineNode>);
1465
1466
/// How many evicted inputs of one tree are fetched back at once.
1467
const INPUT_RELINK_CONCURRENCY: usize = 32;
1468
1469
/// How many times an input is fetched into the fast store and resolved
1470
/// before the action fails. A store at its ceiling can take a blob away
1471
/// again between the fetch and the lookup, or between the lookup and the
1472
/// link.
1473
const INPUT_FETCH_ATTEMPTS: u32 = 3;
1474
1475
/// The pause before a retry grows by this much per attempt, plus a jitter
1476
/// of up to the same amount taken from the digest, so the retry does not
1477
/// land in the eviction pass that took the blob and a tree's inputs do not
1478
/// all come back in one burst.
1479
const INPUT_FETCH_PAUSE_STEP_MS: u64 = 20;
1480
1481
18
fn input_fetch_pause(attempt: u32, digest: &DigestInfo) -> Duration {
1482
18
    let attempt = u64::from(attempt);
1483
18
    let jitter = (u64::from(digest.packed_hash()[0]) * attempt) % INPUT_FETCH_PAUSE_STEP_MS;
1484
18
    Duration::from_millis(INPUT_FETCH_PAUSE_STEP_MS * attempt + jitter)
1485
18
}
1486
1487
/// The pause before a whole input tree is fetched again doubles from here.
1488
const PREPARE_RETRY_PAUSE_MS: u64 = 1000;
1489
1490
/// The pause before a whole input tree is fetched again never exceeds this.
1491
const PREPARE_RETRY_PAUSE_CAP_MS: u64 = 30_000;
1492
1493
/// Whether a failed input fetch is worth trying again from this worker.
1494
/// These are the codes the store retrier itself treats as transient; a
1495
/// throttled object store surfaces as `Internal` once the retrier gives up,
1496
/// and an unclassified store failure as `Unknown`. The worker's own
1497
/// filesystem lands on those two codes as well: a full disk, a blob at
1498
/// its link limit or a read-only mount map to `Unknown`, a broken pipe to
1499
/// `Internal`. A worker whose disk is the problem has to hand the action
1500
/// back so it lands on a healthy one, not fetch into the same disk until
1501
/// the deadline, so an operating system error is never retried here. Such
1502
/// an error carries the OS error number in its message; a store's does not.
1503
46
fn is_transient_fetch_error(err: &Error) -> bool {
1504
46
    match err.code {
1505
        Code::Unavailable
1506
        | Code::ResourceExhausted
1507
        | Code::Aborted
1508
        | Code::DeadlineExceeded
1509
11
        | Code::DataLoss => true,
1510
25
        Code::Internal | Code::Unknown => !is_os_error(err),
1511
10
        _ => false,
1512
    }
1513
46
}
1514
1515
/// Whether the error was raised by the operating system on this worker:
1516
/// `std::io::Error` prints those as `... (os error N)`.
1517
25
fn is_os_error(err: &Error) -> bool {
1518
25
    err.messages
1519
25
        .iter()
1520
215
        .
any25
(|message| message.contains("(os error "))
1521
25
}
1522
1523
/// Pause before fetching the input tree again: 1 s, 2 s, 4 s ... capped at
1524
/// 30 s, shortened by up to a quarter with a jitter taken from the digest so
1525
/// the workers that failed together do not all come back together.
1526
81
fn prepare_retry_pause(attempt: u32, digest: &DigestInfo) -> Duration {
1527
81
    let doublings = attempt.saturating_sub(1).min(5);
1528
81
    let base_ms = (PREPARE_RETRY_PAUSE_MS << doublings).min(PREPARE_RETRY_PAUSE_CAP_MS);
1529
81
    let jitter_byte = (u64::from(digest.packed_hash()[0]) * u64::from(attempt)) % 256;
1530
81
    Duration::from_millis(base_ms - base_ms * jitter_byte / 1024)
1531
81
}
1532
1533
/// Counts the retries of one input fetch on `worker.prepare.retries` and
1534
/// settles it on exactly one terminal outcome once a retry was made:
1535
/// `recovered`, `exhausted`, `failed` when a later attempt failed for good,
1536
/// or `abandoned` when the fetch was dropped mid-retry by a kill or the
1537
/// download timeout. A fetch that succeeded or failed first time records
1538
/// nothing, so the terminal outcomes add up to the fetches retried.
1539
struct PrepareRetries<R: Fn(&'static str)> {
1540
    record: R,
1541
    retries: u32,
1542
    settled: bool,
1543
}
1544
1545
impl<R: Fn(&'static str)> PrepareRetries<R> {
1546
353
    const fn new(record: R) -> Self {
1547
353
        Self {
1548
353
            record,
1549
353
            retries: 0,
1550
353
            settled: false,
1551
353
        }
1552
353
    }
1553
1554
24
    fn retried(&mut self) {
1555
24
        self.retries += 1;
1556
24
        (self.record)("retried");
1557
24
    }
1558
1559
349
    fn settle(mut self, outcome: &'static str) {
1560
349
        self.settled = true;
1561
349
        if self.retries > 0 {
1562
11
            (self.record)(outcome);
1563
338
        }
1564
349
    }
1565
}
1566
1567
impl<R: Fn(&'static str)> Drop for PrepareRetries<R> {
1568
353
    fn drop(&mut self) {
1569
353
        if !self.settled && 
self.retries > 04
{
1570
2
            (self.record)("abandoned");
1571
351
        }
1572
353
    }
1573
}
1574
1575
/// Records on `worker.prepare.retries`. A named function rather than the
1576
/// helper itself, so the metrics reference generator sees the call.
1577
32
fn record_prepare_outcome(outcome: &'static str) {
1578
32
    record_prepare_retry(outcome);
1579
32
}
1580
1581
/// Fetches a blob into the fast store with `populate` and resolves it there
1582
/// with `resolve`, again when the store has taken it away in between.
1583
172
async fn fetch_and_resolve<T, P, PFut, R, RFut>(
1584
172
    digest: DigestInfo,
1585
172
    populate: P,
1586
172
    resolve: R,
1587
172
) -> Result<T, Error>
1588
172
where
1589
172
    P: Fn() -> PFut,
1590
172
    PFut: Future<Output = Result<(), Error>>,
1591
172
    R: Fn() -> RFut,
1592
172
    RFut: Future<Output = Result<T, Error>>,
1593
172
{
1594
172
    let mut attempt = 1;
1595
    loop {
1596
176
        populate()
1597
176
            .await
1598
173
            .err_tip(|| 
format!22
("Fetching {digest} for an input"))
?22
;
1599
151
        match resolve().await {
1600
6
            Err(
err5
) if err.code == Code::NotFoun
d5
=> {
1601
5
                if attempt >= INPUT_FETCH_ATTEMPTS {
1602
1
                    record_input_relink("exhausted");
1603
1
                    return Err(err);
1604
4
                }
1605
4
                tokio::time::sleep(input_fetch_pause(attempt, &digest)).await;
1606
4
                attempt += 1;
1607
            }
1608
146
            result => return result,
1609
        }
1610
    }
1611
169
}
1612
1613
/// Executes every deferred hardlink of an input tree in one batched pass.
1614
///
1615
/// A link whose source has gone missing since it was resolved is made again
1616
/// from a fresh copy of the blob; any other failure fails the tree.
1617
73
async fn materialize_links(
1618
73
    cas_store: &FastSlowStore,
1619
73
    filesystem_store: Pin<&FilesystemStore>,
1620
73
    links: Vec<PendingLink>,
1621
73
    downloads: &Semaphore,
1622
73
) -> Result<(), Error> {
1623
73
    let pairs = links
1624
73
        .iter()
1625
149
        .
map73
(|link| (link.src.clone(), link.dst.clone()))
1626
73
        .collect();
1627
73
    let mut evicted = Vec::new();
1628
149
    for (link, result) in 
links.iter()73
.
zip73
(
fs::hard_link_many73
(pairs).await
?0
) {
1629
149
        let Err(
err2
) = result else {
1630
147
            continue;
1631
        };
1632
        // `NotFound` is the source: this walk made the destination's parent.
1633
        // The store evicted the blob, or deleted the variant, after it was
1634
        // resolved. Looking for the source now would prove nothing, since a
1635
        // neighbouring action that stages the same file builds the variant
1636
        // again within milliseconds; the link is made again from a fetch.
1637
2
        if err.code != Code::NotFound {
1638
0
            return Err(hardlink_error(filesystem_store, link, err));
1639
2
        }
1640
2
        evicted.push(link);
1641
    }
1642
73
    if evicted.is_empty() {
1643
71
        return Ok(());
1644
2
    }
1645
    // The store at its ceiling is where several inputs of one tree go at
1646
    // once, so they are fetched back together, not one round trip each.
1647
2
    let repairs: Vec<BoxFuture<'_, Result<(), Error>>> = evicted
1648
2
        .into_iter()
1649
2
        .map(|link| relink_and_report(cas_store, filesystem_store, link, downloads).boxed())
1650
2
        .collect();
1651
2
    futures::stream::iter(repairs)
1652
2
        .buffer_unordered(INPUT_RELINK_CONCURRENCY)
1653
2
        .try_collect::<()>()
1654
2
        .await
1655
73
}
1656
1657
2
async fn relink_and_report(
1658
2
    cas_store: &FastSlowStore,
1659
2
    filesystem_store: Pin<&FilesystemStore>,
1660
2
    link: &PendingLink,
1661
2
    downloads: &Semaphore,
1662
2
) -> Result<(), Error> {
1663
2
    let _permit = acquire_download_permit(downloads).await
?0
;
1664
2
    info!(
1665
        digest = %link.digest,
1666
        src_path = ?link.src,
1667
        "Hardlink source was evicted while the input tree was staged, fetching it again"
1668
    );
1669
2
    relink_evicted_input(cas_store, filesystem_store, link)
1670
2
        .await
1671
2
        .map_err(|err| 
hardlink_error0
(
filesystem_store0
,
link0
,
err0
))
1672
2
}
1673
1674
/// The error a failed input hardlink surfaces as, with what an operator needs
1675
/// to tell an evicted source from everything else.
1676
0
fn hardlink_error(filesystem_store: Pin<&FilesystemStore>, link: &PendingLink, e: Error) -> Error {
1677
0
    let (src_path, dest) = (&link.src, &link.dst);
1678
0
    let src_metadata = std::fs::metadata(src_path);
1679
0
    let dest_metadata = std::fs::metadata(dest);
1680
0
    let dest_parent_metadata = dest.parent().map(Path::metadata);
1681
0
    let snapshot = filesystem_store.get_eviction_snapshot();
1682
0
    warn!(?e, fs_eviction_snapshot = %snapshot, ?src_path, ?src_metadata, ?dest, ?dest_metadata, ?dest_parent_metadata, "Could not make hardlink");
1683
0
    if e.code == Code::NotFound {
1684
0
        e.append(
1685
0
            format!(
1686
            "Could not make hardlink from {} to {}, file was likely evicted from cache.\n\
1687
            This error often occurs when the filesystem store's max_bytes is too small for your workload.\n\
1688
            To fix this issue:\n\
1689
            1. Increase the 'max_bytes' value in your filesystem store configuration\n\
1690
            2. Example: Change 'max_bytes: 10000000000' to 'max_bytes: 50000000000' (or higher)\n\
1691
            3. The setting is typically found in your nativelink.json config under:\n\
1692
            stores -> [your_filesystem_store] -> filesystem -> eviction_policy -> max_bytes\n\
1693
            4. Restart NativeLink after making the change\n\n\
1694
            If this error persists after increasing max_bytes several times, please report at:\n\
1695
            https://github.com/TraceMachina/nativelink/issues\n\
1696
            Include your config file and both server and client logs to help us assist you.",
1697
0
            src_path.display(), dest.display()
1698
        ))
1699
    } else {
1700
0
        e.append(format!(
1701
            "Could not make hardlink from {} to {}",
1702
0
            src_path.display(),
1703
0
            dest.display()
1704
        ))
1705
    }
1706
0
}
1707
1708
/// Makes one hardlink again after the batch found its source gone.
1709
///
1710
/// Every source of an input tree is resolved before the first link is made,
1711
/// and a store at its ceiling can evict a blob in between: eviction renames a
1712
/// plain blob out from under the path that was resolved, and deletes an
1713
/// executable variant outright. The blob is still in the slow tier, so it is
1714
/// fetched back and linked at once. A plain blob is linked inside its entry's
1715
/// path lock, which an eviction has to take to rename it, so this link cannot
1716
/// lose the same race again; a variant is linked straight after it is
1717
/// resolved, which leaves a window of one call where the batch left a whole
1718
/// tree's worth.
1719
2
async fn relink_evicted_input(
1720
2
    cas_store: &FastSlowStore,
1721
2
    filesystem_store: Pin<&FilesystemStore>,
1722
2
    link: &PendingLink,
1723
2
) -> Result<(), Error> {
1724
2
    if link.executable {
1725
1
        fetch_and_resolve(
1726
1
            link.digest,
1727
1
            || cas_store.populate_fast_store(link.digest.into()),
1728
1
            || async {
1729
1
                let src = filesystem_store
1730
1
                    .get_executable_hardlink_source(&link.digest)
1731
1
                    .await
1732
1
                    .err_tip(|| 
{0
1733
0
                        format!("Resolving the executable variant for {} again", link.digest)
1734
0
                    })?;
1735
1
                fs::hard_link(src, &link.dst).await
1736
2
            },
1737
        )
1738
1
        .await
?0
;
1739
1
        record_input_relink("executable");
1740
1
        return Ok(());
1741
1
    }
1742
1
    fetch_and_resolve(
1743
1
        link.digest,
1744
1
        || cas_store.populate_fast_store(link.digest.into()),
1745
1
        || async {
1746
1
            let file_entry = filesystem_store
1747
1
                .get_file_entry_for_digest(&link.digest)
1748
1
                .await
1749
1
                .err_tip(|| "Resolving an evicted hardlink source again")
?0
;
1750
1
            let dst = link.dst.clone();
1751
1
            file_entry
1752
2
                .
get_file_path_locked1
(|src| async move
{1
fs::hard_link1
(
src1
, dst).await })
1753
1
                .await
1754
2
        },
1755
    )
1756
1
    .await
?0
;
1757
1
    record_input_relink("plain");
1758
1
    Ok(())
1759
2
}
1760
1761
/// Resolves every non-executable file of an input tree to its CAS blob path.
1762
///
1763
/// The blob is nearly always resident, and `populate_fast_store` would take the
1764
/// store's eviction lock only to discover that, so resolving the entry directly
1765
/// answers the same question and yields the path. Only a genuine miss pays the
1766
/// populate round trip.
1767
96
async fn resolve_plain_files(
1768
96
    cas_store: &FastSlowStore,
1769
96
    filesystem_store: Pin<&FilesystemStore>,
1770
96
    files: Vec<PendingFile>,
1771
96
    downloads: &Semaphore,
1772
96
) -> Result<Vec<PendingLink>, Error> {
1773
96
    let mut tasks = Vec::with_capacity(files.len());
1774
164
    for file in 
files96
{
1775
164
        tasks.push(async move {
1776
164
            let 
file_entry140
= if let Ok(
entry3
) = filesystem_store
1777
164
                .get_file_entry_for_digest(&file.digest)
1778
164
                .await
1779
            {
1780
3
                record_input_file(&file.digest, true);
1781
3
                entry
1782
            } else {
1783
161
                let _permit = acquire_download_permit(downloads).await
?0
;
1784
161
                let 
entry137
= fetch_and_resolve(
1785
161
                    file.digest,
1786
161
                    || cas_store.populate_fast_store(file.digest.into()),
1787
137
                    || filesystem_store.get_file_entry_for_digest(&file.digest),
1788
                )
1789
161
                .await
1790
158
                .err_tip(|| "During hard link")
?21
;
1791
137
                record_input_file(&file.digest, false);
1792
137
                entry
1793
            };
1794
            // TODO: add a test for #2051: deadlock with large number of files
1795
140
            let src_path = file_entry
1796
280
                .
get_file_path_locked140
(|src| async move
{140
Ok(src)140
})
1797
140
                .await
1798
140
                .err_tip(|| 
format!0
("for digest {}", file.digest))
?0
;
1799
140
            Ok::<_, Error>(PendingLink {
1800
140
                src: PathBuf::from(src_path),
1801
140
                dst: PathBuf::from(file.dst),
1802
140
                digest: file.digest,
1803
140
                executable: false,
1804
140
                _keepalive: Some(file_entry),
1805
140
            })
1806
161
        });
1807
    }
1808
96
    collect_links(tasks).await
1809
94
}
1810
1811
/// Runs the resolution tasks of one input tree, bounded, into the link batch.
1812
169
async fn collect_links(
1813
169
    tasks: Vec<impl Future<Output = Result<PendingLink, Error>>>,
1814
169
) -> Result<Vec<PendingLink>, Error> {
1815
169
    futures::stream::iter(tasks)
1816
169
        .buffer_unordered(DOWNLOAD_TO_DIRECTORY_CONCURRENCY)
1817
169
        .try_collect()
1818
169
        .await
1819
167
}
1820
1821
/// Resolves every executable file of an input tree to its 0o555 variant.
1822
///
1823
/// The shared 0o444 CAS blob cannot carry the executable bit, so each of these
1824
/// digests hardlinks a private variant instead, and the batch checks the whole
1825
/// tree's variants for existence in one dispatch.
1826
///
1827
/// A variant cannot be built from a blob that is not resident, and only this
1828
/// caller can fetch one, so those digests are fetched and resolved here, again
1829
/// if the store evicts the blob before the variant is built.
1830
73
async fn resolve_exec_files(
1831
73
    cas_store: &FastSlowStore,
1832
73
    filesystem_store: Pin<&FilesystemStore>,
1833
73
    files: Vec<PendingFile>,
1834
73
    downloads: &Semaphore,
1835
73
) -> Result<Vec<PendingLink>, Error> {
1836
73
    let digests: Vec<DigestInfo> = files.iter().map(|file| file.digest).collect();
1837
73
    let sources = filesystem_store
1838
73
        .get_executable_hardlink_sources(&digests)
1839
73
        .await;
1840
1841
73
    let mut tasks = Vec::with_capacity(files.len());
1842
73
    for (
file9
,
source9
) in files.into_iter().zip(sources) {
1843
9
        tasks.push(async move {
1844
9
            let src_path = if let Ok(
src_path4
) = source {
1845
4
                record_input_file(&file.digest, true);
1846
4
                src_path
1847
            } else {
1848
5
                let _permit = acquire_download_permit(downloads).await
?0
;
1849
5
                let src_path = fetch_and_resolve(
1850
5
                    file.digest,
1851
5
                    || cas_store.populate_fast_store(file.digest.into()),
1852
5
                    || filesystem_store.get_executable_hardlink_source(&file.digest),
1853
                )
1854
5
                .await
1855
5
                .err_tip(|| 
format!0
("Resolving executable variant for {}", file.digest))
?0
;
1856
5
                record_input_file(&file.digest, false);
1857
5
                src_path
1858
            };
1859
9
            Ok::<_, Error>(PendingLink {
1860
9
                src: PathBuf::from(src_path),
1861
9
                dst: PathBuf::from(file.dst),
1862
9
                digest: file.digest,
1863
9
                executable: true,
1864
9
                _keepalive: None,
1865
9
            })
1866
9
        });
1867
    }
1868
73
    collect_links(tasks).await
1869
73
}
1870
1871
/// Walks the input tree, reading its `Directory` protos and sorting every node
1872
/// into one of the passes the caller then runs: directories to create, files to
1873
/// hardlink in a batch, and the few nodes that need individual treatment.
1874
///
1875
/// Nothing is written to disk here, which is what lets the caller create every
1876
/// directory before any node is materialized into one.
1877
127
fn collect_download_links<'a>(
1878
127
    cas_store: &'a FastSlowStore,
1879
127
    digest: &'a DigestInfo,
1880
127
    current_directory: &'a str,
1881
127
    input_lease: Option<Arc<ActionInputLease>>,
1882
127
    input_mounts: Option<Arc<InputMounts>>,
1883
127
    downloads: &'a Semaphore,
1884
127
) -> BoxFuture<'a, Result<CollectedTree, Error>> {
1885
127
    async move {
1886
127
        if let Some(
input_lease24
) = &input_lease {
1887
24
            input_lease.lease_digest(digest);
1888
103
        }
1889
127
        if let Some(
mounts21
) = &input_mounts {
1890
21
            let lease = input_lease
1891
21
                .as_deref()
1892
21
                .map(|lease| -> &dyn crate::directory_cache::DigestLease { lease });
1893
21
            if mounts
1894
21
                .prepare(Path::new(current_directory), *digest, lease)
1895
21
                .await
?1
1896
            {
1897
6
                return Ok((Vec::new(), Vec::new(), Vec::new()));
1898
14
            }
1899
106
        }
1900
        // The permit covers the fetch alone, never the walk below it: a
1901
        // parent holding one while its children wait for theirs would
1902
        // deadlock a tree deeper than the pool.
1903
120
        let directory = {
1904
120
            let _permit = acquire_download_permit(downloads).await
?0
;
1905
120
            get_and_decode_digest::<ProtoDirectory>(cas_store, digest.into())
1906
120
                .await
1907
120
                .err_tip(|| "Converting digest to Directory")
?0
1908
        };
1909
120
        let mut futures = Vec::new();
1910
120
        let mut pending_files = Vec::new();
1911
120
        let mut inline_nodes = Vec::new();
1912
1913
180
        for file in 
directory.files120
{
1914
180
            let digest: DigestInfo = file
1915
180
                .digest
1916
180
                .err_tip(|| "Expected Digest to exist in Directory::file::digest")
?0
1917
180
                .try_into()
1918
180
                .err_tip(|| "In Directory::file::digest")
?0
;
1919
180
            let dest = format!("{}/{}", current_directory, file.name);
1920
180
            let is_executable = file.is_executable;
1921
180
            let (mtime, custom_unix_mode) = match file.node_properties {
1922
3
                Some(properties) => (properties.mtime, properties.unix_mode),
1923
177
                None => (None, None),
1924
            };
1925
180
            if let Some(
input_lease1
) = &input_lease {
1926
1
                input_lease.lease_digest(&digest);
1927
179
            }
1928
            // Hot path: nothing to stamp, so the file needs nothing from the
1929
            // walk beyond its digest. Hand it up unresolved and let the caller
1930
            // resolve and link the whole tree in one batch.
1931
180
            if mtime.is_none() && 
custom_unix_mode178
.
is_none178
() &&
!is_zero_digest(digest)177
{
1932
173
                pending_files.push(PendingFile {
1933
173
                    digest,
1934
173
                    dst: dest,
1935
173
                    executable: is_executable,
1936
173
                });
1937
173
                continue;
1938
7
            }
1939
7
            inline_nodes.push(InlineNode::File {
1940
7
                digest,
1941
7
                dst: dest,
1942
7
                is_executable,
1943
7
                mtime: mtime
1944
7
                    .map(|mtime| 
FileTime::from_unix_time2
(
mtime.seconds2
,
mtime.nanos as u322
)),
1945
7
                custom_unix_mode,
1946
            });
1947
        }
1948
1949
120
        for 
sub_directory30
in directory.directories {
1950
30
            let digest: DigestInfo = sub_directory
1951
30
                .digest
1952
30
                .err_tip(|| "Expected Digest to exist in Directory::directories::digest")
?0
1953
30
                .try_into()
1954
30
                .err_tip(|| "In Directory::file::digest")
?0
;
1955
30
            let new_directory_path = format!("{}/{}", current_directory, sub_directory.name);
1956
30
            if let Some(
input_lease16
) = &input_lease {
1957
16
                // Reserve queued child directories before their futures are
1958
16
                // polled. Without this, a large parent can leave later
1959
16
                // directory digests exposed to slow-tier eviction while the
1960
16
                // first batch of children is being materialized.
1961
16
                input_lease.lease_digest(&digest);
1962
16
            
}14
1963
30
            let input_lease = input_lease.clone();
1964
30
            let input_mounts = input_mounts.clone();
1965
30
            futures.push(
1966
30
                async move {
1967
30
                    let (
mut sub_dirs29
,
files29
,
inline29
) = collect_download_links(
1968
30
                        cas_store,
1969
30
                        &digest,
1970
30
                        &new_directory_path,
1971
30
                        input_lease,
1972
30
                        input_mounts,
1973
30
                        downloads,
1974
30
                    )
1975
30
                    .await
1976
30
                    .err_tip(|| 
format!1
("in download_to_directory : {new_directory_path}"))
?1
;
1977
29
                    sub_dirs.push(PathBuf::from(new_directory_path));
1978
29
                    Ok::<_, Error>((sub_dirs, files, inline))
1979
30
                }
1980
30
                .boxed(),
1981
            );
1982
        }
1983
1984
        #[cfg(target_family = "unix")]
1985
120
        for 
symlink_node4
in directory.symlinks {
1986
4
            inline_nodes.push(InlineNode::Symlink {
1987
4
                target: symlink_node.target,
1988
4
                dst: format!("{}/{}", current_directory, symlink_node.name),
1989
4
            });
1990
4
        }
1991
1992
        // Gate concurrency: at most DOWNLOAD_TO_DIRECTORY_CONCURRENCY futures
1993
        // polled at once for this directory level. Previously all futures were
1994
        // pushed into an unbounded FuturesUnordered, which on macOS produced
1995
        // thousands of parallel hardlink(2) calls fighting APFS's per-volume
1996
        // metadata lock and regressing throughput vs serial.
1997
120
        let 
collected119
:
Vec<CollectedTree>119
= futures::stream::iter(futures)
1998
120
            .buffer_unordered(DOWNLOAD_TO_DIRECTORY_CONCURRENCY)
1999
120
            .try_collect()
2000
120
            .await
?1
;
2001
2002
119
        let mut dirs = Vec::new();
2003
119
        for (
sub_dirs28
,
sub_files28
,
sub_inline28
) in collected {
2004
28
            dirs.extend(sub_dirs);
2005
28
            pending_files.extend(sub_files);
2006
28
            inline_nodes.extend(sub_inline);
2007
28
        }
2008
119
        Ok((dirs, pending_files, inline_nodes))
2009
127
    }
2010
127
    .boxed()
2011
127
}
2012
2013
/// A node materialized one at a time rather than through a batch, because it
2014
/// needs a private inode or is not a hardlink at all.
2015
///
2016
/// These are a tiny fraction of an input tree, and deferring them alongside the
2017
/// batched files keeps one ordering rule for the whole walk: nothing is written
2018
/// until every directory exists.
2019
enum InlineNode {
2020
    /// A zero-digest file, or one carrying metadata that must be stamped onto
2021
    /// an inode private to this action.
2022
    File {
2023
        digest: DigestInfo,
2024
        dst: String,
2025
        is_executable: bool,
2026
        mtime: Option<FileTime>,
2027
        custom_unix_mode: Option<u32>,
2028
    },
2029
    #[cfg(target_family = "unix")]
2030
    Symlink { target: String, dst: String },
2031
}
2032
2033
/// Stamps the mode an input file asked for. `dest` must be an inode private to
2034
/// this action: the same call on a hardlinked CAS blob would mutate the inode
2035
/// every other action shares.
2036
#[cfg(target_family = "unix")]
2037
7
async fn stamp_unix_mode(
2038
7
    dest: &str,
2039
7
    is_executable: bool,
2040
7
    custom_unix_mode: Option<u32>,
2041
7
) -> Result<(), Error> {
2042
7
    let 
mode3
= match (custom_unix_mode, is_executable) {
2043
1
        (Some(mode), true) => mode | 0o111,
2044
2
        (Some(mode), false) => mode,
2045
0
        (None, true) => 0o555,
2046
4
        (None, false) => return Ok(()),
2047
    };
2048
3
    fs::set_permissions(dest, Permissions::from_mode(mode))
2049
3
        .await
2050
3
        .err_tip(|| 
format!0
("Could not set unix mode in download_to_directory {dest}"))
2051
7
}
2052
2053
/// Non-unix has no mode to stamp.
2054
#[cfg(not(target_family = "unix"))]
2055
async fn stamp_unix_mode(
2056
    _dest: &str,
2057
    _is_executable: bool,
2058
    _custom_unix_mode: Option<u32>,
2059
) -> Result<(), Error> {
2060
    Ok(())
2061
}
2062
2063
/// Materializes the nodes the batched passes cannot handle.
2064
73
async fn materialize_inline_nodes(
2065
73
    cas_store: &FastSlowStore,
2066
73
    filesystem_store: Pin<&FilesystemStore>,
2067
73
    nodes: Vec<InlineNode>,
2068
73
    downloads: &Semaphore,
2069
73
) -> Result<(), Error> {
2070
73
    let mut futures = Vec::with_capacity(nodes.len());
2071
73
    for 
node11
in nodes {
2072
11
        futures.push(async move {
2073
11
            match node {
2074
                #[cfg(target_family = "unix")]
2075
4
                InlineNode::Symlink { target, dst } => fs::symlink(&target, &dst)
2076
4
                    .await
2077
4
                    .err_tip(|| 
format!0
("Could not create symlink {target} -> {dst}")),
2078
                InlineNode::File {
2079
7
                    digest,
2080
7
                    dst: dest,
2081
7
                    is_executable,
2082
7
                    mtime,
2083
7
                    custom_unix_mode,
2084
                } => {
2085
                    {
2086
7
                        let _permit = acquire_download_permit(downloads).await
?0
;
2087
7
                        cas_store.populate_fast_store(digest.into()).await
?0
;
2088
                    }
2089
7
                    async move {
2090
7
                        if is_zero_digest(digest) {
2091
                            // Zero-digest files are never persisted by the
2092
                            // FilesystemStore, so materialise them directly in
2093
                            // the worker exec dir.
2094
4
                            let mut file_slot = fs::create_file(&dest)
2095
4
                                .await
2096
4
                                .err_tip(|| 
format!0
("Could not create zero-digest file at {dest}"))
?0
;
2097
4
                            file_slot
2098
4
                                .write_all(&[])
2099
4
                                .await
2100
4
                                .err_tip(|| 
format!0
("Could not write zero-digest file at {dest}"))
?0
;
2101
                        } else {
2102
                            // Rare path: per-file metadata (a custom unix_mode or
2103
                            // an mtime) must land on a PRIVATE inode. A
2104
                            // chmod/utimes on a hardlink mutates the shared CAS
2105
                            // inode for every other action that hardlinked it
2106
                            // (the #2347 corruption class). Copy the blob into a
2107
                            // private inode, then stamp mode/mtime onto it below.
2108
3
                            let file_entry = filesystem_store
2109
3
                                .get_file_entry_for_digest(&digest)
2110
3
                                .await
2111
3
                                .err_tip(|| "During private copy")
?0
;
2112
3
                            let src_path = file_entry
2113
6
                                .
get_file_path_locked3
(|src| async move
{3
Ok(src)3
})
2114
3
                                .await
?0
;
2115
3
                            let spawned_dest = dest.clone();
2116
3
                            spawn_blocking!("download_to_directory_private_copy", move || {
2117
3
                                std::fs::copy(&src_path, &spawned_dest).map(|_| ()).map_err(|e| 
{0
2118
0
                                    make_err!(
2119
0
                                        Code::Internal,
2120
                                        "Failed to copy CAS blob into a private inode at {spawned_dest}: {e:?}"
2121
                                    )
2122
0
                                })
2123
3
                            })
2124
3
                            .await
2125
3
                            .err_tip(|| 
{0
2126
0
                                "Failed to launch spawn_blocking private copy in download_to_directory"
2127
0
                            })??;
2128
                        }
2129
2130
                        // Private-inode tail (zero-digest or private copy only):
2131
                        // stamp the requested mode and mtime. Safe because the
2132
                        // inode is private to this action.
2133
7
                        stamp_unix_mode(&dest, is_executable, custom_unix_mode).await
?0
;
2134
7
                        if let Some(
mtime2
) = mtime {
2135
2
                            let spawned_dest = dest.clone();
2136
2
                            spawn_blocking!("download_to_directory_set_mtime", move || {
2137
2
                                set_file_mtime(&spawned_dest, mtime).err_tip(|| 
{0
2138
0
                                    format!("Failed to set mtime in download_to_directory {spawned_dest}")
2139
0
                                })
2140
2
                            })
2141
2
                            .await
2142
2
                            .err_tip(
2143
                                || "Failed to launch spawn_blocking in download_to_directory",
2144
0
                            )??;
2145
5
                        }
2146
7
                        Ok(())
2147
7
                    }
2148
7
                    .await
2149
7
                    .map_err(|e: Error| 
e0
.
append0
(
format!0
("for digest {digest}")))
2150
                }
2151
            }
2152
11
        });
2153
    }
2154
73
    futures::stream::iter(futures)
2155
73
        .buffer_unordered(DOWNLOAD_TO_DIRECTORY_CONCURRENCY)
2156
73
        .try_collect()
2157
73
        .await
2158
73
}
2159
2160
/// Prepares action inputs by first trying the directory cache (if available),
2161
/// then falling back to traditional `download_to_directory`.
2162
///
2163
/// This provides a significant performance improvement for repeated builds
2164
/// with the same input directories.
2165
///
2166
/// `work_directory` must already exist and be empty when this is called: the
2167
/// caller pre-creates it so that, on the fallback path, `download_to_directory`
2168
/// has a destination to write into. The directory cache, however, materializes
2169
/// the tree with `hardlink_directory_tree` / APFS `clonefile(2)`, both of which
2170
/// require the destination to *not* exist — so this function removes the empty
2171
/// directory before invoking the cache and recreates it if the cache fails.
2172
0
pub async fn prepare_action_inputs(
2173
0
    directory_cache: &Option<Arc<crate::directory_cache::DirectoryCache>>,
2174
0
    cas_store: &FastSlowStore,
2175
0
    filesystem_store: Pin<&FilesystemStore>,
2176
0
    digest: &DigestInfo,
2177
0
    work_directory: &str,
2178
0
    downloads: &Semaphore,
2179
0
) -> Result<(), Error> {
2180
    prepare_action_inputs_with_lease(
2181
        directory_cache,
2182
        cas_store,
2183
        filesystem_store,
2184
        digest,
2185
        work_directory,
2186
        None,
2187
        downloads,
2188
    )
2189
    .await
2190
}
2191
2192
79
async fn prepare_action_inputs_with_lease(
2193
79
    directory_cache: &Option<Arc<crate::directory_cache::DirectoryCache>>,
2194
79
    cas_store: &FastSlowStore,
2195
79
    filesystem_store: Pin<&FilesystemStore>,
2196
79
    digest: &DigestInfo,
2197
79
    work_directory: &str,
2198
79
    input_lease: Option<Arc<ActionInputLease>>,
2199
79
    downloads: &Semaphore,
2200
79
) -> Result<(), Error> {
2201
    // Try cache first if available. Cache hits are safe while an action lease
2202
    // is active because the directory cache pins its own materialized tree
2203
    // across the hardlink operation. On a miss, pass the action lease into
2204
    // cache construction so every CAS digest is reserved before it is
2205
    // fetched/populated.
2206
79
    if let Some(
cache3
) = directory_cache {
2207
        // `clonefile(2)` and `hardlink_directory_tree` both require the
2208
        // destination to not exist. Remove the empty directory the caller
2209
        // pre-created; without this the cache fails its precondition on every
2210
        // action and silently falls back to the slow download path.
2211
3
        fs::remove_dir(work_directory)
2212
3
            .await
2213
3
            .err_tip(|| 
format!0
("Failed to clear pre-created work directory {work_directory}"))
?0
;
2214
3
        let cache_result = if let Some(
input_lease2
) = &input_lease {
2215
2
            cache
2216
2
                .get_or_create_with_lease(
2217
2
                    *digest,
2218
2
                    Path::new(work_directory),
2219
2
                    Some(input_lease.as_ref()),
2220
2
                )
2221
2
                .await
2222
        } else {
2223
1
            cache
2224
1
                .get_or_create(*digest, Path::new(work_directory))
2225
1
                .await
2226
        };
2227
3
        match cache_result {
2228
3
            Ok(cache_hit) => {
2229
                // The materialized tree is already usable. The directory
2230
                // cache locks each entry down with `set_readonly_recursive`,
2231
                // which leaves directories writable (0o755) and only makes
2232
                // files read-only (0o555). The macOS `clonefile(2)` path
2233
                // copies those modes verbatim and the Linux hardlink walk
2234
                // creates fresh writable directories, so every directory in
2235
                // the materialized tree already accepts the nested output
2236
                // files Bazel actions declare — no separate per-materialize
2237
                // recursive chmod walk is needed here. Files stay read-only,
2238
                // preserving the hermeticity contract and the CAS-hardlink
2239
                // shared-inode invariant.
2240
3
                trace!(
2241
                    ?digest,
2242
                    work_directory, cache_hit, "Successfully prepared inputs via directory cache"
2243
                );
2244
3
                return Ok(());
2245
            }
2246
0
            Err(e) => {
2247
0
                warn!(
2248
                    ?digest,
2249
                    ?e,
2250
                    "Directory cache failed, falling back to traditional download"
2251
                );
2252
                // The cache may have materialized a partial tree before
2253
                // failing. `download_to_directory` needs an existing, empty
2254
                // destination, so discard any partial state and recreate the
2255
                // work directory.
2256
0
                let _cleanup = fs::remove_dir_all(work_directory).await;
2257
0
                fs::create_dir(work_directory)
2258
0
                    .await
2259
0
                    .err_tip(|| format!("Failed to recreate work directory {work_directory}"))?;
2260
            }
2261
        }
2262
76
    }
2263
2264
    // Traditional path (cache disabled or failed)
2265
76
    download_to_directory_with_mounts(
2266
76
        cas_store,
2267
76
        filesystem_store,
2268
76
        digest,
2269
76
        work_directory,
2270
76
        input_lease,
2271
76
        None,
2272
76
        downloads,
2273
76
    )
2274
76
    .await
2275
77
}
2276
2277
#[cfg(target_family = "windows")]
2278
fn is_executable(_metadata: &std::fs::Metadata, full_path: &impl AsRef<Path>) -> bool {
2279
    static EXECUTABLE_EXTENSIONS: &[&str] = &["exe", "bat", "com"];
2280
    EXECUTABLE_EXTENSIONS
2281
        .iter()
2282
        .any(|ext| full_path.as_ref().extension().map_or(false, |v| v == *ext))
2283
}
2284
2285
#[cfg(target_family = "unix")]
2286
280
fn is_executable(metadata: &std::fs::Metadata, _full_path: &impl AsRef<Path>) -> bool {
2287
280
    (metadata.mode() & 0o111) != 0
2288
280
}
2289
2290
type DigestUploader = Arc<tokio::sync::OnceCell<()>>;
2291
2292
280
async fn upload_file(
2293
280
    cas_store: Pin<&impl StoreLike>,
2294
280
    full_path: impl AsRef<Path> + Debug + Send + Sync,
2295
280
    hasher: DigestHasherFunc,
2296
280
    metadata: std::fs::Metadata,
2297
280
    digest_uploaders: Arc<Mutex<HashMap<DigestInfo, DigestUploader>>>,
2298
280
    uploads: Arc<Semaphore>,
2299
280
) -> Result<FileInfo, Error> {
2300
    // Held from the open to the end of the upload: this is the bound on
2301
    // files open at once for the whole output tree.
2302
280
    let _permit = uploads
2303
280
        .acquire()
2304
280
        .await
2305
280
        .err_tip(|| "Upload semaphore closed")
?0
;
2306
280
    let is_executable = is_executable(&metadata, &full_path);
2307
280
    let file_size = metadata.len();
2308
280
    let file = fs::open_file(&full_path, 0, u64::MAX)
2309
280
        .await
2310
280
        .err_tip(|| 
format!0
("Could not open file {full_path:?}"))
?0
;
2311
2312
280
    let (digest, mut file) = hasher
2313
280
        .hasher()
2314
280
        .digest_for_file(&full_path, file.into_inner(), Some(file_size))
2315
280
        .await
2316
280
        .err_tip(|| 
format!0
("Failed to hash file in digest_for_file failed for {full_path:?}"))
?0
;
2317
2318
280
    let digest_uploader = match digest_uploaders.lock().entry(digest) {
2319
1
        std::collections::hash_map::Entry::Occupied(occupied_entry) => occupied_entry.get().clone(),
2320
279
        std::collections::hash_map::Entry::Vacant(vacant_entry) => vacant_entry
2321
279
            .insert(Arc::new(tokio::sync::OnceCell::new()))
2322
279
            .clone(),
2323
    };
2324
2325
    // Only upload a file with a given hash once.  The file may exist multiple
2326
    // times in the output with different names.
2327
280
    digest_uploader
2328
558
        .
get_or_try_init280
(async || {
2329
            // Only upload if the digest doesn't already exist, this should be
2330
            // a much cheaper operation than an upload.
2331
279
            let cas_store = cas_store.as_store_driver_pin();
2332
279
            let store_key: nativelink_util::store_trait::StoreKey<'_> = digest.into();
2333
279
            let has_start = std::time::Instant::now();
2334
279
            if cas_store
2335
279
                .has(store_key.borrow())
2336
279
                .await
2337
279
                .is_ok_and(|result| result.is_some())
2338
            {
2339
7
                trace!(
2340
                    ?digest,
2341
7
                    has_elapsed_ms = has_start.elapsed().as_millis(),
2342
                    "upload_file: digest already exists in CAS, skipping upload",
2343
                );
2344
7
                return Ok(());
2345
272
            }
2346
272
            trace!(
2347
                ?digest,
2348
272
                has_elapsed_ms = has_start.elapsed().as_millis(),
2349
272
                file_size = digest.size_bytes(),
2350
                "upload_file: digest not in CAS, starting upload",
2351
            );
2352
2353
272
            file.rewind().await.err_tip(|| "Could not rewind file")
?0
;
2354
2355
            // Note: For unknown reasons we appear to be hitting:
2356
            // https://github.com/rust-lang/rust/issues/92096
2357
            // or a similar issue if we try to use the non-store driver function, so we
2358
            // are using the store driver function here.
2359
272
            let store_key_for_upload = store_key.clone();
2360
272
            let file_upload_start = std::time::Instant::now();
2361
272
            let upload_result = cas_store
2362
272
                .update_with_whole_file(
2363
272
                    store_key_for_upload,
2364
272
                    full_path.as_ref().into(),
2365
272
                    file,
2366
272
                    UploadSizeInfo::ExactSize(digest.size_bytes()),
2367
272
                )
2368
272
                .await
2369
272
                .map(|_slot| ());
2370
272
            trace!(
2371
                ?digest,
2372
272
                upload_elapsed_ms = file_upload_start.elapsed().as_millis(),
2373
272
                success = upload_result.is_ok(),
2374
                "upload_file: update_with_whole_file completed",
2375
            );
2376
2377
272
            match upload_result {
2378
272
                Ok(()) => Ok(()),
2379
0
                Err(err) => {
2380
                    // Output uploads run concurrently and may overlap (e.g. a file is listed
2381
                    // both as an output file and inside an output directory). When another
2382
                    // upload has already moved the file into CAS, this update can fail with
2383
                    // NotFound even though the digest is now present. Per the RE spec, missing
2384
                    // outputs should be ignored, so treat this as success if the digest exists.
2385
0
                    if err.code == Code::NotFound
2386
0
                        && cas_store
2387
0
                            .has(store_key.borrow())
2388
0
                            .await
2389
0
                            .is_ok_and(|result| result.is_some())
2390
                    {
2391
0
                        Ok(())
2392
                    } else {
2393
0
                        Err(err)
2394
                    }
2395
                }
2396
            }
2397
558
        })
2398
280
        .await
2399
280
        .err_tip(|| 
format!0
("for {full_path:?}"))
?0
;
2400
2401
280
    let name = full_path
2402
280
        .as_ref()
2403
280
        .file_name()
2404
280
        .err_tip(|| 
format!0
("Expected file_name to exist on {full_path:?}"))
?0
2405
280
        .to_str()
2406
280
        .err_tip(|| 
{0
2407
0
            make_err!(
2408
0
                Code::Internal,
2409
                "Could not convert {:?} to string",
2410
                full_path
2411
            )
2412
0
        })?
2413
280
        .to_string();
2414
2415
280
    Ok(FileInfo {
2416
280
        name_or_path: NameOrPath::Name(name),
2417
280
        digest,
2418
280
        is_executable,
2419
280
    })
2420
280
}
2421
2422
2
async fn upload_symlink(
2423
2
    full_path: impl AsRef<Path> + Debug,
2424
2
    full_work_directory_path: impl AsRef<Path>,
2425
2
) -> Result<SymlinkInfo, Error> {
2426
2
    let full_target_path = fs::read_link(full_path.as_ref())
2427
2
        .await
2428
2
        .err_tip(|| 
format!0
("Could not get read_link path of {full_path:?}"))
?0
;
2429
2430
    // Detect if our symlink is inside our work directory, if it is find the
2431
    // relative path otherwise use the absolute path.
2432
2
    let target = if full_target_path.starts_with(full_work_directory_path.as_ref()) {
2433
0
        let full_target_path = RelativePath::from_path(&full_target_path)
2434
0
            .map_err(|v| make_err!(Code::Internal, "Could not convert {} to RelativePath", v))?;
2435
0
        RelativePath::from_path(full_work_directory_path.as_ref())
2436
0
            .map_err(|v| make_err!(Code::Internal, "Could not convert {} to RelativePath", v))?
2437
0
            .relative(full_target_path)
2438
0
            .normalize()
2439
0
            .into_string()
2440
    } else {
2441
2
        full_target_path
2442
2
            .to_str()
2443
2
            .err_tip(|| 
{0
2444
0
                make_err!(
2445
0
                    Code::Internal,
2446
                    "Could not convert '{:?}' to string",
2447
                    full_target_path
2448
                )
2449
0
            })?
2450
2
            .to_string()
2451
    };
2452
2453
2
    let name = full_path
2454
2
        .as_ref()
2455
2
        .file_name()
2456
2
        .err_tip(|| 
format!0
("Expected file_name to exist on {full_path:?}"))
?0
2457
2
        .to_str()
2458
2
        .err_tip(|| 
{0
2459
0
            make_err!(
2460
0
                Code::Internal,
2461
                "Could not convert {:?} to string",
2462
                full_path
2463
            )
2464
0
        })?
2465
2
        .to_string();
2466
2467
2
    Ok(SymlinkInfo {
2468
2
        name_or_path: NameOrPath::Name(name),
2469
2
        target,
2470
2
    })
2471
2
}
2472
2473
16
fn upload_directory<'a, P: AsRef<Path> + Debug + Send + Sync + Clone + 'a>(
2474
16
    cas_store: Pin<&'a impl StoreLike>,
2475
16
    full_dir_path: P,
2476
16
    full_work_directory: &'a str,
2477
16
    hasher: DigestHasherFunc,
2478
16
    digest_uploaders: Arc<Mutex<HashMap<DigestInfo, DigestUploader>>>,
2479
16
    uploads: Arc<Semaphore>,
2480
16
) -> BoxFuture<'a, Result<(Directory, VecDeque<ProtoDirectory>), Error>> {
2481
16
    Box::pin(async move {
2482
16
        let mut file_futures = Vec::new();
2483
16
        let mut dir_futures = Vec::new();
2484
16
        let mut symlink_futures = Vec::new();
2485
        {
2486
16
            let (_permit, dir_handle) = fs::read_dir(&full_dir_path)
2487
16
                .await
2488
16
                .err_tip(|| 
format!0
("Error reading dir for reading {full_dir_path:?}"))
?0
2489
16
                .into_inner();
2490
16
            let mut dir_stream = ReadDirStream::new(dir_handle);
2491
            // Note: Try very hard to not leave file descriptors open. Try to keep them as short
2492
            // lived as possible. This is why we iterate the directory and then build a bunch of
2493
            // futures with all the work we are wanting to do then execute it. It allows us to
2494
            // close the directory iterator file descriptor, then open the child files/folders.
2495
291
            while let Some(
entry_result275
) = dir_stream.next().await {
2496
275
                let entry = entry_result.err_tip(|| "Error while iterating directory")
?0
;
2497
275
                let file_type = entry
2498
275
                    .file_type()
2499
275
                    .await
2500
275
                    .err_tip(|| 
format!0
("Error running file_type() on {entry:?}"))
?0
;
2501
275
                let full_path = full_dir_path.as_ref().join(entry.path());
2502
275
                if file_type.is_dir() {
2503
11
                    let full_dir_path = full_dir_path.clone();
2504
11
                    dir_futures.push(
2505
11
                        upload_directory(
2506
11
                            cas_store,
2507
11
                            full_path.clone(),
2508
11
                            full_work_directory,
2509
11
                            hasher,
2510
11
                            digest_uploaders.clone(),
2511
11
                            uploads.clone(),
2512
                        )
2513
11
                        .and_then(|(dir, all_dirs)| async move {
2514
11
                            let directory_name = full_path
2515
11
                                .file_name()
2516
11
                                .err_tip(|| 
{0
2517
0
                                    format!("Expected file_name to exist on {full_dir_path:?}")
2518
0
                                })?
2519
11
                                .to_str()
2520
11
                                .err_tip(|| 
{0
2521
0
                                    make_err!(
2522
0
                                        Code::Internal,
2523
                                        "Could not convert {:?} to string",
2524
                                        full_dir_path
2525
                                    )
2526
0
                                })?
2527
11
                                .to_string();
2528
2529
11
                            let digest =
2530
11
                                serialize_and_upload_message(&dir, cas_store, &mut hasher.hasher())
2531
11
                                    .await
2532
11
                                    .err_tip(|| 
format!0
("for {}",
full_path.display()0
))
?0
;
2533
2534
11
                            Result::<(DirectoryNode, VecDeque<Directory>), Error>::Ok((
2535
11
                                DirectoryNode {
2536
11
                                    name: directory_name,
2537
11
                                    digest: Some(digest.into()),
2538
11
                                },
2539
11
                                all_dirs,
2540
11
                            ))
2541
22
                        })
2542
11
                        .boxed(),
2543
                    );
2544
264
                } else if file_type.is_file() {
2545
263
                    let digest_uploaders = digest_uploaders.clone();
2546
263
                    let uploads = uploads.clone();
2547
263
                    file_futures.push(async move {
2548
263
                        let metadata = fs::metadata(&full_path)
2549
263
                            .await
2550
263
                            .err_tip(|| 
format!0
("Could not open file {}",
full_path.display()0
))
?0
;
2551
263
                        upload_file(
2552
263
                            cas_store,
2553
263
                            &full_path,
2554
263
                            hasher,
2555
263
                            metadata,
2556
263
                            digest_uploaders,
2557
263
                            uploads,
2558
263
                        )
2559
263
                        .map_ok(TryInto::try_into)
2560
263
                        .await
?0
2561
263
                    });
2562
1
                } else if file_type.is_symlink() {
2563
1
                    symlink_futures.push(
2564
1
                        upload_symlink(full_path, &full_work_directory)
2565
1
                            .map(|symlink| symlink
?0
.try_into()),
2566
                    );
2567
0
                }
2568
            }
2569
        }
2570
2571
        // The stream bound keeps the fan-out in check at each level; the
2572
        // files open at once are bounded by `uploads`, which every level
2573
        // shares, since per-level bounds would multiply.
2574
16
        let dir_entries = futures::stream::iter(dir_futures)
2575
16
            .buffer_unordered(UPLOAD_CONCURRENCY)
2576
16
            .try_collect::<Vec<(DirectoryNode, VecDeque<Directory>)>>()
2577
16
            .await
?0
;
2578
16
        let (mut file_nodes, mut symlinks) = try_join(
2579
16
            futures::stream::iter(file_futures)
2580
16
                .buffer_unordered(UPLOAD_CONCURRENCY)
2581
16
                .try_collect::<Vec<FileNode>>(),
2582
16
            futures::stream::iter(symlink_futures)
2583
16
                .buffer_unordered(UPLOAD_CONCURRENCY)
2584
16
                .try_collect::<Vec<SymlinkNode>>(),
2585
16
        )
2586
16
        .await
?0
;
2587
2588
16
        let mut directory_nodes = Vec::with_capacity(dir_entries.len());
2589
        // For efficiency we use a deque because it allows cheap concat of Vecs.
2590
        // We make the assumption here that when performance is important it is because
2591
        // our directory is quite large. This allows us to cheaply merge large amounts of
2592
        // directories into one VecDeque. Then after we are done we need to collapse it
2593
        // down into a single Vec.
2594
16
        let mut all_child_directories = VecDeque::with_capacity(dir_entries.len());
2595
16
        for (
directory_node11
,
mut recursive_child_directories11
) in dir_entries {
2596
11
            directory_nodes.push(directory_node);
2597
11
            all_child_directories.append(&mut recursive_child_directories);
2598
11
        }
2599
2600
1.26k
        
file_nodes16
.
sort_unstable_by16
(|a, b| a.name.cmp(&b.name));
2601
20
        
directory_nodes16
.
sort_unstable_by16
(|a, b| a.name.cmp(&b.name));
2602
16
        symlinks.sort_unstable_by(|a, b| 
a.name0
.
cmp0
(
&b.name0
));
2603
2604
16
        let directory = Directory {
2605
16
            files: file_nodes,
2606
16
            directories: directory_nodes,
2607
16
            symlinks,
2608
16
            node_properties: None, // We don't support file properties.
2609
16
        };
2610
16
        all_child_directories.push_back(directory.clone());
2611
2612
16
        Ok((directory, all_child_directories))
2613
16
    })
2614
16
}
2615
2616
0
async fn process_side_channel_file(
2617
0
    side_channel_file: Cow<'_, OsStr>,
2618
0
    args: &[&OsStr],
2619
0
    timeout: Duration,
2620
0
) -> Result<Option<Error>, Error> {
2621
0
    let mut json_contents = String::new();
2622
    {
2623
        // Note: Scoping `file_slot` allows the file_slot semaphore to be released faster.
2624
0
        let mut file_slot = match fs::open_file(side_channel_file, 0, u64::MAX).await {
2625
0
            Ok(file_slot) => file_slot,
2626
0
            Err(e) => {
2627
0
                if e.code != Code::NotFound {
2628
0
                    return Err(e).err_tip(|| "Error opening side channel file");
2629
0
                }
2630
                // Note: If file does not exist, it's ok. Users are not required to create this file.
2631
0
                return Ok(None);
2632
            }
2633
        };
2634
0
        file_slot
2635
0
            .read_to_string(&mut json_contents)
2636
0
            .await
2637
0
            .err_tip(|| "Error reading side channel file")?;
2638
    }
2639
2640
0
    let side_channel_info: SideChannelInfo = serde_json5::from_str(&json_contents)
2641
0
        .map_err(Error::from)
2642
0
        .err_tip(|| "Could not convert contents of side channel file (json) to SideChannelInfo")?;
2643
0
    Ok(side_channel_info.failure.map(|failure| match failure {
2644
        SideChannelFailureReason::Timeout => {
2645
0
            let join_args = args.join(OsStr::new(" "));
2646
0
            let command = join_args.to_string_lossy();
2647
0
            warn!(%command, timeout=timeout.as_secs_f32(), "Side channel timeout for command");
2648
0
            Error::new(
2649
0
                Code::DeadlineExceeded,
2650
0
                format!(
2651
                    "Command '{}' timed out after {} seconds",
2652
                    command,
2653
0
                    timeout.as_secs_f32()
2654
                ),
2655
            )
2656
        }
2657
0
    }))
2658
0
}
2659
2660
275
async fn do_cleanup(
2661
275
    running_actions_manager: &Arc<RunningActionsManagerImpl>,
2662
275
    operation_id: &OperationId,
2663
275
    action_directory: &str,
2664
275
) -> Result<(), Error> {
2665
    // Mark this operation as being cleaned up. The other holders of the
2666
    // mark are the orphan sweep and a retry's stale-directory removal, both
2667
    // brief; waiting on them is what keeps this cleanup from being skipped,
2668
    // which would leave the action's entries and reservations in place for
2669
    // good.
2670
275
    let _cleaning_guard = running_actions_manager
2671
275
        .take_cleanup_mark(operation_id.clone())
2672
275
        .await;
2673
2674
275
    let capture = running_actions_manager
2675
275
        .buck2_captures
2676
275
        .lock()
2677
275
        .remove(operation_id);
2678
275
    if let Some(
capture4
) = capture
2679
4
        && let Err(
err0
) = capture.finish().await
2680
    {
2681
        // Capture completeness is independent of the execution result.
2682
0
        warn!(%operation_id, ?err, "Buck2 archive finalization was partial");
2683
275
    }
2684
2685
    // Debug-only hack to not cleanup workers when we need to debug something
2686
275
    if !env::var("NATIVELINK_DONT_CLEANUP")
2687
275
        .unwrap_or_default()
2688
275
        .is_empty()
2689
    {
2690
0
        info!("Skip because env var");
2691
0
        return Ok(());
2692
275
    }
2693
2694
275
    debug!(%action_directory, "Worker cleaning up");
2695
    // Note: We need to be careful to keep trying to cleanup even if one of the steps fails.
2696
    // A fetch or an upload ended by a kill or a timeout can leave a blocking
2697
    // copy or a stamp finishing inside the tree for a moment; the removal is
2698
    // retried through that instead of failing on the first try.
2699
275
    let remove_deadline = Instant::now() + running_actions_manager.max_cleanup_wait;
2700
275
    let remove_dir_result = loop {
2701
275
        match fs::remove_dir_all(action_directory).await {
2702
275
            Ok(()) => break Ok(()),
2703
0
            Err(err) if Instant::now() < remove_deadline => {
2704
0
                debug!(%operation_id, ?err, "Removing the working directory again");
2705
0
                tokio::time::sleep(Duration::from_millis(100)).await;
2706
            }
2707
0
            Err(err) => {
2708
0
                break Err(err)
2709
0
                    .err_tip(|| format!("Could not remove working directory {action_directory}"));
2710
            }
2711
        }
2712
    };
2713
2714
275
    if let Err(
err0
) = running_actions_manager.cleanup_action(operation_id) {
2715
0
        error!(%operation_id, ?err, "Error cleaning up action");
2716
0
        Result::<(), Error>::Err(err).merge(remove_dir_result)
2717
275
    } else if let Err(
err0
) = remove_dir_result {
2718
0
        error!(%operation_id, ?err, "Error removing working directory");
2719
0
        Err(err)
2720
    } else {
2721
275
        Ok(())
2722
    }
2723
275
}
2724
2725
pub trait RunningAction: Sync + Send + Sized + Unpin + 'static {
2726
    /// Returns the action id of the action.
2727
    fn get_operation_id(&self) -> &OperationId;
2728
2729
    /// Anything that needs to execute before the actions is actually executed should happen here.
2730
    fn prepare_action(self: Arc<Self>) -> impl Future<Output = Result<Arc<Self>, Error>> + Send;
2731
2732
    /// Actually perform the execution of the action.
2733
    fn execute(self: Arc<Self>) -> impl Future<Output = Result<Arc<Self>, Error>> + Send;
2734
2735
    /// Any uploading, processing or analyzing of the results should happen here.
2736
    fn upload_results(self: Arc<Self>) -> impl Future<Output = Result<Arc<Self>, Error>> + Send;
2737
2738
    /// Cleanup any residual files, handles or other junk resulting from running the action.
2739
    fn cleanup(self: Arc<Self>) -> impl Future<Output = Result<Arc<Self>, Error>> + Send;
2740
2741
    /// Returns the final result. As a general rule this action should be thought of as
2742
    /// a consumption of `self`, meaning once a return happens here the lifetime of `Self`
2743
    /// is over and any action performed on it after this call is undefined behavior.
2744
    fn get_finished_result(
2745
        self: Arc<Self>,
2746
    ) -> impl Future<Output = Result<ActionResult, Error>> + Send;
2747
2748
    /// Returns worker-observed resource usage captured while this action ran.
2749
    fn resource_usage(&self) -> Option<ActionResourceUsage>;
2750
2751
    /// Returns the work directory of the action.
2752
    fn get_work_directory(&self) -> &String;
2753
}
2754
2755
/// What an action wrote to stdout or stderr: in memory when small, on disk
2756
/// past `max_captured_output_bytes`, with the head kept for the log.
2757
#[derive(Debug)]
2758
pub enum CapturedOutput {
2759
    Memory(Bytes),
2760
    Spilled {
2761
        path: PathBuf,
2762
        len: u64,
2763
        head: Bytes,
2764
    },
2765
}
2766
2767
impl CapturedOutput {
2768
90
    const fn len(&self) -> u64 {
2769
90
        match self {
2770
89
            Self::Memory(bytes) => bytes.len() as u64,
2771
1
            Self::Spilled { len, .. } => *len,
2772
        }
2773
90
    }
2774
2775
    /// The first bytes, for a log line; all of them when in memory.
2776
0
    fn head(&self) -> &[u8] {
2777
0
        match self {
2778
0
            Self::Memory(bytes) => bytes,
2779
0
            Self::Spilled { head, .. } => head,
2780
        }
2781
0
    }
2782
}
2783
2784
/// The first `limit` bytes of captured output as text for a log line. The
2785
/// cut is by bytes, so one through a multibyte character becomes U+FFFD
2786
/// rather than a panic.
2787
33
pub fn log_excerpt(bytes: &[u8], limit: usize) -> Cow<'_, str> {
2788
33
    String::from_utf8_lossy(&bytes[..min(bytes.len(), limit)])
2789
33
}
2790
2791
impl From<Bytes> for CapturedOutput {
2792
4
    fn from(bytes: Bytes) -> Self {
2793
4
        Self::Memory(bytes)
2794
4
    }
2795
}
2796
2797
/// Reads a child's stream to the end, or until `stop` says to keep what
2798
/// has been read: an action can leave a process behind that holds the
2799
/// pipe open after the action itself has exited, and its output must not
2800
/// wait on that. Up to `cap` bytes stay in memory; the moment the buffer
2801
/// passes the cap, everything so far and everything after goes to
2802
/// `spill_path`, and the buffer is cut back to the cap as the head.
2803
92
pub async fn capture_output<R: tokio::io::AsyncRead + Unpin>(
2804
92
    mut reader: R,
2805
92
    cap: u64,
2806
92
    spill_path: PathBuf,
2807
92
    what: &str,
2808
92
    mut stop: watch::Receiver<bool>,
2809
92
) -> Result<CapturedOutput, Error> {
2810
    // `None` when told to stop; a closed sender counts as that too.
2811
295
    async fn read_or_stop<R: tokio::io::AsyncRead + Unpin>(
2812
295
        reader: &mut R,
2813
295
        buf: &mut BytesMut,
2814
295
        stop: &mut watch::Receiver<bool>,
2815
295
        what: &str,
2816
295
    ) -> Result<Option<usize>, Error> {
2817
295
        if *stop.borrow() {
2818
0
            return Ok(None);
2819
295
        }
2820
295
        tokio::select! {
2821
            // Data already in the pipe is read before a stop is honoured.
2822
            biased;
2823
295
            read = reader.read_buf(buf) => read
2824
295
                .map(Some)
2825
295
                .err_tip(|| 
format!0
("Error reading {what} stream")),
2826
295
            _ = stop.changed() => 
Ok(None)0
,
2827
        }
2828
295
    }
2829
92
    let mut buf = BytesMut::new();
2830
    // A raw handle, outside the open-file budget on purpose: the spill
2831
    // lives as long as the child's stream, an fd the budget never covered,
2832
    // and a permit held for a whole noisy action would starve the store
2833
    // paths the budget exists for.
2834
92
    let mut spill: Option<(tokio::fs::File, u64)> = None;
2835
    loop {
2836
295
        if let Some((
file72
,
len72
)) = spill.as_mut() {
2837
72
            let mut chunk = BytesMut::with_capacity(64 * 1024);
2838
72
            let Some(sz) = read_or_stop(&mut reader, &mut chunk, &mut stop, what).await
?0
else {
2839
0
                break;
2840
            };
2841
72
            if sz == 0 {
2842
1
                break;
2843
71
            }
2844
71
            file.write_all(&chunk)
2845
71
                .await
2846
71
                .err_tip(|| 
format!0
("Error spilling {what} to {}",
spill_path.display()0
))
?0
;
2847
71
            *len += sz as u64;
2848
71
            continue;
2849
223
        }
2850
223
        let Some(sz) = read_or_stop(&mut reader, &mut buf, &mut stop, what).await
?0
else {
2851
0
            break;
2852
        };
2853
223
        if sz == 0 {
2854
91
            break;
2855
132
        }
2856
132
        if cap > 0 && 
buf.len() as u64 > cap116
{
2857
1
            let mut file = tokio::fs::File::create(&spill_path)
2858
1
                .await
2859
1
                .err_tip(|| 
format!0
("Error creating {what} spill file {}",
spill_path.display()0
))
?0
;
2860
1
            file.write_all(&buf)
2861
1
                .await
2862
1
                .err_tip(|| 
format!0
("Error spilling {what} to {}",
spill_path.display()0
))
?0
;
2863
1
            let len = buf.len() as u64;
2864
1
            buf.truncate(usize::try_from(cap).unwrap_or(usize::MAX));
2865
1
            spill = Some((file, len));
2866
131
        }
2867
    }
2868
92
    match spill {
2869
91
        None => Ok(CapturedOutput::Memory(buf.freeze())),
2870
1
        Some((mut file, len)) => {
2871
1
            file.flush()
2872
1
                .await
2873
1
                .err_tip(|| 
format!0
("Error flushing {what} spill file"))
?0
;
2874
1
            drop(file);
2875
1
            Ok(CapturedOutput::Spilled {
2876
1
                path: spill_path,
2877
1
                len,
2878
1
                head: buf.freeze(),
2879
1
            })
2880
        }
2881
    }
2882
92
}
2883
2884
/// Uploads captured output to the CAS, from memory or from its spill file.
2885
90
async fn upload_captured_output(
2886
90
    cas_store: Pin<&FastSlowStore>,
2887
90
    output: CapturedOutput,
2888
90
    hasher: DigestHasherFunc,
2889
90
    what: &str,
2890
90
) -> Result<DigestInfo, Error> {
2891
90
    match output {
2892
89
        CapturedOutput::Memory(data) => {
2893
89
            let digest = compute_buf_digest(&data, &mut hasher.hasher());
2894
89
            cas_store
2895
89
                .update_oneshot(digest, data)
2896
89
                .await
2897
87
                .err_tip(|| 
format!0
("Uploading {what}"))
?0
;
2898
87
            Ok(digest)
2899
        }
2900
1
        CapturedOutput::Spilled { path, len, .. } => {
2901
1
            let file = fs::open_file(&path, 0, u64::MAX)
2902
1
                .await
2903
1
                .err_tip(|| 
format!0
("Could not open {what} spill file {}",
path.display()0
))
?0
;
2904
1
            let (digest, file) = hasher
2905
1
                .hasher()
2906
1
                .digest_for_file(&path, file.into_inner(), Some(len))
2907
1
                .await
2908
1
                .err_tip(|| 
format!0
("Failed to hash {what} spill file {}",
path.display()0
))
?0
;
2909
1
            cas_store
2910
1
                .update_with_whole_file(
2911
1
                    digest,
2912
1
                    path.clone().into_os_string(),
2913
1
                    file,
2914
1
                    UploadSizeInfo::ExactSize(len),
2915
1
                )
2916
1
                .await
2917
1
                .err_tip(|| 
format!0
("Uploading {what} from {}",
path.display()0
))
?0
;
2918
1
            Ok(digest)
2919
        }
2920
    }
2921
88
}
2922
2923
#[derive(Debug)]
2924
struct RunningActionImplExecutionResult {
2925
    stdout: CapturedOutput,
2926
    stderr: CapturedOutput,
2927
    exit_code: i32,
2928
    resource_usage: Option<ActionResourceUsage>,
2929
}
2930
2931
#[derive(Debug)]
2932
struct RunningActionImplState {
2933
    command_proto: Option<ProtoCommand>,
2934
    // TODO(palfrey) Kill is not implemented yet, but is instrumented.
2935
    // However, it is used if the worker disconnects to destroy current jobs.
2936
    kill_channel_tx: Option<oneshot::Sender<()>>,
2937
    kill_channel_rx: Option<oneshot::Receiver<()>>,
2938
    execution_result: Option<RunningActionImplExecutionResult>,
2939
    action_result: Option<ActionResult>,
2940
    resource_usage: Option<ActionResourceUsage>,
2941
    /// Input bytes of the fetch attempt that succeeded, and the cached part.
2942
    input_bytes: (u64, u64),
2943
    execution_metadata: ExecutionMetadata,
2944
    // If there was an internal error, this will be set.
2945
    // This should NOT be set if everything was fine, but the process had a
2946
    // non-zero exit code. Instead this should be used for internal errors
2947
    // that prevented the action from running, upload failures, timeouts, exc...
2948
    // but we have (or could have) the action results (like stderr/stdout).
2949
    error: Option<Error>,
2950
    input_mounts: Option<Arc<InputMounts>>,
2951
}
2952
2953
#[derive(Debug)]
2954
pub struct RunningActionImpl {
2955
    operation_id: OperationId,
2956
    action_directory: String,
2957
    work_directory: String,
2958
    action_info: ActionInfo,
2959
    /// `Some` only when the manager was configured with
2960
    /// `experimental_active_input_leases`; `None` leaves eviction untouched.
2961
    input_lease: Option<Arc<ActionInputLease>>,
2962
    timeout: Duration,
2963
    running_actions_manager: Arc<RunningActionsManagerImpl>,
2964
    /// Set on a kill; every phase that can wait watches it, so a kill ends
2965
    /// a fetch or an upload as surely as it ends the command.
2966
    kill_token: watch::Sender<bool>,
2967
    state: Mutex<RunningActionImplState>,
2968
    has_manager_entry: AtomicBool,
2969
    did_cleanup: AtomicBool,
2970
    /// The sampler's latest memory figure and the pressure monitor's kill
2971
    /// request; see [`ActionMemoryWatch`].
2972
    memory_watch: Arc<ActionMemoryWatch>,
2973
}
2974
2975
impl RunningActionImpl {
2976
    /// The memory the sampler last saw this action hold, 0 before a sample.
2977
3
    pub fn last_memory_kb(&self) -> u64 {
2978
3
        self.memory_watch.last_memory_kb()
2979
3
    }
2980
2981
    /// Asks the action's sampler to end it for worker memory pressure,
2982
    /// naming the `full avg10` percent that chose it (never 0). Takes
2983
    /// effect on the sampler's next pass; a second request changes nothing.
2984
2
    pub fn request_memory_pressure_kill(&self, full_avg10_percent: u64) {
2985
2
        self.memory_watch
2986
2
            .kill_requested_percent
2987
2
            .store(full_avg10_percent.max(1), Ordering::Relaxed);
2988
2
    }
2989
2990
278
    pub fn new(
2991
278
        execution_metadata: ExecutionMetadata,
2992
278
        operation_id: OperationId,
2993
278
        action_directory: String,
2994
278
        action_info: ActionInfo,
2995
278
        timeout: Duration,
2996
278
        running_actions_manager: Arc<RunningActionsManagerImpl>,
2997
278
    ) -> Self {
2998
278
        let work_directory = format!("{}/{}", action_directory, "work");
2999
278
        let input_lease = running_actions_manager.active_input_leases.then(|| 
{9
3000
9
            ActionInputLease::new(
3001
9
                running_actions_manager.cas_store.as_ref(),
3002
9
                action_info.command_digest,
3003
9
                action_info.input_root_digest,
3004
            )
3005
9
        });
3006
278
        let (kill_channel_tx, kill_channel_rx) = oneshot::channel();
3007
278
        let (kill_token, _) = watch::channel(false);
3008
278
        Self {
3009
278
            kill_token,
3010
278
            operation_id,
3011
278
            action_directory,
3012
278
            work_directory,
3013
278
            action_info,
3014
278
            input_lease,
3015
278
            timeout,
3016
278
            running_actions_manager,
3017
278
            state: Mutex::new(RunningActionImplState {
3018
278
                command_proto: None,
3019
278
                kill_channel_rx: Some(kill_channel_rx),
3020
278
                kill_channel_tx: Some(kill_channel_tx),
3021
278
                execution_result: None,
3022
278
                action_result: None,
3023
278
                resource_usage: None,
3024
278
                input_bytes: (0, 0),
3025
278
                execution_metadata,
3026
278
                error: None,
3027
278
                input_mounts: None,
3028
278
            }),
3029
278
            // Set to true only after the action is inserted into the manager.
3030
278
            // The constructor can fail the operation-id uniqueness check
3031
278
            // immediately after this object is created.
3032
278
            has_manager_entry: AtomicBool::new(false),
3033
278
            // Only needs to be cleaned up after a prepare_action call, set there.
3034
278
            did_cleanup: AtomicBool::new(true),
3035
278
            memory_watch: Arc::new(ActionMemoryWatch::default()),
3036
278
        }
3037
278
    }
3038
3039
    #[allow(
3040
        clippy::missing_const_for_fn,
3041
        reason = "False positive on stable, but not on nightly"
3042
    )]
3043
0
    fn metrics(&self) -> &Arc<Metrics> {
3044
0
        &self.running_actions_manager.metrics
3045
0
    }
3046
3047
    /// Prepares any actions needed to execute this action. This action will do the following:
3048
    ///
3049
    /// * Download any files needed to execute the action
3050
    /// * Build a folder with all files needed to execute the action.
3051
    ///
3052
    /// This function will aggressively download and spawn potentially thousands of futures. It is
3053
    /// up to the stores to rate limit if needed.
3054
    /// Fetches the Command and the input tree into the work directory,
3055
    /// again after a transient store failure while `deadline` (none:
3056
    /// unbounded) leaves room for another attempt. A worker that already
3057
    /// holds part of the tree in its fast store finishes the job here rather
3058
    /// than handing the action back for another worker to fetch everything
3059
    /// against the same struggling store. The two fetches are retried
3060
    /// together: a Command read that fails during the same storm would
3061
    /// otherwise fail the action while the inputs were still being retried.
3062
    /// A `prefetched_command`, read earlier to choose the read-only input
3063
    /// mounts, is used instead of reading the Command again.
3064
70
    async fn fetch_with_retry(
3065
70
        &self,
3066
70
        filesystem_store: Pin<&FilesystemStore>,
3067
70
        deadline: Option<Instant>,
3068
70
        prefetched_command: Option<ProtoCommand>,
3069
70
    ) -> Result<ProtoCommand, Error> {
3070
70
        let started = Instant::now();
3071
70
        let mut attempt: u32 = 1;
3072
70
        let mut retries = PrepareRetries::new(record_prepare_outcome);
3073
        // Bytes earlier attempts fetched from the CAS. The next attempt finds
3074
        // them in the local store, so they are taken back off its cached part.
3075
70
        let mut fetched_earlier: u64 = 0;
3076
        loop {
3077
86
            let command_fut = async {
3078
86
                match &prefetched_command {
3079
8
                    Some(command) => Ok(command.clone()),
3080
78
                    None => self.fetch_command().await,
3081
                }
3082
86
            };
3083
86
            let stats = Arc::new(InputFetchStats::default());
3084
86
            let inputs_fut = INPUT_FETCH_STATS.scope(
3085
86
                stats.clone(),
3086
86
                self.metrics()
3087
86
                    .download_to_directory
3088
86
                    .wrap(self.download_inputs(filesystem_store)),
3089
            );
3090
86
            let 
result84
= try_join(command_fut, inputs_fut).await;
3091
84
            let (total, cached) = stats.bytes();
3092
21
            let 
err20
= match result {
3093
63
                Ok((command, ())) => {
3094
63
                    self.state.lock().input_bytes = (total, cached.saturating_sub(fetched_earlier));
3095
63
                    if attempt > 1 {
3096
5
                        info!(
3097
                            operation_id = %self.operation_id,
3098
                            attempt,
3099
5
                            elapsed_s = started.elapsed().as_secs(),
3100
                            "prepare_action: input fetch recovered"
3101
                        );
3102
58
                    }
3103
63
                    retries.settle("recovered");
3104
63
                    return Ok(command);
3105
                }
3106
21
                Err(
err20
) if is_transient_fetch_error(&err
)20
=>
err20
,
3107
1
                Err(err) => {
3108
1
                    retries.settle("failed");
3109
1
                    return Err(err);
3110
                }
3111
            };
3112
20
            fetched_earlier = fetched_earlier.saturating_add(total - cached);
3113
20
            self.running_actions_manager
3114
20
                .suggest_input_download_pool(&self.operation_id, &err);
3115
20
            let pause = prepare_retry_pause(attempt, &self.action_info.input_root_digest);
3116
20
            if deadline.is_some_and(|deadline| Instant::now() + pause >= deadline) {
3117
3
                retries.settle("exhausted");
3118
3
                return Err(err
3119
3
                    .append(format!(
3120
3
                        "Input fetch of operation {} failed {attempt} time(s) over {}s; the next retry would start after max_download_timeout",
3121
3
                        self.operation_id,
3122
3
                        started.elapsed().as_secs()
3123
3
                    ))
3124
3
                    .append(STORE_UNREACHABLE_ERROR_TIP));
3125
17
            }
3126
17
            warn!(
3127
                operation_id = %self.operation_id,
3128
                attempt,
3129
17
                wait_ms = pause.as_millis(),
3130
                error = %err,
3131
                "prepare_action: input fetch failed, retrying"
3132
            );
3133
17
            retries.retried();
3134
            // Start the next attempt from an empty directory: the
3135
            // directories and links of the partial tree would otherwise
3136
            // fail it with AlreadyExists.
3137
17
            if let Err(
err0
) = self.clear_work_directory().await {
3138
0
                retries.settle("failed");
3139
0
                return Err(err);
3140
17
            }
3141
17
            tokio::time::sleep(pause).await;
3142
16
            attempt += 1;
3143
        }
3144
67
    }
3145
3146
    /// Empties the work directory between two fetch attempts.
3147
17
    
async fn clear_work_directory(&self) -> Result<(), Error>0
{
3148
17
        if let Err(
remove_err0
) = fs::remove_dir_all(&self.work_directory).await
3149
0
            && remove_err.code != Code::NotFound
3150
        {
3151
0
            return Err(remove_err.append(format!(
3152
0
                "Failed to clear work directory {} before fetching the inputs again",
3153
0
                self.work_directory
3154
0
            )));
3155
17
        }
3156
17
        fs::create_dir(&self.work_directory)
3157
17
            .await
3158
17
            .err_tip(|| 
format!0
("Error recreating work directory {}", self.work_directory))
3159
17
    }
3160
3161
    /// Fetches and decodes the action's Command.
3162
86
    async fn fetch_command(&self) -> Result<ProtoCommand, Error> {
3163
86
        self.metrics()
3164
86
            .get_proto_command_from_store
3165
86
            .wrap(async {
3166
86
                get_and_decode_digest::<ProtoCommand>(
3167
86
                    self.running_actions_manager.cas_store.as_ref(),
3168
86
                    self.action_info.command_digest.into(),
3169
86
                )
3170
86
                .await
3171
86
                .err_tip(|| "Converting command_digest to Command")
3172
86
            })
3173
86
            .await
3174
86
    }
3175
3176
    /// Chooses the read-only input mounts for this action and keeps them in
3177
    /// its state, where their cache pins are held through cleanup. Whether
3178
    /// mounts apply depends on the Command's output paths, so with mounts
3179
    /// configured the Command is read here and returned for the fetch to
3180
    /// reuse; without them nothing is read, and the Command and inputs are
3181
    /// fetched concurrently. A failure here only means ordinary input setup:
3182
    /// the fetch that follows reads the Command and inputs with its retries.
3183
70
    
async fn select_input_mounts(&self) -> Option<ProtoCommand>0
{
3184
70
        let manager = &self.running_actions_manager;
3185
        #[cfg(target_os = "linux")]
3186
70
        let mount_paths = &manager.readonly_input_mounts;
3187
        #[cfg(not(target_os = "linux"))]
3188
        let mount_paths: &[String] = &[];
3189
70
        if mount_paths.is_empty() {
3190
62
            return None;
3191
8
        }
3192
8
        let command = if action_supports_persistent_workers(&self.action_info).is_none() {
3193
8
            match self.fetch_command().await {
3194
8
                Ok(command) => Some(command),
3195
0
                Err(error) => {
3196
0
                    warn!(
3197
                        ?error,
3198
                        "Reading the Command to choose input mounts failed; using ordinary input setup"
3199
                    );
3200
0
                    None
3201
                }
3202
            }
3203
        } else {
3204
0
            None
3205
        };
3206
8
        let mut input_mounts = match (&command, &manager.directory_cache) {
3207
8
            (Some(command), Some(cache)) => InputMounts::for_command(
3208
8
                cache.clone(),
3209
8
                mount_paths,
3210
8
                Path::new(&self.work_directory),
3211
8
                command,
3212
            ),
3213
0
            _ => None,
3214
        };
3215
8
        if input_mounts.is_some() {
3216
7
            match crate::input_mounts::has_mountable_inputs(
3217
7
                &manager.cas_store,
3218
7
                self.action_info.input_root_digest,
3219
7
                mount_paths,
3220
7
                self.input_lease
3221
7
                    .as_deref()
3222
7
                    .map(|lease| -> &dyn crate::directory_cache::DigestLease { lease }),
3223
            )
3224
7
            .await
3225
            {
3226
7
                Ok(true) => {}
3227
0
                Ok(false) => input_mounts = None,
3228
0
                Err(error) => {
3229
0
                    warn!(
3230
                        ?error,
3231
                        "Checking for mountable inputs failed; using ordinary input setup"
3232
                    );
3233
0
                    input_mounts = None;
3234
                }
3235
            }
3236
1
        }
3237
8
        if input_mounts.is_none() {
3238
1
            INPUT_MOUNT_METRICS.fallbacks.add(1, &[]);
3239
7
        }
3240
8
        self.state.lock().input_mounts.clone_from(&input_mounts);
3241
8
        command
3242
70
    }
3243
3244
    /// Materializes the input tree into the work directory: through the
3245
    /// read-only input mounts chosen for this action, which leave a mount
3246
    /// point for each selected subtree, or else through the directory cache
3247
    /// when one is configured. If preparing the mounts fails, they are
3248
    /// dropped and the tree is downloaded without them; later attempts of
3249
    /// the fetch then take the ordinary path.
3250
86
    async fn download_inputs(&self, filesystem_store: Pin<&FilesystemStore>) -> Result<(), Error> {
3251
86
        let manager = &self.running_actions_manager;
3252
86
        let downloads = &manager.input_downloads;
3253
86
        let input_mounts = self.state.lock().input_mounts.clone();
3254
86
        if input_mounts.is_none() {
3255
79
            return prepare_action_inputs_with_lease(
3256
79
                &manager.directory_cache,
3257
79
                &manager.cas_store,
3258
79
                filesystem_store,
3259
79
                &self.action_info.input_root_digest,
3260
79
                &self.work_directory,
3261
79
                self.input_lease.clone(),
3262
79
                downloads,
3263
79
            )
3264
79
            .await;
3265
7
        }
3266
7
        let result = download_to_directory_with_mounts(
3267
7
            &manager.cas_store,
3268
7
            filesystem_store,
3269
7
            &self.action_info.input_root_digest,
3270
7
            &self.work_directory,
3271
7
            self.input_lease.clone(),
3272
7
            input_mounts,
3273
7
            downloads,
3274
7
        )
3275
7
        .await;
3276
7
        if let Err(
error1
) = result {
3277
1
            INPUT_MOUNT_METRICS.prepare_failures.add(1, &[]);
3278
1
            INPUT_MOUNT_METRICS.fallbacks.add(1, &[]);
3279
1
            warn!(
3280
                ?error,
3281
                "Input mount preparation failed; retrying without mounts"
3282
            );
3283
            // No child has started. Discard partial inputs before
3284
            // releasing pins and retrying the ordinary download once.
3285
1
            fs::remove_dir_all(&self.work_directory).await
?0
;
3286
1
            self.state.lock().input_mounts.take();
3287
1
            fs::create_dir(&self.work_directory).await
?0
;
3288
1
            download_to_directory_with_mounts(
3289
1
                &manager.cas_store,
3290
1
                filesystem_store,
3291
1
                &self.action_info.input_root_digest,
3292
1
                &self.work_directory,
3293
1
                self.input_lease.clone(),
3294
1
                None,
3295
1
                downloads,
3296
1
            )
3297
1
            .await
?0
;
3298
6
        }
3299
7
        Ok(())
3300
84
    }
3301
3302
0
    async fn inner_prepare_action(
3303
0
        self: Arc<Self>,
3304
0
        download_deadline: Option<Instant>,
3305
72
    ) -> Result<Arc<Self>, Error> {
3306
        // Before anything lands on disk: an action that reserved more than
3307
        // is free is refused as backpressure, not run into ENOSPC halfway
3308
        // through its inputs. Free means free after what the actions
3309
        // already admitted reserved: four 4 GiB actions dispatched in one
3310
        // second all see the same 10 GiB, so the disk alone would admit
3311
        // them all. The reservation is held until the action's cleanup.
3312
72
        if let Some(
property6
) = self
3313
72
            .running_actions_manager
3314
72
            .execution_configuration
3315
72
            .resource_enforcement
3316
72
            .as_ref()
3317
72
            .and_then(|enforcement| 
enforcement.disk_property_name10
.
as_deref10
())
3318
6
            && let Some(
reserved_kb5
) = self
3319
6
                .action_info
3320
6
                .platform_properties
3321
6
                .get(property)
3322
6
                .and_then(|value| 
value.parse::<u64>()5
.
ok5
())
3323
5
            && reserved_kb > 0
3324
5
            && let Some(free_kb) = free_disk_kb(&self.running_actions_manager.root_action_directory)
3325
        {
3326
            // Held across the check and the insert, so two actions
3327
            // preparing at once cannot both pass on the same free space.
3328
5
            let mut reservations = self.running_actions_manager.disk_reservations_kb.lock();
3329
5
            let reserved_by_others_kb: u64 = reservations
3330
5
                .iter()
3331
5
                .filter(|(operation_id, _)| 
**operation_id1
!=
self.operation_id1
)
3332
5
                .map(|(_, kb)| *kb)
3333
5
                .sum();
3334
5
            if free_kb.saturating_sub(reserved_by_others_kb) < reserved_kb {
3335
2
                self.metrics().disk_guard_refusals.inc();
3336
2
                return Err(make_err!(
3337
2
                    Code::ResourceExhausted,
3338
2
                    "Not enough free disk for this action: reserved {reserved_kb} KiB ({property}), {free_kb} KiB free under {} with {reserved_by_others_kb} KiB reserved by the actions already admitted",
3339
2
                    self.running_actions_manager.root_action_directory
3340
2
                ));
3341
3
            }
3342
3
            reservations.insert(self.operation_id.clone(), reserved_kb);
3343
67
        }
3344
70
        {
3345
70
            let mut state = self.state.lock();
3346
70
            state.execution_metadata.input_fetch_start_timestamp =
3347
70
                (self.running_actions_manager.callbacks.now_fn)();
3348
70
        }
3349
63
        let command = {
3350
70
            let filesystem_store_pin =
3351
70
                Pin::new(self.running_actions_manager.filesystem_store.as_ref());
3352
70
            let prefetched_command = self.select_input_mounts().await;
3353
70
            fs::create_dir(&self.work_directory)
3354
70
                .await
3355
70
                .err_tip(|| 
format!0
("Error creating work directory {}",
self.work_directory0
))
?0
;
3356
            // Now the work directory has been created, we have to clean up.
3357
70
            self.did_cleanup.store(false, Ordering::Release);
3358
            // Fetch and decode the Command and download the input tree into
3359
            // the work directory, retried together after a store failure.
3360
70
            self.fetch_with_retry(filesystem_store_pin, download_deadline, prefetched_command)
3361
70
                .await
3362
67
                .map_err(tag_missing_input)
?4
3363
        };
3364
        {
3365
            // Create all directories needed for our output paths. This is required by the bazel spec.
3366
63
            let prepare_output_directories = |output_file| 
{27
3367
27
                let full_output_path = if command.working_directory.is_empty() {
3368
11
                    format!("{}/{}", self.work_directory, output_file)
3369
                } else {
3370
16
                    format!(
3371
                        "{}/{}/{}",
3372
16
                        self.work_directory, command.working_directory, output_file
3373
                    )
3374
                };
3375
27
                async move {
3376
27
                    let full_parent_path = Path::new(&full_output_path)
3377
27
                        .parent()
3378
27
                        .err_tip(|| 
format!0
("Parent path for {full_output_path} has no parent"))
?0
;
3379
27
                    fs::create_dir_all(full_parent_path).await.err_tip(|| 
{0
3380
0
                        format!(
3381
                            "Error creating output directory {} (file)",
3382
0
                            full_parent_path.display()
3383
                        )
3384
0
                    })?;
3385
27
                    Result::<(), Error>::Ok(())
3386
27
                }
3387
27
            };
3388
63
            self.metrics()
3389
63
                .prepare_output_files
3390
63
                .wrap(try_join_all(
3391
63
                    command.output_files.iter().map(prepare_output_directories),
3392
63
                ))
3393
63
                .await
?0
;
3394
63
            self.metrics()
3395
63
                .prepare_output_paths
3396
63
                .wrap(try_join_all(
3397
63
                    command.output_paths.iter().map(prepare_output_directories),
3398
63
                ))
3399
63
                .await
?0
;
3400
        }
3401
63
        debug!(?command, "Worker received command");
3402
63
        {
3403
63
            let mut state = self.state.lock();
3404
63
            state.command_proto = Some(command);
3405
63
            state.execution_metadata.input_fetch_completed_timestamp =
3406
63
                (self.running_actions_manager.callbacks.now_fn)();
3407
63
        }
3408
63
        Ok(self)
3409
69
    }
3410
3411
51
    pub fn canonicalise_path(
3412
51
        self: &Arc<Self>,
3413
51
        arg: &OsStr,
3414
51
        working_directory: &String,
3415
51
    ) -> Result<PathBuf, Error> {
3416
        #[cfg(target_os = "linux")]
3417
51
        if let Some(
mounts5
) = self.state.lock().input_mounts.as_ref()
3418
5
            && let Some(
path2
) = mounts.executable_path(working_directory, arg)
?1
3419
        {
3420
2
            return Ok(path);
3421
48
        }
3422
        // If the program contains a slash, we treat it as a path and resolve it relative to the work directory.
3423
48
        Ok(if Path::new(arg).components().count() > 1 {
3424
14
            let canonical_path = PathBuf::from(&self.work_directory)
3425
14
                .join(working_directory)
3426
14
                .join(arg);
3427
14
            if cfg!(target_os = "windows") {
3428
                // Workaround for https://github.com/rust-lang/rust/issues/42869 using a windows-specific crate
3429
0
                dunce::canonicalize(canonical_path)
3430
            } else {
3431
14
                canonical_path.canonicalize()
3432
            }
3433
14
            .err_tip(|| 
{1
3434
1
                format!(
3435
                    "Could not canonicalize path for command root {}.",
3436
1
                    arg.to_string_lossy()
3437
                )
3438
1
            })?
3439
        } else {
3440
34
            PathBuf::from(arg)
3441
        })
3442
51
    }
3443
3444
    /// The environment an action's process starts with: the worker's
3445
    /// `additional_environment`, then the action's own variables on top,
3446
    /// and on Windows the `SystemRoot` and `PATH` defaults without which
3447
    /// nothing runs. With `per_action` the sources that belong to one
3448
    /// action (its timeout, a side channel file, its directory) are in and
3449
    /// the side channel file is reported through it; without it they are
3450
    /// left out, for a pooled process that serves many actions.
3451
48
    fn action_environment(
3452
48
        &self,
3453
48
        command_proto: &ProtoCommand,
3454
48
        requested_timeout: Duration,
3455
48
        mut per_action: Option<&mut Option<Cow<'_, OsStr>>>,
3456
48
    ) -> BTreeMap<String, String> {
3457
48
        let mut env = BTreeMap::new();
3458
48
        if let Some(
additional_environment2
) = &self
3459
48
            .running_actions_manager
3460
48
            .execution_configuration
3461
48
            .additional_environment
3462
        {
3463
2
            for (name, source) in additional_environment {
3464
2
                let value = match source {
3465
2
                    EnvironmentSource::Property(property) => self
3466
2
                        .action_info
3467
2
                        .platform_properties
3468
2
                        .get(property)
3469
2
                        .cloned()
3470
2
                        .unwrap_or_default(),
3471
0
                    EnvironmentSource::Value(value) => value.clone(),
3472
0
                    EnvironmentSource::FromEnvironment => env::var(name).unwrap_or_default(),
3473
                    EnvironmentSource::TimeoutMillis => {
3474
0
                        if per_action.is_none() {
3475
0
                            continue;
3476
0
                        }
3477
0
                        requested_timeout.as_millis().to_string()
3478
                    }
3479
                    EnvironmentSource::SideChannelFile => {
3480
0
                        let Some(side_channel_file) = per_action.as_mut() else {
3481
0
                            continue;
3482
                        };
3483
0
                        let file = format!("{}/{}", self.action_directory, Uuid::new_v4().simple());
3484
0
                        **side_channel_file = Some(Cow::Owned(file.clone().into()));
3485
0
                        file
3486
                    }
3487
                    EnvironmentSource::ActionDirectory => {
3488
0
                        if per_action.is_none() {
3489
0
                            continue;
3490
0
                        }
3491
0
                        self.action_directory.clone()
3492
                    }
3493
                };
3494
2
                env.insert(name.clone(), value);
3495
            }
3496
46
        }
3497
48
        for 
variable42
in &command_proto.environment_variables {
3498
42
            env.insert(variable.name.clone(), variable.value.clone());
3499
42
        }
3500
        // If SystemRoot is not set on windows we set it to default. Failing
3501
        // to do this causes all commands to fail.
3502
        #[cfg(target_family = "windows")]
3503
        {
3504
            if !env.keys().any(|name| name.to_uppercase() == "SYSTEMROOT") {
3505
                env.insert("SystemRoot".to_string(), "C:\\Windows".to_string());
3506
            }
3507
            if !env.keys().any(|name| name.to_uppercase() == "PATH") {
3508
                env.insert("PATH".to_string(), "C:\\Windows\\System32".to_string());
3509
            }
3510
        }
3511
48
        env
3512
48
    }
3513
3514
    /// What the scheduler's kill leaves behind for a pooled action, whether
3515
    /// it came while waiting for a worker process or while one served it.
3516
0
    fn record_persistent_worker_kill(&self, command_proto: ProtoCommand, command: &str) {
3517
0
        let mut state = self.state.lock();
3518
0
        state.error = Error::merge_option(
3519
0
            state.error.take(),
3520
0
            Some(Error::new(
3521
0
                Code::Cancelled,
3522
0
                format!("Persistent worker command '{command}' was killed by scheduler"),
3523
0
            )),
3524
        );
3525
0
        state.command_proto = Some(command_proto);
3526
0
        state.execution_result = Some(RunningActionImplExecutionResult {
3527
0
            stdout: Bytes::new().into(),
3528
0
            stderr: Bytes::new().into(),
3529
0
            exit_code: EXIT_CODE_FOR_SIGNAL,
3530
0
            resource_usage: None,
3531
0
        });
3532
0
        state.execution_metadata.execution_completed_timestamp =
3533
0
            (self.running_actions_manager.callbacks.now_fn)();
3534
0
    }
3535
3536
51
    async fn inner_execute(self: Arc<Self>) -> Result<Arc<Self>, Error> {
3537
50
        let (command_proto, mut kill_channel_rx) = {
3538
51
            let mut state = self.state.lock();
3539
51
            state.execution_metadata.execution_start_timestamp =
3540
51
                (self.running_actions_manager.callbacks.now_fn)();
3541
            (
3542
51
                state
3543
51
                    .command_proto
3544
51
                    .take()
3545
51
                    .err_tip(|| "Expected state to have command_proto in execute()")
?1
,
3546
50
                state
3547
50
                    .kill_channel_rx
3548
50
                    .take()
3549
50
                    .err_tip(|| "Expected state to have kill_channel_rx in execute()")
?0
3550
                    // This is important as we may be killed at any point.
3551
50
                    .fuse(),
3552
            )
3553
        };
3554
50
        if command_proto.arguments.is_empty() {
3555
0
            return Err(make_input_err!("No arguments provided in Command proto"));
3556
50
        }
3557
50
        let args: Vec<&OsStr> = if let Some(
entrypoint0
) = &self
3558
50
            .running_actions_manager
3559
50
            .execution_configuration
3560
50
            .entrypoint
3561
        {
3562
0
            core::iter::once(entrypoint.as_ref())
3563
0
                .chain(command_proto.arguments.iter().map(AsRef::as_ref))
3564
0
                .collect()
3565
        } else {
3566
50
            command_proto.arguments.iter().map(AsRef::as_ref).collect()
3567
        };
3568
50
        let command_directory = if command_proto.working_directory.is_empty() {
3569
16
            self.work_directory.clone()
3570
        } else {
3571
34
            format!(
3572
                "{}/{}",
3573
34
                self.work_directory, command_proto.working_directory
3574
            )
3575
        };
3576
        // TODO(palfrey): This should probably be in debug, but currently
3577
        //                    that's too busy and we often rely on this to
3578
        //                    figure out toolchain misconfiguration issues.
3579
        //                    De-bloat the `debug` level by using the `trace`
3580
        //                    level more effectively and adjust this.
3581
50
        info!(?args, %command_directory, "Executing command");
3582
3583
50
        let 
program48
= self
3584
50
            .canonicalise_path(args[0], &command_proto.working_directory)
3585
50
            .err_tip(|| 
format!2
("Canonicalisation failure. Command={args:#?}"))
?2
;
3586
48
        let requested_timeout = if self.action_info.timeout.is_zero() {
3587
41
            self.running_actions_manager.max_action_timeout
3588
        } else {
3589
7
            self.action_info.timeout
3590
        };
3591
48
        if self
3592
48
            .running_actions_manager
3593
48
            .execution_configuration
3594
48
            .persistent_workers
3595
48
            .enabled
3596
48
            && let Some(
wire_format_result2
) = action_supports_persistent_workers(&self.action_info)
3597
        {
3598
2
            match wire_format_result {
3599
2
                Ok(wire_format) => {
3600
2
                    let command_argv = os_args_to_strings(&args)
?0
;
3601
                    // What a one-shot action gets, but for the sources that
3602
                    // belong to one action: a process that serves many
3603
                    // cannot carry a timeout, a side channel file or an
3604
                    // action directory.
3605
2
                    let env = self.action_environment(&command_proto, requested_timeout, None);
3606
2
                    let key = WorkerKey::from_argv(&command_argv, wire_format)
?0
3607
2
                        .with_env(env.into_iter().collect());
3608
2
                    let request = WorkRequest {
3609
2
                        arguments: persistent_worker_request_arguments(&command_argv),
3610
2
                        inputs: Vec::<PersistentWorkerInput>::new(),
3611
2
                        request_id: 0,
3612
2
                        cancel: false,
3613
2
                        verbosity: 0,
3614
2
                        sandbox_dir: command_directory.clone(),
3615
2
                    };
3616
2
                    let worker_cwd =
3617
2
                        PathBuf::from(&self.running_actions_manager.root_action_directory);
3618
3619
                    // The wait for a process is bounded by the action's own
3620
                    // timeout as well as the pool's, and raced against the
3621
                    // kill: a cancelled action must not hold a slot, and a
3622
                    // short action cannot wait longer than it may run.
3623
2
                    let acquire_started = Instant::now();
3624
2
                    let acquire_bound = self
3625
2
                        .running_actions_manager
3626
2
                        .persistent_worker_pool
3627
2
                        .acquire_timeout()
3628
2
                        .min(self.timeout);
3629
2
                    let acquired = {
3630
2
                        let acquire_fut = self
3631
2
                            .running_actions_manager
3632
2
                            .persistent_worker_pool
3633
2
                            .acquire(key.clone(), &program, &worker_cwd);
3634
2
                        tokio::pin!(acquire_fut);
3635
2
                        tokio::select! {
3636
2
                            result = &mut acquire_fut => Some(result),
3637
2
                            _ = &mut kill_channel_rx => 
None0
,
3638
2
                            () = tokio::time::sleep(acquire_bound) => Some(Err(make_err!(
3639
0
                                Code::DeadlineExceeded,
3640
0
                                "No persistent worker came back within the action's remaining time ({acquire_bound:?})"
3641
0
                            ))),
3642
                        }
3643
                    };
3644
2
                    let Some(acquired) = acquired else {
3645
0
                        let command = args.join(OsStr::new(" ")).to_string_lossy().into_owned();
3646
0
                        self.record_persistent_worker_kill(command_proto, &command);
3647
0
                        return Ok(self);
3648
                    };
3649
2
                    match acquired {
3650
2
                        Ok(mut lease) => {
3651
2
                            self.metrics().persistent_worker_dispatches.inc();
3652
2
                            let timer = self.metrics().child_process.begin_timer();
3653
2
                            let execution_started = Instant::now();
3654
                            // What the pooled process and its children use
3655
                            // while they serve this request. The process is
3656
                            // shared, so nothing here is enforced against
3657
                            // the action's reservation; it is measured so
3658
                            // the sizing loop learns what the tool needs.
3659
                            // CPU is what the process spent from here on;
3660
                            // peak memory is the whole process, which holds
3661
                            // what earlier requests left in it.
3662
2
                            let sampler = lease.worker().pid().map(|pgid| {
3663
2
                                start_action_resource_usage_sampler(
3664
2
                                    pgid,
3665
2
                                    Ceilings {
3666
2
                                        memory_limit_kb: None,
3667
2
                                        disk_limit_kb: None,
3668
2
                                        over_limit_tx: None,
3669
2
                                        // A detached watch: the process
3670
2
                                        // outlives this action and serves
3671
2
                                        // others, so it is never a pressure
3672
2
                                        // kill's victim.
3673
2
                                        watch: Arc::new(ActionMemoryWatch::default()),
3674
2
                                    },
3675
                                    // Its outcome is classified against no limit.
3676
2
                                    None,
3677
2
                                    None,
3678
2
                                    self.running_actions_manager.process_table.clone(),
3679
                                )
3680
2
                            });
3681
2
                            let dispatch_result = {
3682
2
                                let remaining =
3683
2
                                    self.timeout.saturating_sub(acquire_started.elapsed());
3684
2
                                let dispatch_fut =
3685
2
                                    lease.worker().dispatch_with_timeout(&request, remaining);
3686
2
                                tokio::pin!(dispatch_fut);
3687
2
                                tokio::select! {
3688
2
                                    result = &mut dispatch_fut => Some(result),
3689
2
                                    _ = &mut kill_channel_rx => 
None0
,
3690
                                }
3691
                            };
3692
2
                            let sampled_usage = match sampler {
3693
2
                                Some(sampler) => finish_action_resource_usage_sampler(sampler)
3694
2
                                    .await
3695
2
                                    .unwrap_or_default(),
3696
0
                                None => SampledResourceUsage::default(),
3697
                            };
3698
2
                            let response = match dispatch_result {
3699
2
                                Some(Ok(response)) => {
3700
2
                                    lease.release(true).await;
3701
2
                                    response
3702
                                }
3703
0
                                Some(Err(err)) => {
3704
0
                                    lease.release(false).await;
3705
0
                                    return Err(err).err_tip(|| {
3706
0
                                        format!("Dispatching action to persistent worker {key:?}")
3707
0
                                    });
3708
                                }
3709
                                None => {
3710
0
                                    drop(timer);
3711
0
                                    lease.release(false).await;
3712
0
                                    let command =
3713
0
                                        args.join(OsStr::new(" ")).to_string_lossy().into_owned();
3714
0
                                    self.record_persistent_worker_kill(command_proto, &command);
3715
0
                                    return Ok(self);
3716
                                }
3717
                            };
3718
2
                            timer.measure();
3719
3720
2
                            if response.exit_code == 0 {
3721
2
                                self.metrics().child_process_success_error_code.inc();
3722
2
                            } else {
3723
0
                                self.metrics().child_process_failure_error_code.inc();
3724
0
                            }
3725
2
                            info!(?args, ?key, "Persistent worker command complete");
3726
2
                            let (memory_property, disk_property) = self
3727
2
                                .running_actions_manager
3728
2
                                .execution_configuration
3729
2
                                .resource_enforcement
3730
2
                                .as_ref()
3731
2
                                .map_or((None, None), |e| 
{0
3732
                                    (
3733
0
                                        e.memory.as_ref().map(|m| m.property_name.as_str()),
3734
0
                                        e.disk_property_name.as_deref(),
3735
                                    )
3736
0
                                });
3737
2
                            let (outcome, enforced) =
3738
2
                                classify_outcome(None, None, sampled_usage.last_memory_kb, None);
3739
2
                            let fetched_input = self.state.lock().input_bytes;
3740
2
                            let resource_usage = Some(ActionResourceUsage {
3741
2
                                peak_memory_kb: sampled_usage.peak_memory_kb,
3742
2
                                cpu_time_ms: sampled_usage.cpu_time_ms,
3743
2
                                sampled: sampled_usage.peak_memory_kb > 0
3744
0
                                    || sampled_usage.cpu_time_ms > 0,
3745
2
                                operation_id: String::new(),
3746
2
                                worker_id: String::new(),
3747
2
                                wall_time_ms: u64::try_from(
3748
2
                                    execution_started.elapsed().as_millis(),
3749
                                )
3750
2
                                .unwrap_or(u64::MAX),
3751
                                peak_disk_kb: 0,
3752
2
                                outcome: outcome.into(),
3753
2
                                enforced,
3754
2
                                reserved: reservation_of(
3755
2
                                    &self.action_info.platform_properties,
3756
2
                                    memory_property,
3757
2
                                    disk_property,
3758
                                ),
3759
2
                                input_bytes: fetched_input.0,
3760
2
                                input_cached_bytes: fetched_input.1,
3761
2
                                ..Default::default()
3762
                            });
3763
2
                            {
3764
2
                                let mut state = self.state.lock();
3765
2
                                state.command_proto = Some(command_proto);
3766
2
                                state.execution_result = Some(RunningActionImplExecutionResult {
3767
2
                                    stdout: Bytes::new().into(),
3768
2
                                    stderr: Bytes::from(response.output).into(),
3769
2
                                    exit_code: response.exit_code,
3770
2
                                    resource_usage,
3771
2
                                });
3772
2
                                state.execution_metadata.execution_completed_timestamp =
3773
2
                                    (self.running_actions_manager.callbacks.now_fn)();
3774
2
                            }
3775
2
                            return Ok(self);
3776
                        }
3777
0
                        Err(err) => {
3778
0
                            self.metrics().persistent_worker_fallbacks.inc();
3779
0
                            info!(
3780
                                ?err,
3781
                                ?key,
3782
                                "Falling back to one-shot execution; persistent worker unavailable"
3783
                            );
3784
                        }
3785
                    }
3786
                }
3787
0
                Err(err) => {
3788
0
                    self.metrics().persistent_worker_fallbacks.inc();
3789
0
                    info!(
3790
                        ?err,
3791
                        "Falling back to one-shot execution; unsupported persistent worker protocol"
3792
                    );
3793
                }
3794
            }
3795
46
        }
3796
3797
46
        let mut command_builder = process::Command::new(program);
3798
        #[cfg(target_family = "unix")]
3799
46
        command_builder.arg0(args[0]);
3800
46
        command_builder
3801
46
            .args(&args[1..])
3802
46
            .kill_on_drop(true)
3803
46
            .stdin(Stdio::null())
3804
46
            .stdout(Stdio::piped())
3805
46
            .stderr(Stdio::piped())
3806
46
            .current_dir(command_directory.clone())
3807
46
            .env_clear();
3808
3809
46
        if self
3810
46
            .running_actions_manager
3811
46
            .execution_configuration
3812
46
            .set_tmpdir
3813
        {
3814
            // One tmp per action, removed with the action directory, so two
3815
            // actions writing the same name under $TMPDIR no longer collide.
3816
            // Code that hardcodes /tmp still does.
3817
1
            let tmp_directory = format!("{}/tmp", self.action_directory);
3818
1
            fs::create_dir_all(&tmp_directory)
3819
1
                .await
3820
1
                .err_tip(|| 
format!0
("Creating {tmp_directory} for TMPDIR"))
?0
;
3821
1
            command_builder.env("TMPDIR", &tmp_directory);
3822
45
        }
3823
3824
46
        let mut maybe_side_channel_file: Option<Cow<'_, OsStr>> = None;
3825
46
        for (
name42
,
value42
) in self.action_environment(
3826
46
            &command_proto,
3827
46
            requested_timeout,
3828
46
            Some(&mut maybe_side_channel_file),
3829
46
        ) {
3830
42
            command_builder.env(name, value);
3831
42
        }
3832
3833
        // Sandboxing of the command if we are running on Linux, this resolves issues where
3834
        // children can spawn children and also provides better reproducibility.
3835
        #[cfg(target_os = "linux")]
3836
        {
3837
46
            let use_namespaces = self.running_actions_manager.use_namespaces;
3838
3839
46
            if !
matches!38
(use_namespaces, UseNamespaces::No) {
3840
38
                let (mount, isolate_tmp) = match use_namespaces {
3841
38
                    UseNamespaces::YesAndMount { isolate_tmp } => (true, isolate_tmp),
3842
0
                    UseNamespaces::No | UseNamespaces::Yes => (false, false),
3843
                };
3844
38
                let root_action_directory = std::ffi::CString::new(
3845
38
                    self.running_actions_manager.root_action_directory.clone(),
3846
                )
3847
38
                .err_tip(|| "In RunningActionImpl::inner_execute()")
?0
;
3848
38
                let action_directory = std::ffi::CString::new(self.action_directory.clone())
3849
38
                    .err_tip(|| "In RunningActionImpl::inner_execute()")
?0
;
3850
                // The action's private /tmp is its own tmp directory, the
3851
                // same one TMPDIR points at, bound over /tmp in the child.
3852
38
                let tmp_directory = if isolate_tmp {
3853
7
                    let tmp_directory = format!("{}/tmp", self.action_directory);
3854
7
                    fs::create_dir_all(&tmp_directory)
3855
7
                        .await
3856
7
                        .err_tip(|| 
format!0
("Creating {tmp_directory} for the private /tmp"))
?0
;
3857
                    Some(
3858
7
                        std::ffi::CString::new(tmp_directory)
3859
7
                            .err_tip(|| "In RunningActionImpl::inner_execute()")
?0
,
3860
                    )
3861
                } else {
3862
31
                    None
3863
                };
3864
3865
38
                let input_mounts = self.state.lock().input_mounts.clone();
3866
38
                let bind_mounts = input_mounts
3867
38
                    .as_ref()
3868
38
                    .map(|mounts| 
mounts4
.
bind_mounts4
())
3869
38
                    .transpose()
?0
3870
38
                    .unwrap_or_default();
3871
                // SAFETY: This function is specifically designed to operate in a async-signal-safe
3872
                // environment.
3873
                unsafe {
3874
38
                    command_builder.pre_exec(move || 
{0
3875
0
                        crate::namespace_utils::configure_namespace_with_input_mounts(
3876
0
                            mount,
3877
0
                            tmp_directory.as_deref(),
3878
0
                            &root_action_directory,
3879
0
                            &action_directory,
3880
0
                            &bind_mounts,
3881
                        )
3882
0
                    });
3883
                }
3884
8
            }
3885
        }
3886
        // Run the action as its own process-group leader (pgid == child
3887
        // pid) on every unix. The resource-usage sampler attributes memory
3888
        // by process group, and the kill at exit ends the group, so this
3889
        // keeps the whole action together, including processes reparented
3890
        // to the worker when an intermediate shell exits, and never
3891
        // conflates it with the worker's own group.
3892
        #[cfg(target_family = "unix")]
3893
46
        command_builder.process_group(0);
3894
3895
46
        let mut child_process = command_builder
3896
46
            .spawn()
3897
46
            .err_tip(|| 
format!0
("Could not execute command {args:?}"))
?0
;
3898
        // Ours until the wait below collects it; the reaper leaves it alone.
3899
46
        let _owned_child = crate::reaper::OwnedChild::new(child_process.id());
3900
46
        let stdout_reader = child_process
3901
46
            .stdout
3902
46
            .take()
3903
46
            .err_tip(|| "Expected stdout to exist on command this should never happen")
?0
;
3904
46
        let stderr_reader = child_process
3905
46
            .stderr
3906
46
            .take()
3907
46
            .err_tip(|| "Expected stderr to exist on command this should never happen")
?0
;
3908
3909
        #[cfg(target_os = "linux")]
3910
        // Wrap the child process to send SIGTERM rather than SIGKILL if namespaced to
3911
        // prevent zombie processes.
3912
46
        let child_process = crate::namespace_utils::MaybeNamespacedChild::new(
3913
38
            !matches!(
3914
46
                self.running_actions_manager.use_namespaces,
3915
                UseNamespaces::No,
3916
            ),
3917
46
            child_process,
3918
        );
3919
3920
        // The reservation the scheduler placed this action by, if the
3921
        // worker is asked to hold it to that number.
3922
46
        let memory_reservation = self
3923
46
            .running_actions_manager
3924
46
            .execution_configuration
3925
46
            .resource_enforcement
3926
46
            .as_ref()
3927
46
            .and_then(|enforcement| 
{6
3928
6
                let 
memory4
= enforcement.memory.as_ref()
?2
;
3929
4
                let reserved_kb = self
3930
4
                    .action_info
3931
4
                    .platform_properties
3932
4
                    .get(&memory.property_name)
?0
3933
4
                    .parse::<u64>()
3934
4
                    .ok()
?0
;
3935
4
                (reserved_kb > 0).then(|| {
3936
4
                    let limit_kb = reserved_kb.saturating_mul(100 + memory.headroom_percent) / 100;
3937
4
                    (reserved_kb, limit_kb)
3938
4
                })
3939
6
            });
3940
46
        self.memory_watch.reserved_memory_kb.store(
3941
46
            memory_reservation.map_or(0, |(reserved_kb, _)| reserved_kb),
3942
46
            Ordering::Relaxed,
3943
        );
3944
        // The disk reservation, held to only with `disk: soft`; disk is
3945
        // measured whenever the enforcement names a disk property.
3946
46
        let disk_enforcement = self
3947
46
            .running_actions_manager
3948
46
            .execution_configuration
3949
46
            .resource_enforcement
3950
46
            .as_ref()
3951
46
            .and_then(|enforcement| 
{6
3952
6
                let 
property2
= enforcement.disk_property_name.as_deref()
?4
;
3953
2
                let reserved_kb = self
3954
2
                    .action_info
3955
2
                    .platform_properties
3956
2
                    .get(property)
3957
2
                    .and_then(|value| 
value.parse::<u64>()1
.
ok1
())
3958
2
                    .unwrap_or(0);
3959
2
                let limit_kb = (enforcement.disk_soft && 
reserved_kb > 01
).then(||
{1
3960
1
                    reserved_kb.saturating_mul(100 + enforcement.disk_headroom_percent) / 100
3961
1
                });
3962
2
                Some((reserved_kb, limit_kb))
3963
6
            });
3964
46
        let (over_limit_tx, over_limit_rx) = oneshot::channel::<OverLimit>();
3965
        // Holds the sender when no ceiling is enforced, so the receiver
3966
        // below stays pending instead of resolving closed.
3967
46
        let mut over_limit_keepalive = Some(over_limit_tx);
3968
        // What a SIGKILL nobody here sent is judged against; the sampler
3969
        // keeps its figure exact near it.
3970
46
        let oom_limit_kb = memory_reservation
3971
46
            .map(|(_, limit_kb)| limit_kb)
3972
46
            .or(self.running_actions_manager.pod_memory_limit_kb);
3973
46
        let mut maybe_resource_usage_sampler = child_process.id().map(|pgid| {
3974
46
            let memory_limit_kb = memory_reservation.map(|(_, limit_kb)| limit_kb);
3975
46
            let disk_limit_kb = disk_enforcement.and_then(|(_, limit_kb)| limit_kb);
3976
46
            let pressure_kill_armed = self
3977
46
                .running_actions_manager
3978
46
                .execution_configuration
3979
46
                .resource_enforcement
3980
46
                .as_ref()
3981
46
                .is_some_and(|enforcement| 
enforcement.pressure_kill6
.
is_some6
());
3982
46
            let over_limit_tx =
3983
46
                (memory_limit_kb.is_some() || 
disk_limit_kb42
.
is_some42
() ||
pressure_kill_armed41
)
3984
46
                    .then(|| 
over_limit_keepalive5
.
take5
())
3985
46
                    .flatten();
3986
46
            let disk_directory = disk_enforcement.map(|_| 
{2
3987
2
                let directory = PathBuf::from(&self.action_directory);
3988
2
                let since = directory_stamp(&directory).unwrap_or_else(|err| 
{0
3989
0
                    warn!(?err, "Timing the disk walk by the wall clock instead");
3990
0
                    SystemTime::now()
3991
0
                });
3992
2
                (directory, since)
3993
2
            });
3994
46
            start_action_resource_usage_sampler(
3995
46
                pgid,
3996
46
                Ceilings {
3997
46
                    memory_limit_kb,
3998
46
                    disk_limit_kb,
3999
46
                    over_limit_tx,
4000
46
                    watch: self.memory_watch.clone(),
4001
46
                },
4002
46
                oom_limit_kb,
4003
46
                disk_directory,
4004
46
                self.running_actions_manager.process_table.clone(),
4005
            )
4006
46
        });
4007
46
        let mut over_limit_fut = over_limit_rx.fuse();
4008
4009
        // The group to end when the action's own process has exited; taken
4010
        // before the guard owns the child.
4011
        #[cfg(target_family = "unix")]
4012
46
        let action_pgid = child_process.id();
4013
46
        let mut child_process_guard = guard(child_process, |mut child_process| 
{0
4014
0
            let result: Result<Option<std::process::ExitStatus>, std::io::Error> =
4015
0
                child_process.try_wait();
4016
0
            match result {
4017
0
                Ok(res) if res.is_some() => {
4018
0
                    // The child already exited, probably a timeout or kill operation
4019
0
                }
4020
0
                result => {
4021
0
                    error!(
4022
                        ?result,
4023
                        "Child process was not cleaned up before dropping the call to execute(), killing in background spawn."
4024
                    );
4025
0
                    background_spawn!("running_actions_manager_kill_child_process", async move {
4026
0
                        drop(child_process.kill().await);
4027
0
                    });
4028
                }
4029
            }
4030
0
        });
4031
4032
46
        let output_cap = self
4033
46
            .running_actions_manager
4034
46
            .execution_configuration
4035
46
            .max_captured_output_bytes;
4036
46
        let stdout_spill = PathBuf::from(&self.action_directory).join("nativelink.stdout");
4037
46
        let stderr_spill = PathBuf::from(&self.action_directory).join("nativelink.stderr");
4038
        // Flipped when the pipes have had their drain after the action
4039
        // ended; the readers then keep what they have.
4040
46
        let (drain_stop_tx, drain_stop_rx) = watch::channel(false);
4041
46
        let stdout_stop = drain_stop_rx.clone();
4042
46
        let all_stdout_fut = spawn!("stdout_reader", async move {
4043
46
            capture_output(
4044
46
                stdout_reader,
4045
46
                output_cap,
4046
46
                stdout_spill,
4047
46
                "stdout",
4048
46
                stdout_stop,
4049
46
            )
4050
46
            .await
4051
46
        });
4052
46
        let all_stderr_fut = spawn!("stderr_reader", async move {
4053
46
            capture_output(
4054
46
                stderr_reader,
4055
46
                output_cap,
4056
46
                stderr_spill,
4057
46
                "stderr",
4058
46
                drain_stop_rx,
4059
46
            )
4060
46
            .await
4061
46
        });
4062
46
        let mut kill_reason: Option<KillReason> = None;
4063
46
        let kill_grace = self
4064
46
            .running_actions_manager
4065
46
            .execution_configuration
4066
46
            .kill_grace;
4067
        // Armed by a soft kill; fires SIGKILL if the action is still there
4068
        // when the grace period ends.
4069
46
        let mut grace_fut: Fuse<BoxFuture<'static, ()>> = Fuse::terminated();
4070
46
        let execution_started = std::time::Instant::now();
4071
4072
46
        let timer = self.metrics().child_process.begin_timer();
4073
46
        let mut sleep_fut = (self.running_actions_manager.callbacks.sleep_fn)(self.timeout).fuse();
4074
        loop {
4075
58
            tokio::select! {
4076
58
                () = &mut sleep_fut => {
4077
5
                    self.running_actions_manager.metrics.task_timeouts.inc();
4078
5
                    kill_reason = Some(KillReason::Timeout);
4079
5
                    grace_fut = stop_action(&mut child_process_guard, &self.memory_watch, kill_grace, "action timeout").await;
4080
                    {
4081
5
                        let joined_command = args.join(OsStr::new(" "));
4082
5
                        let command = joined_command.to_string_lossy();
4083
5
                        info!(
4084
5
                            seconds = self.action_info.timeout.as_secs_f32(),
4085
                            %command,
4086
                            %command_directory,
4087
                            "Command timed out"
4088
                        );
4089
5
                        let mut state = self.state.lock();
4090
5
                        state.error = Error::merge_option(state.error.take(), Some(Error::new(
4091
5
                            Code::DeadlineExceeded,
4092
5
                            format!(
4093
5
                                "Command '{}' timed out after {} seconds",
4094
5
                                command,
4095
5
                                self.action_info.timeout.as_secs_f32()
4096
5
                            )
4097
5
                        )));
4098
                    }
4099
                },
4100
58
                () = &mut grace_fut => {
4101
0
                    warn!(
4102
0
                        operation_id = ?self.operation_id,
4103
0
                        grace_ms = kill_grace.as_millis(),
4104
                        "Action did not leave on SIGTERM within its grace period, sending SIGKILL"
4105
                    );
4106
0
                    hard_kill(&mut child_process_guard, &self.memory_watch, "grace period expired").await;
4107
                },
4108
58
                Ok(
over_limit4
) = &mut over_limit_fut => match
over_limit4
{
4109
1
                    OverLimit::Disk(observed_kb) => {
4110
1
                        self.running_actions_manager.metrics.disk_reservation_kills.inc();
4111
1
                        kill_reason = Some(KillReason::Disk);
4112
                        // The files are still being written; no grace here.
4113
1
                        hard_kill(&mut child_process_guard, &self.memory_watch, "disk reservation").await;
4114
1
                        let (reserved_kb, limit_kb) = disk_enforcement
4115
1
                            .map_or((0, 0), |(reserved_kb, limit_kb)| {
4116
1
                                (reserved_kb, limit_kb.unwrap_or(0))
4117
1
                            });
4118
1
                        let (property, headroom) = self
4119
1
                            .running_actions_manager
4120
1
                            .execution_configuration
4121
1
                            .resource_enforcement
4122
1
                            .as_ref()
4123
1
                            .map_or(("", 0), |e| (e.disk_property_name.as_deref().unwrap_or(""), e.disk_headroom_percent));
4124
1
                        warn!(
4125
1
                            operation_id = ?self.operation_id,
4126
                            reserved_kb,
4127
                            limit_kb,
4128
                            observed_kb,
4129
                            "Action exceeded its disk reservation, killed"
4130
                        );
4131
1
                        let mut state = self.state.lock();
4132
1
                        state.error = Error::merge_option(state.error.take(), Some(Error::new(
4133
1
                            Code::FailedPrecondition,
4134
1
                            format!(
4135
1
                                "Action exceeded its disk reservation: reserved {reserved_kb} KiB ({property}), \
4136
1
                                 limit {limit_kb} KiB with {headroom}% headroom, its own files came to {observed_kb} KiB. \
4137
1
                                 Raise the reservation or write less."
4138
1
                            ),
4139
1
                        )));
4140
4141
                    }
4142
1
                    OverLimit::Memory(observed_kb) => {
4143
1
                        self.running_actions_manager.metrics.memory_reservation_kills.inc();
4144
1
                        kill_reason = Some(KillReason::Memory);
4145
                        // Memory is still growing; no grace here.
4146
1
                        hard_kill(&mut child_process_guard, &self.memory_watch, "memory reservation").await;
4147
1
                        let (reserved_kb, limit_kb) = memory_reservation.unwrap_or((0, 0));
4148
1
                        let (property, headroom) = self
4149
1
                            .running_actions_manager
4150
1
                            .execution_configuration
4151
1
                            .resource_enforcement
4152
1
                            .as_ref()
4153
1
                            .and_then(|e| e.memory.as_ref())
4154
1
                            .map_or(("", 0), |m| (m.property_name.as_str(), m.headroom_percent));
4155
1
                        warn!(
4156
1
                            operation_id = ?self.operation_id,
4157
                            reserved_kb,
4158
                            limit_kb,
4159
                            observed_kb,
4160
                            "Action exceeded its memory reservation, killed"
4161
                        );
4162
1
                        let mut state = self.state.lock();
4163
1
                        state.error = Error::merge_option(state.error.take(), Some(Error::new(
4164
1
                            Code::FailedPrecondition,
4165
1
                            format!(
4166
1
                                "Action exceeded its memory reservation: reserved {reserved_kb} KiB ({property}), \
4167
1
                                 limit {limit_kb} KiB with {headroom}% headroom, observed {observed_kb} KiB. \
4168
1
                                 Raise the reservation or shrink the action."
4169
1
                            ),
4170
1
                        )));
4171
4172
                    }
4173
2
                    OverLimit::MemoryPressure { observed_kb, full_avg10_percent } => {
4174
2
                        self.running_actions_manager.metrics.memory_pressure_kills.inc();
4175
2
                        kill_reason = Some(KillReason::Memory);
4176
                        // The worker is thrashing; no grace here either.
4177
2
                        hard_kill(&mut child_process_guard, &self.memory_watch, "memory pressure").await;
4178
2
                        let reserved_kb = memory_reservation.map_or(0, |(reserved_kb, _)| reserved_kb);
4179
2
                        let after_s = self
4180
2
                            .running_actions_manager
4181
2
                            .execution_configuration
4182
2
                            .resource_enforcement
4183
2
                            .as_ref()
4184
2
                            .and_then(|e| e.pressure_kill.as_ref())
4185
2
                            .map_or(0, |p| p.after.as_secs());
4186
2
                        warn!(
4187
2
                            operation_id = ?self.operation_id,
4188
                            reserved_kb,
4189
                            observed_kb,
4190
                            full_avg10_percent,
4191
                            "Action killed under sustained worker memory pressure"
4192
                        );
4193
2
                        let why = if observed_kb > reserved_kb {
4194
0
                            format!(
4195
                                "this action was the furthest over its reservation of those running, \
4196
                                 {observed_kb} KiB against {reserved_kb} KiB. The reservation is too small for it."
4197
                            )
4198
                        } else {
4199
2
                            format!(
4200
                                "the worker was out of memory and this action held the most of what was running, \
4201
                                 {observed_kb} KiB, within its reservation of {reserved_kb} KiB."
4202
                            )
4203
                        };
4204
2
                        let mut state = self.state.lock();
4205
2
                        state.error = Error::merge_option(state.error.take(), Some(Error::new(
4206
2
                            Code::FailedPrecondition,
4207
2
                            format!(
4208
2
                                "Action was killed under worker memory pressure: the worker's cgroup spent \
4209
2
                                 {full_avg10_percent}% of the last 10 s with every task stalled on memory for {after_s} s, \
4210
2
                                 and {why}"
4211
2
                            ),
4212
2
                        )));
4213
                    }
4214
                },
4215
58
                
maybe_exit_status46
= child_process_guard.wait() => {
4216
                    // Defuse our guard so it does not try to cleanup and make senseless logs.
4217
46
                    drop(ScopeGuard::<_, _>::into_inner(child_process_guard));
4218
46
                    drop(over_limit_keepalive.take());
4219
46
                    let exit_status = maybe_exit_status.err_tip(|| "Failed to collect exit code of process")
?0
;
4220
                    // TODO(palfrey) We should implement stderr/stdout streaming to client here.
4221
                    // If we get killed before the stream is started, then these will lock up.
4222
46
                    let killed_action = kill_reason.is_some();
4223
46
                    if killed_action {
4224
12
                        drop(timer);
4225
34
                    } else {
4226
34
                        timer.measure();
4227
34
                    }
4228
                    // The action is over; anything still running in its
4229
                    // group is a straggler (a background job, a helper that
4230
                    // daemonized) holding the pipes open, and without this
4231
                    // the action would sit here until its timeout. The
4232
                    // sandboxes Bazel runs locally end them the same way.
4233
                    #[cfg(target_family = "unix")]
4234
46
                    let had_stragglers = action_pgid
4235
46
                        .is_some_and(|pgid| signal_action(pgid, &self.memory_watch, libc::SIGKILL));
4236
                    #[cfg(not(target_family = "unix"))]
4237
                    let had_stragglers = false;
4238
                    // An action that exited on its own with nothing left in
4239
                    // its group closes its pipes itself, and its output is
4240
                    // the result, so that read runs to the end: a reader
4241
                    // slowed by a busy spill must not lose bytes to a clock.
4242
                    // The wait is bounded only when there was something to
4243
                    // hold the pipes: a killed action, whose output is a
4244
                    // bonus, or stragglers, one of which may have left the
4245
                    // group. What was read by then is kept.
4246
46
                    let mut drain = core::pin::pin!(async { tokio::join!(all_stdout_fut, all_stderr_fut) });
4247
46
                    let (maybe_all_stdout, maybe_all_stderr) = if killed_action || 
had_stragglers34
{
4248
12
                        tokio::select! {
4249
12
                            outputs = &mut drain => outputs,
4250
12
                            () = tokio::time::sleep(OUTPUT_DRAIN_TIMEOUT) => {
4251
0
                                warn!(
4252
0
                                    operation_id = ?self.operation_id,
4253
0
                                    drain_s = OUTPUT_DRAIN_TIMEOUT.as_secs(),
4254
                                    killed_action,
4255
                                    had_stragglers,
4256
                                    "A process held the action's output pipes open past the drain; keeping what was read"
4257
                                );
4258
0
                                drain_stop_tx.send_replace(true);
4259
0
                                drain.await
4260
                            }
4261
                        }
4262
                    } else {
4263
34
                        drain.await
4264
                    };
4265
46
                    let (stdout, stderr) = (
4266
46
                        maybe_all_stdout.err_tip(|| "Internal error reading from stdout of worker task")
?0
?0
,
4267
46
                        maybe_all_stderr.err_tip(|| "Internal error reading from stderr of worker task")
?0
?0
,
4268
                    );
4269
4270
46
                    let sampled_usage = match maybe_resource_usage_sampler.take() {
4271
46
                        Some(sampler) => finish_action_resource_usage_sampler(sampler)
4272
46
                            .await
4273
46
                            .unwrap_or_default(),
4274
0
                        None => SampledResourceUsage::default(),
4275
                    };
4276
4277
46
                    let memory_limit_kb = oom_limit_kb;
4278
                    // The signal that ended the process, where the platform reports one.
4279
                    #[cfg(target_family = "unix")]
4280
46
                    let signal = exit_status.signal();
4281
                    #[cfg(not(target_family = "unix"))]
4282
                    let signal = None;
4283
46
                    let (outcome, enforced) = classify_outcome(
4284
46
                        kill_reason,
4285
46
                        signal,
4286
46
                        sampled_usage.last_memory_kb,
4287
46
                        memory_limit_kb,
4288
46
                    );
4289
4290
46
                    let exit_code = exit_status.code().map_or_else(|| 
{11
4291
11
                        self.metrics().child_process_failure_error_code.inc();
4292
11
                        if kill_reason.is_none() {
4293
                            // No exit code and no kill of ours: a signal from
4294
                            // outside. SIGKILL near the memory limit is the
4295
                            // kernel's OOM killer.
4296
0
                            warn!(
4297
                                ?args,
4298
0
                                outcome = outcome.as_str_name(),
4299
                                last_memory_kb = sampled_usage.last_memory_kb,
4300
                                ?memory_limit_kb,
4301
                                "Runner subprocess terminated by a signal nobody here sent"
4302
                            );
4303
11
                        }
4304
11
                        EXIT_CODE_FOR_SIGNAL
4305
35
                    
}11
, |exit_code| {
4306
35
                        if exit_code == 0 {
4307
32
                            self.metrics().child_process_success_error_code.inc();
4308
32
                        } else {
4309
3
                            self.metrics().child_process_failure_error_code.inc();
4310
3
                        }
4311
35
                        exit_code
4312
35
                    });
4313
46
                    if kill_reason.is_none() && 
outcome == ResourceOutcome::KilledMemory34
{
4314
0
                        let mut state = self.state.lock();
4315
0
                        state.error = Error::merge_option(state.error.take(), Some(Error::new(
4316
0
                            Code::FailedPrecondition,
4317
0
                            format!(
4318
0
                                "Action was killed by the kernel at {} KiB, within 10% of the {} KiB memory limit: \
4319
0
                                 out of memory. Raise the reservation or shrink the action.",
4320
0
                                sampled_usage.last_memory_kb,
4321
0
                                memory_limit_kb.unwrap_or(0),
4322
0
                            ),
4323
0
                        )));
4324
46
                    }
4325
4326
46
                    let (memory_property, disk_property) = self
4327
46
                        .running_actions_manager
4328
46
                        .execution_configuration
4329
46
                        .resource_enforcement
4330
46
                        .as_ref()
4331
46
                        .map_or((None, None), |e| 
{6
4332
                            (
4333
6
                                e.memory.as_ref().map(|m| 
m.property_name4
.
as_str4
()),
4334
6
                                e.disk_property_name.as_deref(),
4335
                            )
4336
6
                        });
4337
46
                    let fetched_input = self.state.lock().input_bytes;
4338
46
                    let resource_usage = Some(ActionResourceUsage {
4339
46
                        peak_memory_kb: sampled_usage.peak_memory_kb,
4340
46
                        cpu_time_ms: sampled_usage.cpu_time_ms,
4341
                        // An action too short to catch a sample leaves both at
4342
                        // zero; say so rather than report a misleading zero.
4343
46
                        sampled: sampled_usage.peak_memory_kb > 0
4344
14
                            || sampled_usage.cpu_time_ms > 0
4345
14
                            || sampled_usage.peak_disk_kb > 0,
4346
46
                        operation_id: String::new(),
4347
46
                        worker_id: String::new(),
4348
46
                        wall_time_ms: u64::try_from(execution_started.elapsed().as_millis()).unwrap_or(u64::MAX),
4349
46
                        peak_disk_kb: sampled_usage.peak_disk_kb,
4350
46
                        outcome: outcome.into(),
4351
46
                        enforced,
4352
46
                        reserved: reservation_of(
4353
46
                            &self.action_info.platform_properties,
4354
46
                            memory_property,
4355
46
                            disk_property,
4356
                        ),
4357
46
                        input_bytes: fetched_input.0,
4358
46
                        input_cached_bytes: fetched_input.1,
4359
                        // Phases and outputs are known only once the
4360
                        // result is uploaded; `with_phases` fills them.
4361
46
                        ..Default::default()
4362
                    });
4363
4364
                    // log something useful instead of repeating same ?arg
4365
46
                    info!(?exit_code, outcome = outcome.as_str_name(), "Command complete");
4366
4367
46
                    let maybe_error_override = if let Some(
side_channel_file0
) = maybe_side_channel_file {
4368
0
                        process_side_channel_file(side_channel_file.clone(), &args, requested_timeout).await
4369
0
                        .err_tip(|| format!("Error processing side channel file: {}", side_channel_file.display()))?
4370
                    } else {
4371
46
                        None
4372
                    };
4373
46
                    {
4374
46
                        let mut state = self.state.lock();
4375
46
                        state.error = Error::merge_option(state.error.take(), maybe_error_override);
4376
46
4377
46
                        state.command_proto = Some(command_proto);
4378
46
                        state.execution_result = Some(RunningActionImplExecutionResult{
4379
46
                            stdout,
4380
46
                            stderr,
4381
46
                            exit_code,
4382
46
                            resource_usage,
4383
46
                        });
4384
46
                        state.execution_metadata.execution_completed_timestamp = (self.running_actions_manager.callbacks.now_fn)();
4385
46
                    }
4386
46
                    return Ok(self);
4387
                },
4388
58
                _ = &mut kill_channel_rx => {
4389
3
                    kill_reason = Some(KillReason::External);
4390
3
                    grace_fut = stop_action(&mut child_process_guard, &self.memory_watch, kill_grace, "scheduler kill").await;
4391
3
                    {
4392
3
                        let mut state = self.state.lock();
4393
3
                        state.error = Error::merge_option(state.error.take(), Some(Error::new(
4394
3
                            Code::Aborted,
4395
3
                            format!(
4396
3
                                "Command '{}' was killed by scheduler",
4397
3
                                args.join(OsStr::new(" ")).to_string_lossy()
4398
3
                            )
4399
3
                        )));
4400
3
                    }
4401
                },
4402
            }
4403
        }
4404
        // Unreachable.
4405
51
    }
4406
4407
45
    
async fn inner_upload_results(self: Arc<Self>) -> Result<Arc<Self>, Error>0
{
4408
        enum OutputType {
4409
            None,
4410
            File(FileInfo),
4411
            Directory(DirectoryInfo),
4412
            FileSymlink(SymlinkInfo),
4413
            DirectorySymlink(SymlinkInfo),
4414
        }
4415
4416
45
        let upload_start = std::time::Instant::now();
4417
45
        debug!(
4418
45
            operation_id = ?self.operation_id,
4419
            "Worker uploading results - starting",
4420
        );
4421
45
        let (mut command_proto, execution_result, mut execution_metadata) = {
4422
45
            let mut state = self.state.lock();
4423
45
            state.execution_metadata.output_upload_start_timestamp =
4424
45
                (self.running_actions_manager.callbacks.now_fn)();
4425
            (
4426
45
                state
4427
45
                    .command_proto
4428
45
                    .take()
4429
45
                    .err_tip(|| "Expected state to have command_proto in execute()")
?0
,
4430
45
                state
4431
45
                    .execution_result
4432
45
                    .take()
4433
45
                    .err_tip(|| "Execution result does not exist at upload_results stage")
?0
,
4434
45
                state.execution_metadata.clone(),
4435
            )
4436
        };
4437
45
        let cas_store = self.running_actions_manager.cas_store.as_ref();
4438
45
        let hasher = self.action_info.unique_qualifier.digest_function();
4439
4440
45
        let mut output_path_futures: Vec<BoxFuture<'_, Result<OutputType, Error>>> = Vec::new();
4441
45
        let mut output_paths = command_proto.output_paths;
4442
45
        if output_paths.is_empty() {
4443
25
            output_paths
4444
25
                .reserve(command_proto.output_files.len() + command_proto.output_directories.len());
4445
25
            output_paths.append(&mut command_proto.output_files);
4446
25
            output_paths.append(&mut command_proto.output_directories);
4447
25
        
}20
4448
45
        let digest_uploaders = Arc::new(Mutex::new(HashMap::new()));
4449
45
        let uploads = Arc::new(Semaphore::new(UPLOAD_CONCURRENCY));
4450
45
        for 
entry23
in output_paths {
4451
23
            let full_path = OsString::from(if command_proto.working_directory.is_empty() {
4452
8
                format!("{}/{}", self.work_directory, entry)
4453
            } else {
4454
15
                format!(
4455
                    "{}/{}/{}",
4456
15
                    self.work_directory, command_proto.working_directory, entry
4457
                )
4458
            });
4459
23
            let work_directory = &self.work_directory;
4460
23
            let digest_uploaders = digest_uploaders.clone();
4461
23
            let uploads = uploads.clone();
4462
23
            output_path_futures.push(
4463
23
                async move {
4464
8
                    let metadata = {
4465
23
                        let metadata = match fs::symlink_metadata(&full_path).await {
4466
23
                            Ok(file) => file,
4467
0
                            Err(e) => {
4468
0
                                if e.code == Code::NotFound {
4469
                                    // In the event our output does not exist, according to the bazel remote
4470
                                    // execution spec, we simply ignore it continue.
4471
0
                                    return Result::<OutputType, Error>::Ok(OutputType::None);
4472
0
                                }
4473
0
                                return Err(e).err_tip(|| {
4474
0
                                    format!("Could not open file {}", full_path.display())
4475
0
                                });
4476
                            }
4477
                        };
4478
4479
23
                        if metadata.is_file() {
4480
                            return Ok(OutputType::File(
4481
15
                                upload_file(
4482
15
                                    cas_store.as_pin(),
4483
15
                                    &full_path,
4484
15
                                    hasher,
4485
15
                                    metadata,
4486
15
                                    digest_uploaders,
4487
15
                                    uploads,
4488
15
                                )
4489
15
                                .await
4490
15
                                .map(|mut file_info| {
4491
15
                                    file_info.name_or_path = NameOrPath::Path(entry);
4492
15
                                    file_info
4493
15
                                })
4494
15
                                .err_tip(|| 
format!0
("Uploading file {}",
full_path.display()0
))
?0
,
4495
                            ));
4496
8
                        }
4497
8
                        metadata
4498
                    };
4499
8
                    if metadata.is_dir() {
4500
                        Ok(OutputType::Directory(
4501
4
                            upload_directory(
4502
4
                                cas_store.as_pin(),
4503
4
                                &full_path,
4504
4
                                work_directory,
4505
4
                                hasher,
4506
4
                                digest_uploaders,
4507
4
                                uploads,
4508
                            )
4509
4
                            .and_then(|(root_dir, children)| async move {
4510
4
                                let tree = ProtoTree {
4511
4
                                    root: Some(root_dir),
4512
4
                                    children: children.into(),
4513
4
                                };
4514
4
                                let tree_digest = serialize_and_upload_message(
4515
4
                                    &tree,
4516
4
                                    cas_store.as_pin(),
4517
4
                                    &mut hasher.hasher(),
4518
4
                                )
4519
4
                                .await
4520
4
                                .err_tip(|| 
format!0
("While processing {entry}"))
?0
;
4521
4
                                Ok(DirectoryInfo {
4522
4
                                    path: entry,
4523
4
                                    tree_digest,
4524
4
                                })
4525
8
                            })
4526
4
                            .await
4527
4
                            .err_tip(|| 
format!0
("Uploading directory {}",
full_path.display()0
))
?0
,
4528
                        ))
4529
4
                    } else if metadata.is_symlink() {
4530
                        // Resolve the symlink to determine what it points to.
4531
                        // Symlinks created by DirectoryCache (absolute paths into
4532
                        // the cache directory) must NOT be uploaded as symlinks —
4533
                        // the target path is worker-local and meaningless to the
4534
                        // client. Instead, follow the symlink and upload the
4535
                        // resolved content (file or directory).
4536
4
                        let target = fs::read_link(&full_path).await.err_tip(|| 
{0
4537
0
                            format!("Reading symlink target for {}", full_path.display())
4538
0
                        })?;
4539
4
                        let is_absolute_symlink = Path::new(&target).is_absolute();
4540
4541
4
                        if is_absolute_symlink {
4542
                            // Absolute symlink — resolve and upload contents.
4543
3
                            match fs::metadata(&full_path).await {
4544
3
                                Ok(resolved_meta) => {
4545
3
                                    if resolved_meta.is_dir() {
4546
                                        // Upload as directory (Tree proto).
4547
                                        Ok(OutputType::Directory(
4548
1
                                            upload_directory(
4549
1
                                                cas_store.as_pin(),
4550
1
                                                &full_path,
4551
1
                                                work_directory,
4552
1
                                                hasher,
4553
1
                                                digest_uploaders,
4554
1
                                                uploads,
4555
                                            )
4556
1
                                            .and_then(|(root_dir, children)| async move {
4557
1
                                                let tree = ProtoTree {
4558
1
                                                    root: Some(root_dir),
4559
1
                                                    children: children.into(),
4560
1
                                                };
4561
1
                                                let tree_digest = serialize_and_upload_message(
4562
1
                                                    &tree,
4563
1
                                                    cas_store.as_pin(),
4564
1
                                                    &mut hasher.hasher(),
4565
1
                                                )
4566
1
                                                .await
4567
1
                                                .err_tip(|| 
format!0
("While processing {entry}"))
?0
;
4568
1
                                                Ok(DirectoryInfo {
4569
1
                                                    path: entry,
4570
1
                                                    tree_digest,
4571
1
                                                })
4572
2
                                            })
4573
1
                                            .await
4574
1
                                            .err_tip(
4575
0
                                                || {
4576
0
                                                    format!(
4577
                                                        "Uploading symlinked directory {}",
4578
0
                                                        full_path.display()
4579
                                                    )
4580
0
                                                },
4581
0
                                            )?,
4582
                                        ))
4583
                                    } else {
4584
                                        // Upload as file (follow symlink).
4585
                                        Ok(OutputType::File(
4586
2
                                            upload_file(
4587
2
                                                cas_store.as_pin(),
4588
2
                                                &full_path,
4589
2
                                                hasher,
4590
2
                                                resolved_meta,
4591
2
                                                digest_uploaders,
4592
2
                                                uploads,
4593
2
                                            )
4594
2
                                            .await
4595
2
                                            .map(|mut file_info| {
4596
2
                                                file_info.name_or_path = NameOrPath::Path(entry);
4597
2
                                                file_info
4598
2
                                            })
4599
2
                                            .err_tip(
4600
0
                                                || {
4601
0
                                                    format!(
4602
                                                        "Uploading symlinked file {}",
4603
0
                                                        full_path.display()
4604
                                                    )
4605
0
                                                },
4606
0
                                            )?,
4607
                                        ))
4608
                                    }
4609
                                }
4610
0
                                Err(e) => {
4611
0
                                    if e.code != Code::NotFound {
4612
0
                                        return Err(e).err_tip(|| {
4613
0
                                            format!(
4614
                                                "While resolving absolute symlink {}",
4615
0
                                                full_path.display()
4616
                                            )
4617
0
                                        });
4618
0
                                    }
4619
0
                                    Ok(OutputType::None)
4620
                                }
4621
                            }
4622
                        } else {
4623
                            // Relative symlink — action intentionally created it.
4624
                            // Upload as a proper symlink.
4625
1
                            let output_symlink = upload_symlink(&full_path, work_directory)
4626
1
                                .await
4627
1
                                .map(|mut symlink_info| {
4628
1
                                    symlink_info.name_or_path = NameOrPath::Path(entry);
4629
1
                                    symlink_info
4630
1
                                })
4631
1
                                .err_tip(|| 
format!0
("Uploading symlink {}",
full_path.display()0
))
?0
;
4632
1
                            match fs::metadata(&full_path).await {
4633
1
                                Ok(metadata) => {
4634
1
                                    if metadata.is_dir() {
4635
0
                                        Ok(OutputType::DirectorySymlink(output_symlink))
4636
                                    } else {
4637
                                        // Note: If it's anything but directory we put it as a file symlink.
4638
1
                                        Ok(OutputType::FileSymlink(output_symlink))
4639
                                    }
4640
                                }
4641
0
                                Err(e) => {
4642
0
                                    if e.code != Code::NotFound {
4643
0
                                        return Err(e).err_tip(|| {
4644
0
                                            format!(
4645
                                                "While querying target symlink metadata for {}",
4646
0
                                                full_path.display()
4647
                                            )
4648
0
                                        });
4649
0
                                    }
4650
                                    // If the file doesn't exist, we consider it a file. Even though the
4651
                                    // file doesn't exist we still need to populate an entry.
4652
0
                                    Ok(OutputType::FileSymlink(output_symlink))
4653
                                }
4654
                            }
4655
                        }
4656
                    } else {
4657
0
                        Err(make_err!(
4658
0
                            Code::Internal,
4659
0
                            "{full_path:?} was not a file, folder or symlink. Must be one.",
4660
0
                        ))
4661
                    }
4662
23
                }
4663
23
                .boxed(),
4664
            );
4665
        }
4666
45
        let mut output_files = vec![];
4667
45
        let mut output_folders = vec![];
4668
45
        let mut output_directory_symlinks = vec![];
4669
45
        let mut output_file_symlinks = vec![];
4670
4671
45
        if execution_result.exit_code != 0 {
4672
14
            error!(
4673
                exit_code = ?execution_result.exit_code,
4674
14
                stdout = ?log_excerpt(execution_result.stdout.head(), 1000),
4675
14
                stderr = ?log_excerpt(execution_result.stderr.head(), 1000),
4676
                command = ?command_proto.arguments,
4677
                "Command returned non-zero exit code",
4678
            );
4679
31
        }
4680
4681
45
        let stdout_digest_fut = self.metrics().upload_stdout.wrap(async {
4682
45
            let start = std::time::Instant::now();
4683
45
            let data = execution_result.stdout;
4684
45
            let data_len = data.len();
4685
            // Boxed: the upload future is large, and two of them inline
4686
            // pushed the debug test binary past its thread stack.
4687
45
            let 
digest44
= Box::pin(upload_captured_output(
4688
45
                cas_store.as_pin(),
4689
45
                data,
4690
45
                hasher,
4691
45
                "stdout",
4692
45
            ))
4693
45
            .await
?0
;
4694
44
            debug!(
4695
                ?digest,
4696
                data_len,
4697
44
                elapsed_ms = start.elapsed().as_millis(),
4698
                "upload_results: stdout upload completed",
4699
            );
4700
44
            Result::<DigestInfo, Error>::Ok(digest)
4701
44
        });
4702
45
        let stderr_digest_fut = self.metrics().upload_stderr.wrap(async {
4703
45
            let start = std::time::Instant::now();
4704
45
            let data = execution_result.stderr;
4705
45
            let data_len = data.len();
4706
            // Boxed: the upload future is large, and two of them inline
4707
            // pushed the debug test binary past its thread stack.
4708
45
            let 
digest44
= Box::pin(upload_captured_output(
4709
45
                cas_store.as_pin(),
4710
45
                data,
4711
45
                hasher,
4712
45
                "stderr",
4713
45
            ))
4714
45
            .await
?0
;
4715
44
            debug!(
4716
                ?digest,
4717
                data_len,
4718
44
                elapsed_ms = start.elapsed().as_millis(),
4719
                "upload_results: stderr upload completed",
4720
            );
4721
44
            Result::<DigestInfo, Error>::Ok(digest)
4722
44
        });
4723
4724
45
        debug!(
4725
45
            operation_id = ?self.operation_id,
4726
45
            num_output_paths = output_path_futures.len(),
4727
            "upload_results: starting stdout/stderr/output_paths uploads",
4728
        );
4729
45
        let mut output_path_futures =
4730
45
            futures::stream::iter(output_path_futures).buffer_unordered(UPLOAD_CONCURRENCY);
4731
45
        let join_start = std::time::Instant::now();
4732
45
        let 
upload_result44
= futures::try_join!(stdout_digest_fut, stderr_digest_fut, async {
4733
68
            while let Some(
output_type23
) = output_path_futures.try_next().await
?0
{
4734
23
                match output_type {
4735
17
                    OutputType::File(output_file) => output_files.push(output_file),
4736
5
                    OutputType::Directory(output_folder) => output_folders.push(output_folder),
4737
1
                    OutputType::FileSymlink(output_symlink) => {
4738
1
                        output_file_symlinks.push(output_symlink);
4739
1
                    }
4740
0
                    OutputType::DirectorySymlink(output_symlink) => {
4741
0
                        output_directory_symlinks.push(output_symlink);
4742
0
                    }
4743
0
                    OutputType::None => { /* Safe to ignore */ }
4744
                }
4745
            }
4746
45
            Ok(())
4747
45
        });
4748
44
        drop(output_path_futures);
4749
44
        debug!(
4750
44
            operation_id = ?self.operation_id,
4751
44
            elapsed_ms = join_start.elapsed().as_millis(),
4752
44
            success = upload_result.is_ok(),
4753
            "upload_results: all uploads completed",
4754
        );
4755
44
        let resource_usage = execution_result.resource_usage.clone();
4756
44
        let (stdout_digest, stderr_digest) = match upload_result {
4757
44
            Ok((stdout_digest, stderr_digest, ())) => (stdout_digest, stderr_digest),
4758
0
            Err(e) => return Err(e).err_tip(|| "Error while uploading results"),
4759
        };
4760
4761
44
        execution_metadata.output_upload_completed_timestamp =
4762
44
            (self.running_actions_manager.callbacks.now_fn)();
4763
44
        output_files.sort_unstable_by(|a, b| 
a.name_or_path0
.
cmp0
(
&b.name_or_path0
));
4764
44
        output_folders.sort_unstable_by(|a, b| 
a.path0
.
cmp0
(
&b.path0
));
4765
44
        output_file_symlinks.sort_unstable_by(|a, b| 
a.name_or_path0
.
cmp0
(
&b.name_or_path0
));
4766
44
        output_directory_symlinks.sort_unstable_by(|a, b| 
a.name_or_path0
.
cmp0
(
&b.name_or_path0
));
4767
44
        let num_output_files = output_files.len();
4768
44
        let num_output_folders = output_folders.len();
4769
44
        {
4770
44
            let mut state = self.state.lock();
4771
44
            execution_metadata.worker_completed_timestamp =
4772
44
                (self.running_actions_manager.callbacks.now_fn)();
4773
44
            state.action_result = Some(ActionResult {
4774
44
                output_files,
4775
44
                output_folders,
4776
44
                output_directory_symlinks,
4777
44
                output_file_symlinks,
4778
44
                exit_code: execution_result.exit_code,
4779
44
                stdout_digest,
4780
44
                stderr_digest,
4781
44
                execution_metadata,
4782
44
                server_logs: HashMap::default(), // TODO(palfrey) Not implemented.
4783
44
                error: state.error.clone(),
4784
44
                message: String::new(), // Will be filled in on cache_action_result if needed.
4785
44
            });
4786
44
            state.resource_usage = resource_usage;
4787
44
        }
4788
44
        debug!(
4789
44
            operation_id = ?self.operation_id,
4790
44
            total_elapsed_ms = upload_start.elapsed().as_millis(),
4791
            num_output_files,
4792
            num_output_folders,
4793
            "upload_results: inner_upload_results completed successfully",
4794
        );
4795
44
        Ok(self)
4796
44
    }
4797
4798
44
    async fn inner_get_finished_result(self: Arc<Self>) -> Result<ActionResult, Error> {
4799
44
        let mut state = self.state.lock();
4800
44
        state
4801
44
            .action_result
4802
44
            .take()
4803
44
            .err_tip(|| "Expected action_result to exist in get_finished_result")
4804
44
    }
4805
}
4806
4807
impl Drop for RunningActionImpl {
4808
278
    fn drop(&mut self) {
4809
278
        if self.did_cleanup.load(Ordering::Acquire) {
4810
276
            if self.has_manager_entry.load(Ordering::Acquire) {
4811
2
                drop(
4812
2
                    self.running_actions_manager
4813
2
                        .cleanup_action(&self.operation_id),
4814
2
                );
4815
274
            }
4816
276
            if let Some(
input_lease8
) = self.input_lease.clone() {
4817
8
                background_spawn!(
4818
                    "running_action_impl_release_input_lease",
4819
8
                    input_lease.release()
4820
                );
4821
268
            }
4822
276
            return;
4823
2
        }
4824
2
        let operation_id = self.operation_id.clone();
4825
2
        error!(
4826
            %operation_id,
4827
            "RunningActionImpl did not cleanup. This is a violation of the requirements, will attempt to do it in the background."
4828
        );
4829
2
        let running_actions_manager = self.running_actions_manager.clone();
4830
2
        let action_directory = self.action_directory.clone();
4831
2
        let input_lease = self.input_lease.clone();
4832
2
        let input_mounts = self.state.get_mut().input_mounts.take();
4833
2
        background_spawn!("running_action_impl_drop", async move 
{1
4834
1
            let cleanup_result =
4835
1
                do_cleanup(&running_actions_manager, &operation_id, &action_directory).await;
4836
1
            drop(input_mounts);
4837
1
            if let Some(input_lease) = input_lease {
4838
1
                input_lease.release().await;
4839
0
            }
4840
1
            if let Err(
err0
) = cleanup_result {
4841
0
                error!(
4842
                    %operation_id,
4843
                    ?action_directory,
4844
                    ?err,
4845
                    "Error cleaning up action"
4846
                );
4847
1
            }
4848
1
        });
4849
278
    }
4850
}
4851
4852
impl RunningAction for RunningActionImpl {
4853
0
    fn get_operation_id(&self) -> &OperationId {
4854
0
        &self.operation_id
4855
0
    }
4856
4857
72
    async fn prepare_action(self: Arc<Self>) -> Result<Arc<Self>, Error> {
4858
72
        let download_timeout = self.running_actions_manager.max_download_timeout;
4859
72
        let operation_id = self.operation_id.clone();
4860
72
        let manager = self.running_actions_manager.clone();
4861
72
        let metrics = self.metrics().clone();
4862
72
        let mut kill_rx = self.kill_token.subscribe();
4863
        // The retry loop inside stops short of this; the timeout below is
4864
        // the backstop for an attempt that never answers. The retries are
4865
        // also bounded by the action's own timeout: the scheduler ends the
4866
        // action soon after that passes, whatever the worker is still
4867
        // fetching. A window too far out to represent leaves the retries
4868
        // unbounded, as the timeout is.
4869
72
        let action_timeout = self.action_info.timeout;
4870
72
        let retry_window = if action_timeout.is_zero() {
4871
64
            download_timeout
4872
        } else {
4873
8
            download_timeout.min(action_timeout)
4874
        };
4875
72
        let download_deadline = Instant::now().checked_add(retry_window);
4876
        // Boxed: the prepare state machine is large, and pinning it on the
4877
        // stack next to the select below overflowed a test thread.
4878
72
        let mut prepare_fut = Box::pin(
4879
72
            metrics
4880
72
                .prepare_action
4881
72
                .wrap(Self::inner_prepare_action(self, download_deadline)),
4882
        );
4883
        // A fetch that never answers would otherwise hold the slot for good
4884
        // while the worker's keepalives say it is fine: name it every minute
4885
        // and give up at the timeout, so the scheduler can retry elsewhere.
4886
72
        let stall_warn_fut = async {
4887
72
            let mut elapsed_secs = 0u64;
4888
            loop {
4889
72
                tokio::time::sleep(Duration::from_mins(1)).await;
4890
0
                elapsed_secs += 60;
4891
0
                warn!(
4892
                    ?operation_id,
4893
                    elapsed_s = elapsed_secs,
4894
0
                    timeout_s = download_timeout.as_secs(),
4895
                    "prepare_action: still fetching inputs, possible stall",
4896
                );
4897
            }
4898
        };
4899
72
        let res = tokio::time::timeout(download_timeout, async {
4900
72
            tokio::pin!(stall_warn_fut);
4901
72
            tokio::select! {
4902
72
                
result69
= &mut prepare_fut =>
result69
,
4903
72
                _ = kill_rx.wait_for(|killed| *killed) => {
4904
2
                    warn!(%operation_id, "prepare_action: killed while fetching inputs");
4905
2
                    Err(make_err!(
4906
2
                        Code::Aborted,
4907
2
                        "Operation {operation_id} was killed while fetching its inputs"
4908
2
                    ))
4909
                }
4910
72
                () = &mut stall_warn_fut => 
unreachable!0
(),
4911
            }
4912
71
        })
4913
72
        .await
4914
72
        .map_err(|_| 
{1
4915
1
            warn!(
4916
                %operation_id,
4917
1
                timeout_s = download_timeout.as_secs(),
4918
                "prepare_action: input fetch timed out"
4919
            );
4920
1
            let err = make_err!(
4921
1
                Code::DeadlineExceeded,
4922
                "Fetching the inputs of operation {operation_id} took longer than max_download_timeout ({}s)",
4923
1
                download_timeout.as_secs(),
4924
            );
4925
1
            manager.suggest_input_download_pool(&operation_id, &err);
4926
1
            err
4927
1
        })
4928
72
        .and_then(|res| res);
4929
72
        if let Err(
ref e9
) = res {
4930
9
            warn!(?e, "Error during prepare_action");
4931
63
        }
4932
72
        res
4933
72
    }
4934
4935
51
    async fn execute(self: Arc<Self>) -> Result<Arc<Self>, Error> {
4936
51
        let res = self
4937
51
            .metrics()
4938
51
            .clone()
4939
51
            .execute
4940
51
            .wrap(Self::inner_execute(self))
4941
51
            .await;
4942
51
        if let Err(
ref e3
) = res {
4943
3
            warn!(?e, "Error during prepare_action");
4944
48
        }
4945
51
        res
4946
51
    }
4947
4948
45
    
async fn upload_results(self: Arc<Self>) -> Result<Arc<Self>, Error>0
{
4949
45
        let upload_timeout = self.running_actions_manager.max_upload_timeout;
4950
45
        let operation_id = self.operation_id.clone();
4951
45
        info!(
4952
            ?operation_id,
4953
45
            upload_timeout_s = upload_timeout.as_secs(),
4954
            "upload_results: starting with timeout",
4955
        );
4956
45
        let metrics = self.metrics().clone();
4957
45
        let kill_token = self.kill_token.clone();
4958
45
        let upload_fut = metrics
4959
45
            .upload_results
4960
45
            .wrap(Self::inner_upload_results(self));
4961
4962
45
        let stall_warn_fut = async 
{38
4963
38
            let mut elapsed_secs = 0u64;
4964
            loop {
4965
38
                tokio::time::sleep(Duration::from_mins(1)).await;
4966
0
                elapsed_secs += 60;
4967
0
                warn!(
4968
                    ?operation_id,
4969
                    elapsed_s = elapsed_secs,
4970
0
                    timeout_s = upload_timeout.as_secs(),
4971
                    "upload_results: still in progress — possible stall",
4972
                );
4973
            }
4974
        };
4975
4976
        // Only a kill that arrives during the upload ends it: a kill that
4977
        // already ended the command leaves a killed result that still has to
4978
        // reach the scheduler, so the token's current state is taken as seen
4979
        // and only a change from here on aborts.
4980
45
        let mut kill_rx = kill_token.subscribe();
4981
45
        kill_rx.borrow_and_update();
4982
45
        let res = tokio::time::timeout(upload_timeout, async {
4983
45
            tokio::pin!(upload_fut);
4984
45
            tokio::pin!(stall_warn_fut);
4985
45
            tokio::select! {
4986
45
                
result44
= &mut upload_fut =>
result44
,
4987
45
                Ok(()) = kill_rx.changed() => {
4988
1
                    warn!(%operation_id, "upload_results: killed while uploading");
4989
1
                    Err(make_err!(
4990
1
                        Code::Aborted,
4991
1
                        "Operation {operation_id} was killed while uploading its results"
4992
1
                    ))
4993
                }
4994
45
                () = &mut stall_warn_fut => 
unreachable!0
(),
4995
            }
4996
45
        })
4997
45
        .await
4998
45
        .map_err(|err| 
{0
4999
0
            warn!(%operation_id, timeout=upload_timeout.as_secs(), "Upload results timeout");
5000
0
            Error::from_std_err(Code::DeadlineExceeded, &err).append(format!(
5001
                "Upload results timed out after {}s for operation {:?}",
5002
0
                upload_timeout.as_secs(),
5003
                operation_id,
5004
            ))
5005
0
        })?;
5006
45
        if let Err(
ref e1
) = res {
5007
1
            warn!(?operation_id, ?e, "Error during upload_results");
5008
44
        }
5009
45
        res
5010
45
    }
5011
5012
274
    async fn cleanup(self: Arc<Self>) -> Result<Arc<Self>, Error> {
5013
274
        let res = self
5014
274
            .metrics()
5015
274
            .clone()
5016
274
            .cleanup
5017
274
            .wrap(async move {
5018
274
                let result = do_cleanup(
5019
274
                    &self.running_actions_manager,
5020
274
                    &self.operation_id,
5021
274
                    &self.action_directory,
5022
274
                )
5023
274
                .await;
5024
274
                self.has_manager_entry.store(false, Ordering::Release);
5025
274
                self.did_cleanup.store(true, Ordering::Release);
5026
274
                self.state.lock().input_mounts.take();
5027
                // The work directory and manager entry are gone before the
5028
                // lease is released. If this future is cancelled while the
5029
                // release task is finishing, Drop still observes a completed
5030
                // cleanup and cannot strand the action in the manager.
5031
274
                if let Some(
input_lease8
) = self.input_lease.clone() {
5032
8
                    input_lease.release().await;
5033
266
                }
5034
274
                result.map(move |()| self)
5035
274
            })
5036
274
            .await;
5037
274
        if let Err(
ref e0
) = res {
5038
0
            warn!(?e, "Error during cleanup");
5039
274
        }
5040
274
        res
5041
274
    }
5042
5043
44
    
async fn get_finished_result(self: Arc<Self>) -> Result<ActionResult, Error>0
{
5044
44
        self.metrics()
5045
44
            .clone()
5046
44
            .get_finished_result
5047
44
            .wrap(Self::inner_get_finished_result(self))
5048
44
            .await
5049
44
    }
5050
5051
8
    fn resource_usage(&self) -> Option<ActionResourceUsage> {
5052
8
        self.state.lock().resource_usage.clone()
5053
8
    }
5054
5055
20
    fn get_work_directory(&self) -> &String {
5056
20
        &self.work_directory
5057
20
    }
5058
}
5059
5060
pub trait RunningActionsManager: Sync + Send + Sized + Unpin + 'static {
5061
    type RunningAction: RunningAction;
5062
5063
    fn create_and_add_action(
5064
        self: &Arc<Self>,
5065
        worker_id: String,
5066
        start_execute: StartExecute,
5067
    ) -> impl Future<Output = Result<Arc<Self::RunningAction>, Error>> + Send;
5068
5069
    fn cache_action_result(
5070
        &self,
5071
        action_digest: DigestInfo,
5072
        action_result: &mut ActionResult,
5073
        hasher: DigestHasherFunc,
5074
    ) -> impl Future<Output = Result<(), Error>> + Send;
5075
5076
    fn kill_all(&self) -> impl Future<Output = ()> + Send;
5077
5078
    fn kill_operation(
5079
        &self,
5080
        operation_id: &OperationId,
5081
    ) -> impl Future<Output = Result<(), Error>> + Send;
5082
5083
    fn metrics(&self) -> &Arc<Metrics>;
5084
5085
    /// The highest memory sampled so far for each running operation that
5086
    /// has been sampled, as `(operation id, KiB)`, for the keepalive
5087
    /// (RFC-0018). Empty where nothing samples.
5088
68.9k
    fn memory_peaks(&self) -> Vec<(String, u64)> {
5089
68.9k
        Vec::new()
5090
68.9k
    }
5091
}
5092
5093
/// A function to get the current system time, used to allow mocking for tests
5094
type NowFn = fn() -> SystemTime;
5095
type SleepFn = fn(Duration) -> BoxFuture<'static, ()>;
5096
5097
/// Functions that may be injected for testing purposes, during standard control
5098
/// flows these are specified by the new function.
5099
#[derive(Clone, Copy)]
5100
pub struct Callbacks {
5101
    /// A function that gets the current time.
5102
    pub now_fn: NowFn,
5103
    /// A function that sleeps for a given Duration.
5104
    pub sleep_fn: SleepFn,
5105
}
5106
5107
impl Debug for Callbacks {
5108
0
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
5109
0
        f.debug_struct("Callbacks").finish_non_exhaustive()
5110
0
    }
5111
}
5112
5113
/// The set of additional information for executing an action over and above
5114
/// those given in the `ActionInfo` passed to the worker.  This allows
5115
/// modification of the action for execution on this particular worker.  This
5116
/// may be used to run the action with a particular set of additional
5117
/// environment variables, or perhaps configure it to execute within a
5118
/// container.
5119
/// What the worker holds an action to beyond the scheduler's ledger. Each
5120
/// axis is on or off on its own; the value exists when at least one is on.
5121
#[derive(Debug, Clone)]
5122
pub struct ResourceEnforcement {
5123
    /// Soft memory enforcement: the worker kills an action whose process
5124
    /// group grows past its reservation plus headroom. Linux only; the
5125
    /// sampler that measures it does not exist elsewhere. An action run
5126
    /// through a persistent worker is not covered: that process outlives
5127
    /// the action and serves others, so its memory is no one action's
5128
    /// reservation, and only the pod's limit bounds it.
5129
    pub memory: Option<MemoryLimit>,
5130
    /// Refuse an action whose disk reservation, under this property, is
5131
    /// more than the free space under the work directory less what the
5132
    /// actions already admitted reserved, and measure what the action
5133
    /// writes.
5134
    pub disk_property_name: Option<String>,
5135
    /// Kill an action whose own files pass the disk reservation plus
5136
    /// `disk_headroom_percent`.
5137
    pub disk_soft: bool,
5138
    pub disk_headroom_percent: u64,
5139
    /// Kill the action holding the most memory when the worker's cgroup
5140
    /// stays under memory pressure; `None` leaves a thrashing worker alone.
5141
    pub pressure_kill: Option<PressureKill>,
5142
}
5143
5144
/// When sustained memory pressure on the worker's cgroup kills an action.
5145
#[derive(Debug, Clone, Copy)]
5146
pub struct PressureKill {
5147
    /// `memory.pressure` `full avg10` at or above this value (0 to 100) counts.
5148
    pub full_avg10_percent: u64,
5149
    /// For this long without a break.
5150
    pub after: Duration,
5151
    /// The largest memory reservation the worker advertises; an action
5152
    /// holding it is never the victim. `None` falls back to the cgroup limit.
5153
    pub largest_reservation_kb: Option<u64>,
5154
}
5155
5156
/// The reservation a memory ceiling is built from.
5157
#[derive(Debug, Clone)]
5158
pub struct MemoryLimit {
5159
    /// Platform property carrying the reservation in KiB.
5160
    pub property_name: String,
5161
    /// Percent above the reservation the action may reach.
5162
    pub headroom_percent: u64,
5163
}
5164
5165
impl ResourceEnforcement {
5166
    /// From the worker's configuration. `None` when neither axis is on;
5167
    /// `disk: guard` alone is honoured as much as `memory: soft` alone.
5168
3
    pub fn from_config(config: &ResourceEnforcementConfig) -> Option<Self> {
5169
3
        let memory = match config.memory {
5170
2
            MemoryEnforcement::None => None,
5171
1
            MemoryEnforcement::Soft => Some(MemoryLimit {
5172
1
                property_name: config.memory_property_name.clone(),
5173
1
                headroom_percent: config.memory_headroom_percent,
5174
1
            }),
5175
        };
5176
3
        let disk_property_name =
5177
3
            (config.disk != DiskEnforcement::None).then(|| 
config.disk_property_name1
.
clone1
());
5178
3
        if config.memory_pressure_kill_percent > 100 {
5179
0
            warn!(
5180
                memory_pressure_kill_percent = config.memory_pressure_kill_percent,
5181
                "memory_pressure_kill_percent is over 100; using 100"
5182
            );
5183
3
        }
5184
3
        if config.memory_pressure_kill_percent > 0 && 
config.memory_pressure_kill_after_s == 00
{
5185
0
            warn!("memory_pressure_kill_after_s is 0; using 1");
5186
3
        }
5187
3
        let pressure_kill = (config.memory_pressure_kill_percent > 0).then(|| PressureKill {
5188
0
            full_avg10_percent: config.memory_pressure_kill_percent.min(100),
5189
0
            after: Duration::from_secs(config.memory_pressure_kill_after_s.max(1)),
5190
0
            largest_reservation_kb: None,
5191
0
        });
5192
3
        if pressure_kill.is_some() && 
memory0
.
is_none0
() {
5193
0
            warn!(
5194
                "memory_pressure_kill_percent is set with memory: none; the pressure kill needs memory: soft and stays off"
5195
            );
5196
3
        }
5197
3
        let pressure_kill = pressure_kill.filter(|_| 
memory0
.
is_some0
());
5198
3
        (memory.is_some() || 
disk_property_name2
.
is_some2
()).then_some(Self {
5199
3
            memory,
5200
3
            disk_property_name,
5201
3
            disk_soft: config.disk == DiskEnforcement::Soft,
5202
3
            disk_headroom_percent: config.disk_headroom_percent,
5203
3
            pressure_kill,
5204
3
        })
5205
3
    }
5206
}
5207
5208
/// Free space in KiB on the filesystem holding `path`, as an unprivileged
5209
/// process may use it.
5210
#[cfg(target_family = "unix")]
5211
6
pub fn free_disk_kb(path: &str) -> Option<u64> {
5212
6
    let c_path = std::ffi::CString::new(path).ok()
?0
;
5213
6
    let mut stat: libc::statvfs = unsafe { core::mem::zeroed() };
5214
    // SAFETY: `c_path` is a valid C string and `stat` is a zeroed out
5215
    // parameter the call fills in.
5216
6
    let rc = unsafe { libc::statvfs(c_path.as_ptr(), &raw mut stat) };
5217
6
    if rc != 0 {
5218
0
        return None;
5219
6
    }
5220
6
    Some(widen(stat.f_bavail).saturating_mul(widen(stat.f_frsize)) / 1024)
5221
6
}
5222
5223
/// `statvfs` fields are u32 or u64 depending on the platform.
5224
#[cfg(target_family = "unix")]
5225
12
fn widen(value: impl Into<u64>) -> u64 {
5226
12
    value.into()
5227
12
}
5228
5229
#[cfg(not(target_family = "unix"))]
5230
pub fn free_disk_kb(_path: &str) -> Option<u64> {
5231
    None
5232
}
5233
5234
#[derive(Debug, Default)]
5235
pub struct ExecutionConfiguration {
5236
    /// Bytes of stdout or stderr kept in memory before the rest spills to a
5237
    /// file under the action directory; 0 keeps everything in memory.
5238
    pub max_captured_output_bytes: u64,
5239
    /// Kill an action that exceeds its memory reservation, before the
5240
    /// pod's cgroup limit takes the whole worker with it.
5241
    pub resource_enforcement: Option<ResourceEnforcement>,
5242
    /// Time a timed-out or cancelled action gets between SIGTERM and
5243
    /// SIGKILL to write its own cleanup; zero sends SIGKILL at once.
5244
    pub kill_grace: Duration,
5245
    /// Point `TMPDIR` at a directory of the action's own, removed with it.
5246
    pub set_tmpdir: bool,
5247
    /// How many input blobs the worker fetches into its store at once,
5248
    /// across every action it is staging; 0 shares no pool, and each action
5249
    /// fetches on its own.
5250
    pub max_concurrent_input_downloads: usize,
5251
    /// Buck2-only helper, configured for a dedicated execution container.
5252
    pub buck2_file_capture: Option<Buck2FileCaptureConfig>,
5253
    /// If set, will be executed instead of the first argument passed in the
5254
    /// `ActionInfo` with all of the arguments in the `ActionInfo` passed as
5255
    /// arguments to this command.
5256
    pub entrypoint: Option<String>,
5257
    /// The only environment variables that will be specified when the command
5258
    /// executes other than those in the `ActionInfo`.  On Windows, `SystemRoot`
5259
    /// and PATH are also assigned (see `inner_execute`).
5260
    pub additional_environment: Option<HashMap<String, EnvironmentSource>>,
5261
    /// Bazel persistent workers: whether an action that supports them runs
5262
    /// in a pooled process, and how the pool is sized.
5263
    pub persistent_workers: PersistentWorkersSettings,
5264
}
5265
5266
/// See `ExecutionConfiguration::persistent_workers`.
5267
#[derive(Debug, Clone, Copy)]
5268
pub struct PersistentWorkersSettings {
5269
    pub enabled: bool,
5270
    pub pool: PoolConfig,
5271
}
5272
5273
impl Default for PersistentWorkersSettings {
5274
0
    fn default() -> Self {
5275
0
        Self {
5276
0
            enabled: true,
5277
0
            pool: PoolConfig::default(),
5278
0
        }
5279
0
    }
5280
}
5281
5282
#[derive(Debug)]
5283
struct UploadActionResults {
5284
    upload_ac_results_strategy: UploadCacheResultsStrategy,
5285
    upload_historical_results_strategy: UploadCacheResultsStrategy,
5286
    ac_store: Option<Store>,
5287
    historical_store: Store,
5288
    success_message_template: Template,
5289
    failure_message_template: Template,
5290
}
5291
5292
impl UploadActionResults {
5293
82
    fn new(
5294
82
        config: &UploadActionResultConfig,
5295
82
        ac_store: Option<Store>,
5296
82
        historical_store: Store,
5297
82
    ) -> Result<Self, Error> {
5298
82
        let upload_historical_results_strategy = config
5299
82
            .upload_historical_results_strategy
5300
82
            .unwrap_or(DEFAULT_HISTORICAL_RESULTS_STRATEGY);
5301
8
        if !matches!(
5302
82
            config.upload_ac_results_strategy,
5303
            UploadCacheResultsStrategy::Never
5304
8
        ) && ac_store.is_none()
5305
        {
5306
0
            return Err(make_input_err!(
5307
0
                "upload_ac_results_strategy is set, but no ac_store is configured"
5308
0
            ));
5309
82
        }
5310
        Ok(Self {
5311
82
            upload_ac_results_strategy: config.upload_ac_results_strategy,
5312
82
            upload_historical_results_strategy,
5313
82
            ac_store,
5314
82
            historical_store,
5315
82
            success_message_template: Template::new(&config.success_message_template).map_err(
5316
0
                |e| {
5317
0
                    Error::from_std_err(Code::InvalidArgument, &e).append(format!(
5318
                        "Could not convert success_message_template to rust template: {}",
5319
                        config.success_message_template
5320
                    ))
5321
0
                },
5322
0
            )?,
5323
82
            failure_message_template: Template::new(&config.failure_message_template).map_err(
5324
0
                |e| {
5325
0
                    Error::from_std_err(Code::InvalidArgument, &e).append(format!(
5326
                        "Could not convert failure_message_template to rust template: {}",
5327
                        config.success_message_template
5328
                    ))
5329
0
                },
5330
0
            )?,
5331
        })
5332
82
    }
5333
5334
0
    const fn should_cache_result(
5335
0
        strategy: UploadCacheResultsStrategy,
5336
0
        action_result: &ActionResult,
5337
0
        treat_infra_error_as_failure: bool,
5338
0
    ) -> bool {
5339
0
        let did_fail = action_result.exit_code != 0
5340
0
            || (treat_infra_error_as_failure && action_result.error.is_some());
5341
0
        match strategy {
5342
0
            UploadCacheResultsStrategy::SuccessOnly => !did_fail,
5343
0
            UploadCacheResultsStrategy::Never => false,
5344
            // Never cache internal errors or timeouts.
5345
            UploadCacheResultsStrategy::Everything => {
5346
0
                treat_infra_error_as_failure || action_result.error.is_none()
5347
            }
5348
0
            UploadCacheResultsStrategy::FailuresOnly => did_fail,
5349
        }
5350
0
    }
5351
5352
    /// Formats the message field in `ExecuteResponse` from the `success_message_template`
5353
    /// or `failure_message_template` config templates.
5354
5
    fn format_execute_response_message(
5355
5
        mut template_str: Template,
5356
5
        action_digest_info: DigestInfo,
5357
5
        maybe_historical_digest_info: Option<DigestInfo>,
5358
5
        hasher: DigestHasherFunc,
5359
5
    ) -> Result<String, Error> {
5360
5
        template_str.replace(
5361
            "digest_function",
5362
5
            hasher.proto_digest_func().as_str_name().to_lowercase(),
5363
        );
5364
5
        template_str.replace(
5365
            "action_digest_hash",
5366
5
            action_digest_info.packed_hash().to_string(),
5367
        );
5368
5
        template_str.replace("action_digest_size", action_digest_info.size_bytes());
5369
5
        if let Some(
historical_digest_info3
) = maybe_historical_digest_info {
5370
3
            template_str.replace(
5371
3
                "historical_results_hash",
5372
3
                format!("{}", historical_digest_info.packed_hash()),
5373
3
            );
5374
3
            template_str.replace(
5375
3
                "historical_results_size",
5376
3
                historical_digest_info.size_bytes(),
5377
3
            );
5378
3
        } else {
5379
2
            template_str.replace("historical_results_hash", "");
5380
2
            template_str.replace("historical_results_size", "");
5381
2
        }
5382
5
        template_str.text().map_err(|e| 
{0
5383
0
            Error::from_std_err(Code::InvalidArgument, &e)
5384
0
                .append("Could not convert template to text")
5385
0
        })
5386
5
    }
5387
5388
5
    async fn upload_ac_results(
5389
5
        &self,
5390
5
        action_digest: DigestInfo,
5391
5
        action_result: ProtoActionResult,
5392
5
        hasher: DigestHasherFunc,
5393
5
    ) -> Result<(), Error> {
5394
5
        let Some(ac_store) = self.ac_store.as_ref() else {
5395
0
            return Ok(());
5396
        };
5397
        // If we are a GrpcStore we shortcut here, as this is a special store.
5398
5
        if let Some(
grpc_store0
) = ac_store.downcast_ref::<GrpcStore>(Some(action_digest.into())) {
5399
0
            let update_action_request = UpdateActionResultRequest {
5400
0
                // This is populated by `update_action_result`.
5401
0
                instance_name: String::new(),
5402
0
                action_digest: Some(action_digest.into()),
5403
0
                action_result: Some(action_result),
5404
0
                results_cache_policy: None,
5405
0
                digest_function: hasher.proto_digest_func().into(),
5406
0
            };
5407
0
            return grpc_store
5408
0
                .update_action_result(Request::new(update_action_request))
5409
0
                .await
5410
0
                .map(|_| ())
5411
0
                .err_tip(|| "Caching ActionResult");
5412
5
        }
5413
5414
5
        let mut store_data = BytesMut::with_capacity(ESTIMATED_DIGEST_SIZE);
5415
5
        action_result
5416
5
            .encode(&mut store_data)
5417
5
            .err_tip(|| "Encoding ActionResult for caching")
?0
;
5418
5419
5
        ac_store
5420
5
            .update_oneshot(action_digest, store_data.split().freeze())
5421
5
            .await
5422
5
            .err_tip(|| "Caching ActionResult")
5423
5
    }
5424
5425
3
    async fn upload_historical_results_with_message(
5426
3
        &self,
5427
3
        action_digest: DigestInfo,
5428
3
        execute_response: ExecuteResponse,
5429
3
        message_template: Template,
5430
3
        hasher: DigestHasherFunc,
5431
3
    ) -> Result<String, Error> {
5432
3
        let historical_digest_info = serialize_and_upload_message(
5433
3
            &HistoricalExecuteResponse {
5434
3
                action_digest: Some(action_digest.into()),
5435
3
                execute_response: Some(execute_response.clone()),
5436
3
            },
5437
3
            self.historical_store.as_pin(),
5438
3
            &mut hasher.hasher(),
5439
3
        )
5440
3
        .await
5441
3
        .err_tip(|| 
format!0
("Caching HistoricalExecuteResponse for digest: {action_digest}"))
?0
;
5442
5443
3
        Self::format_execute_response_message(
5444
3
            message_template,
5445
3
            action_digest,
5446
3
            Some(historical_digest_info),
5447
3
            hasher,
5448
        )
5449
3
        .err_tip(|| "Could not format message in upload_historical_results_with_message")
5450
3
    }
5451
5452
6
    async fn cache_action_result(
5453
6
        &self,
5454
6
        action_info: DigestInfo,
5455
6
        action_result: &mut ActionResult,
5456
6
        hasher: DigestHasherFunc,
5457
6
    ) -> Result<(), Error> {
5458
6
        let should_upload_historical_results =
5459
6
            Self::should_cache_result(self.upload_historical_results_strategy, action_result, true);
5460
6
        let should_upload_ac_results =
5461
6
            Self::should_cache_result(self.upload_ac_results_strategy, action_result, false);
5462
        // Shortcut so we don't need to convert to proto if not needed.
5463
6
        if !should_upload_ac_results && 
!should_upload_historical_results1
{
5464
1
            return Ok(());
5465
5
        }
5466
5467
5
        let mut execute_response = to_execute_response(action_result.clone());
5468
5469
        // In theory exit code should always be != 0 if there's an error, but for safety we
5470
        // catch both.
5471
5
        let message_template = if action_result.exit_code == 0 && 
action_result.error4
.
is_none4
() {
5472
3
            self.success_message_template.clone()
5473
        } else {
5474
2
            self.failure_message_template.clone()
5475
        };
5476
5477
5
        let upload_historical_results_with_message_result = if should_upload_historical_results {
5478
3
            let maybe_message = self
5479
3
                .upload_historical_results_with_message(
5480
3
                    action_info,
5481
3
                    execute_response.clone(),
5482
3
                    message_template,
5483
3
                    hasher,
5484
3
                )
5485
3
                .await;
5486
3
            match maybe_message {
5487
3
                Ok(message) => {
5488
3
                    action_result.message.clone_from(&message);
5489
3
                    execute_response.message = message;
5490
3
                    Ok(())
5491
                }
5492
0
                Err(e) => Result::<(), Error>::Err(e),
5493
            }
5494
        } else {
5495
2
            match Self::format_execute_response_message(message_template, action_info, None, hasher)
5496
            {
5497
2
                Ok(message) => {
5498
2
                    action_result.message.clone_from(&message);
5499
2
                    execute_response.message = message;
5500
2
                    Ok(())
5501
                }
5502
0
                Err(e) => Err(e).err_tip(|| "Could not format message in cache_action_result"),
5503
            }
5504
        };
5505
5506
        // Note: Done in this order because we assume most results will succeed and most configs will
5507
        // either always upload upload historical results or only upload on failure. In which case
5508
        // we can avoid an extra clone of the protos by doing this last with the above assumption.
5509
5
        let ac_upload_results = if should_upload_ac_results {
5510
5
            self.upload_ac_results(
5511
5
                action_info,
5512
5
                execute_response
5513
5
                    .result
5514
5
                    .err_tip(|| "No result set in cache_action_result")
?0
,
5515
5
                hasher,
5516
            )
5517
5
            .await
5518
        } else {
5519
0
            Ok(())
5520
        };
5521
5
        upload_historical_results_with_message_result.merge(ac_upload_results)
5522
6
    }
5523
}
5524
5525
#[cfg(target_os = "linux")]
5526
#[derive(Copy, Clone, Debug)]
5527
pub enum UseNamespaces {
5528
    No,
5529
    Yes,
5530
    /// Also unshare the mount namespace. With `isolate_tmp` each action gets
5531
    /// a private `/tmp`, its own tmp directory bound over `/tmp`, on top of
5532
    /// the masked root action directory.
5533
    YesAndMount {
5534
        isolate_tmp: bool,
5535
    },
5536
}
5537
5538
#[derive(Debug)]
5539
pub struct RunningActionsManagerArgs<'a> {
5540
    pub root_action_directory: String,
5541
    pub execution_configuration: ExecutionConfiguration,
5542
    pub cas_store: Arc<FastSlowStore>,
5543
    pub ac_store: Option<Store>,
5544
    pub historical_store: Store,
5545
    pub upload_action_result_config: &'a UploadActionResultConfig,
5546
    pub max_action_timeout: Duration,
5547
    pub max_upload_timeout: Duration,
5548
    pub max_download_timeout: Duration,
5549
    pub max_cleanup_wait: Duration,
5550
    pub max_cleanup_backoff: Duration,
5551
    pub timeout_handled_externally: bool,
5552
    pub directory_cache: Option<Arc<crate::directory_cache::DirectoryCache>>,
5553
    /// When true, every digest in an active action's input Merkle closure is
5554
    /// leased in the locally eviction-managed CAS tiers until the action's
5555
    /// cleanup completes. While leases are held, those tiers may temporarily
5556
    /// exceed their configured `max_bytes` / `max_count` eviction limits.
5557
    pub active_input_leases: bool,
5558
    #[cfg(target_os = "linux")]
5559
    pub readonly_input_mounts: Vec<String>,
5560
    #[cfg(target_os = "linux")]
5561
    pub use_namespaces: UseNamespaces,
5562
}
5563
5564
/// The mark that an operation's directory is being cleaned; dropping it
5565
/// clears the mark and wakes whoever waits for it.
5566
#[derive(Debug)]
5567
pub struct CleanupGuard {
5568
    manager: Weak<RunningActionsManagerImpl>,
5569
    operation_id: OperationId,
5570
}
5571
5572
impl Drop for CleanupGuard {
5573
563
    fn drop(&mut self) {
5574
563
        let Some(manager) = self.manager.upgrade() else {
5575
0
            return;
5576
        };
5577
563
        let mut cleaning = manager.cleaning_up_operations.lock();
5578
563
        cleaning.remove(&self.operation_id);
5579
563
        manager.cleanup_complete_notify.notify_waiters();
5580
563
    }
5581
}
5582
5583
/// Holds state info about what is being executed and the interface for interacting
5584
/// with actions while they are running.
5585
#[derive(Debug)]
5586
pub struct RunningActionsManagerImpl {
5587
    root_action_directory: String,
5588
    execution_configuration: ExecutionConfiguration,
5589
    cas_store: Arc<FastSlowStore>,
5590
    filesystem_store: Arc<FilesystemStore>,
5591
    upload_action_results: UploadActionResults,
5592
    max_action_timeout: Duration,
5593
    max_upload_timeout: Duration,
5594
    max_download_timeout: Duration,
5595
    timeout_handled_externally: bool,
5596
    /// The container's memory limit, the yardstick for calling a SIGKILL
5597
    /// nobody here sent an OOM kill.
5598
    pod_memory_limit_kb: Option<u64>,
5599
    #[cfg(target_os = "linux")]
5600
    use_namespaces: UseNamespaces,
5601
    /// The process table every action's resource-usage sampler shares.
5602
    process_table: Arc<ProcessTableCache>,
5603
    running_actions: Mutex<HashMap<OperationId, Weak<RunningActionImpl>>>,
5604
    /// Set once the memory pressure monitor has been started; it starts
5605
    /// with the first action, so a manager that never runs one never
5606
    /// reads the cgroup.
5607
    pressure_monitor_started: AtomicBool,
5608
    // Note: We don't use Notify because we need to support a .wait_for()-like function, which
5609
    // Notify does not support.
5610
    action_done_tx: watch::Sender<()>,
5611
    callbacks: Callbacks,
5612
    metrics: Arc<Metrics>,
5613
    /// Disk each admitted action reserved, by operation, so the disk guard
5614
    /// checks against what is left after them. An entry lives from the
5615
    /// guard passing to `cleanup_action`.
5616
    disk_reservations_kb: Mutex<HashMap<OperationId, u64>>,
5617
    /// Track operations being cleaned up to avoid directory collisions during action retries.
5618
    /// When an action fails and is retried on the same worker, we need to ensure the previous
5619
    /// attempt's directory is fully cleaned up before creating a new one.
5620
    /// See: <https://github.com/TraceMachina/nativelink/issues/1859>
5621
    cleaning_up_operations: Mutex<HashSet<OperationId>>,
5622
    max_cleanup_wait: Duration,
5623
    max_cleanup_backoff: Duration,
5624
    /// Notify waiters when a cleanup operation completes. This is used in conjunction with
5625
    /// `cleaning_up_operations` to coordinate directory cleanup and creation.
5626
    cleanup_complete_notify: Arc<Notify>,
5627
    /// Optional directory cache for improving performance by caching reconstructed
5628
    /// input directories and using hardlinks.
5629
    directory_cache: Option<Arc<crate::directory_cache::DirectoryCache>>,
5630
    /// Whether active action inputs are leased against eviction in the local
5631
    /// CAS tiers (opt-in via `experimental_active_input_leases`).
5632
    active_input_leases: bool,
5633
    /// The pool every input fetch draws a permit from, shared by all the
5634
    /// actions this worker stages; see
5635
    /// `ExecutionConfiguration::max_concurrent_input_downloads`. Without
5636
    /// one configured it is too large to bound anything.
5637
    input_downloads: Semaphore,
5638
    /// Set while no pool is configured, to suggest one when fetches show
5639
    /// the store behind them struggling.
5640
    input_pool_hint: Option<InputPoolHint>,
5641
    #[cfg(target_os = "linux")]
5642
    readonly_input_mounts: Vec<String>,
5643
    persistent_worker_pool: PersistentWorkerPool,
5644
    /// Retires idle persistent workers; ends with the manager.
5645
    _persistent_worker_sweeper: JoinHandleDropGuard<()>,
5646
    buck2_captures: Mutex<HashMap<OperationId, Buck2FileCapture>>,
5647
}
5648
5649
impl RunningActionsManagerImpl {
5650
    /// Permits free in the input fetch pool right now; with nothing staging,
5651
    /// the pool's size, which is `Semaphore::MAX_PERMITS` without one.
5652
2
    pub fn free_input_downloads(&self) -> usize {
5653
2
        self.input_downloads.available_permits()
5654
2
    }
5655
5656
    /// Suggests `max_concurrent_input_downloads` when this worker fetches
5657
    /// without a shared pool and a fetch just failed or stalled the way an
5658
    /// overloaded store makes it. `DataLoss` is retried as transient but
5659
    /// means a truncated or corrupt stored copy, not a struggling store, so
5660
    /// it suggests nothing.
5661
21
    fn suggest_input_download_pool(&self, operation_id: &OperationId, error: &Error) {
5662
21
        let Some(
hint19
) = &self.input_pool_hint else {
5663
2
            return;
5664
        };
5665
19
        if error.code != Code::DataLoss && 
hint14
.
due14
(
Instant::now14
()) {
5666
7
            warn!(
5667
                %operation_id,
5668
                %error,
5669
                "Input fetches are failing or stalling while max_concurrent_input_downloads is unset: every action this worker stages fetches its inputs on its own, and several cold actions at once can overload the CAS and the object store behind it. Consider setting max_concurrent_input_downloads to 64 so they share one pool of fetches (logged at most every 10 minutes)"
5670
            );
5671
12
        }
5672
21
    }
5673
5674
82
    pub fn new_with_callbacks(
5675
82
        args: RunningActionsManagerArgs<'_>,
5676
82
        callbacks: Callbacks,
5677
82
    ) -> Result<Self, Error> {
5678
        #[cfg(target_os = "linux")]
5679
        {
5680
82
            crate::input_mounts::validate_paths(&args.readonly_input_mounts)
?0
;
5681
82
            if !args.readonly_input_mounts.is_empty()
5682
1
                && (!
matches!0
(args.use_namespaces, UseNamespaces::YesAndMount { .. })
5683
1
                    || args.directory_cache.is_none())
5684
            {
5685
0
                return Err(make_err!(
5686
0
                    Code::InvalidArgument,
5687
0
                    "Read-only input mounts require Linux mount namespaces and a directory cache"
5688
0
                ));
5689
82
            }
5690
        }
5691
        // Sadly because of some limitations of how Any works we need to clone more times than optimal.
5692
82
        let filesystem_store = args
5693
82
            .cas_store
5694
82
            .fast_store()
5695
82
            .downcast_ref::<FilesystemStore>(None)
5696
82
            .err_tip(
5697
                || "Expected FilesystemStore store for .fast_store() in RunningActionsManagerImpl",
5698
0
            )?
5699
82
            .get_arc()
5700
82
            .err_tip(|| "FilesystemStore's internal Arc was lost")
?0
;
5701
82
        let (action_done_tx, _) = watch::channel(());
5702
82
        let persistent_worker_pool =
5703
82
            PersistentWorkerPool::new(args.execution_configuration.persistent_workers.pool);
5704
82
        let persistent_worker_sweeper = persistent_worker_pool.spawn_sweeper();
5705
82
        let max_downloads = args.execution_configuration.max_concurrent_input_downloads;
5706
82
        let input_downloads = input_download_pool(max_downloads);
5707
82
        let input_pool_hint = (max_downloads == 0).then(InputPoolHint::default);
5708
        Ok(Self {
5709
82
            root_action_directory: args.root_action_directory,
5710
82
            execution_configuration: args.execution_configuration,
5711
82
            input_downloads,
5712
82
            input_pool_hint,
5713
82
            cas_store: args.cas_store,
5714
82
            filesystem_store,
5715
82
            upload_action_results: UploadActionResults::new(
5716
82
                args.upload_action_result_config,
5717
82
                args.ac_store,
5718
82
                args.historical_store,
5719
            )
5720
82
            .err_tip(|| "During RunningActionsManagerImpl construction")
?0
,
5721
82
            max_action_timeout: args.max_action_timeout,
5722
82
            pod_memory_limit_kb: pod_memory_limit_kb(),
5723
82
            max_upload_timeout: args.max_upload_timeout,
5724
82
            max_download_timeout: args.max_download_timeout,
5725
82
            timeout_handled_externally: args.timeout_handled_externally,
5726
82
            running_actions: Mutex::new(HashMap::new()),
5727
82
            pressure_monitor_started: AtomicBool::new(false),
5728
82
            action_done_tx,
5729
82
            callbacks,
5730
82
            metrics: Arc::new(Metrics {
5731
82
                directory_cache: args.directory_cache.as_ref().map(Arc::downgrade),
5732
82
                ..Default::default()
5733
82
            }),
5734
82
            disk_reservations_kb: Mutex::new(HashMap::new()),
5735
82
            cleaning_up_operations: Mutex::new(HashSet::new()),
5736
82
            max_cleanup_wait: args.max_cleanup_wait,
5737
82
            max_cleanup_backoff: args.max_cleanup_backoff,
5738
82
            cleanup_complete_notify: Arc::new(Notify::new()),
5739
82
            directory_cache: args.directory_cache,
5740
82
            active_input_leases: args.active_input_leases,
5741
            #[cfg(target_os = "linux")]
5742
82
            readonly_input_mounts: args.readonly_input_mounts,
5743
82
            persistent_worker_pool,
5744
82
            _persistent_worker_sweeper: persistent_worker_sweeper,
5745
82
            buck2_captures: Mutex::new(HashMap::new()),
5746
            #[cfg(target_os = "linux")]
5747
82
            use_namespaces: args.use_namespaces,
5748
82
            process_table: Arc::new(ProcessTableCache::new(RESOURCE_USAGE_SAMPLE_INTERVAL)),
5749
        })
5750
82
    }
5751
5752
60
    pub fn new(args: RunningActionsManagerArgs<'_>) -> Result<Self, Error> {
5753
60
        Self::new_with_callbacks(
5754
60
            args,
5755
            Callbacks {
5756
60
                now_fn: SystemTime::now,
5757
29
                sleep_fn: |duration| Box::pin(tokio::time::sleep(duration)),
5758
            },
5759
        )
5760
60
    }
5761
5762
    /// Fixes a race condition that occurs when an action fails to execute on a worker, and the same worker
5763
    /// attempts to re-execute the same action before the physical cleanup (file is removed) completes.
5764
    /// See this issue for additional details: <https://github.com/TraceMachina/nativelink/issues/1859>
5765
0
    async fn wait_for_cleanup_if_needed(
5766
0
        self: &Arc<Self>,
5767
0
        operation_id: &OperationId,
5768
279
    ) -> Result<(), Error> {
5769
279
        let start = Instant::now();
5770
279
        let mut backoff = Duration::from_millis(10);
5771
279
        let mut has_waited = false;
5772
5773
        loop {
5774
            // The mark is held for the removal below, so it cannot race the
5775
            // orphan sweep's removal of the same tree; while someone else
5776
            // holds it, this waits.
5777
284
            let mark = self.perform_cleanup(operation_id.clone());
5778
284
            if let Some(
_cleaning_guard279
) = mark {
5779
279
                let action_is_running = self
5780
279
                    .running_actions
5781
279
                    .lock()
5782
279
                    .get(operation_id)
5783
279
                    .and_then(Weak::upgrade)
5784
279
                    .is_some();
5785
279
                if action_is_running {
5786
1
                    return Err(make_err!(
5787
1
                        Code::AlreadyExists,
5788
1
                        "Action with operation_id {} is already running",
5789
1
                        operation_id
5790
1
                    ));
5791
278
                }
5792
5793
278
                let dir_path =
5794
278
                    PathBuf::from(&self.root_action_directory).join(operation_id.to_string());
5795
5796
278
                if !dir_path.exists() {
5797
276
                    return Ok(());
5798
2
                }
5799
5800
                // Safety check: ensure we're only removing directories under root_action_directory
5801
2
                let root_path = Path::new(&self.root_action_directory);
5802
2
                let canonical_root = root_path.canonicalize().err_tip(|| 
{0
5803
0
                    format!(
5804
                        "Failed to canonicalize root directory: {}",
5805
0
                        self.root_action_directory
5806
                    )
5807
0
                })?;
5808
2
                let canonical_dir = dir_path.canonicalize().err_tip(|| 
{0
5809
0
                    format!("Failed to canonicalize directory: {}", dir_path.display())
5810
0
                })?;
5811
5812
2
                if !canonical_dir.starts_with(&canonical_root) {
5813
0
                    return Err(make_err!(
5814
0
                        Code::Internal,
5815
0
                        "Attempted to remove directory outside of root_action_directory: {}",
5816
0
                        dir_path.display()
5817
0
                    ));
5818
2
                }
5819
5820
                // Directory exists but not being cleaned - remove it
5821
2
                warn!(
5822
                    "Removing stale directory for {}: {}",
5823
                    operation_id,
5824
2
                    dir_path.display()
5825
                );
5826
2
                self.metrics.stale_removals.inc();
5827
5828
                // Try to remove the directory, with one retry on failure
5829
2
                let remove_result = fs::remove_dir_all(&dir_path).await;
5830
2
                if let Err(
e0
) = remove_result {
5831
                    // Retry once after a short delay in case the directory is temporarily locked
5832
0
                    tokio::time::sleep(Duration::from_millis(100)).await;
5833
0
                    fs::remove_dir_all(&dir_path).await.err_tip(|| {
5834
0
                        format!(
5835
                            "Failed to remove stale directory {} for retry of {} after retry (original error: {})",
5836
0
                            dir_path.display(),
5837
                            operation_id,
5838
                            e
5839
                        )
5840
0
                    })?;
5841
2
                }
5842
2
                return Ok(());
5843
5
            }
5844
5845
5
            if start.elapsed() > self.max_cleanup_wait {
5846
0
                self.metrics.cleanup_wait_timeouts.inc();
5847
0
                warn!(%operation_id, waited=?start.elapsed(), "Timeout waiting for previous operation cleanup");
5848
0
                return Err(make_err!(
5849
0
                    Code::DeadlineExceeded,
5850
0
                    "Timeout waiting for previous operation cleanup: {} (waited {:?})",
5851
0
                    operation_id,
5852
0
                    start.elapsed()
5853
0
                ));
5854
5
            }
5855
5856
5
            if !has_waited {
5857
1
                self.metrics.cleanup_waits.inc();
5858
1
                has_waited = true;
5859
4
            }
5860
5861
5
            trace!(
5862
                "Waiting for cleanup of {} (elapsed: {:?}, backoff: {:?})",
5863
                operation_id,
5864
5
                start.elapsed(),
5865
                backoff
5866
            );
5867
5868
5
            tokio::select! {
5869
5
                () = self.cleanup_complete_notify.notified() => 
{}1
,
5870
5
                () = tokio::time::sleep(backoff) => {
5871
4
                    // Exponential backoff
5872
4
                    backoff = (backoff * 2).min(self.max_cleanup_backoff);
5873
4
                },
5874
            }
5875
        }
5876
279
    }
5877
5878
278
    fn make_action_directory<'a>(
5879
278
        &'a self,
5880
278
        operation_id: &'a OperationId,
5881
278
    ) -> impl Future<Output = Result<String, Error>> + 'a {
5882
278
        self.metrics.make_action_directory.wrap(async move {
5883
278
            let action_directory = format!("{}/{}", self.root_action_directory, operation_id);
5884
278
            fs::create_dir(&action_directory)
5885
278
                .await
5886
278
                .err_tip(|| 
format!0
("Error creating action directory {action_directory}"))
?0
;
5887
278
            Ok(action_directory)
5888
278
        })
5889
278
    }
5890
5891
    /// Reads the Action, again after a transient store failure while
5892
    /// `max_download_timeout` leaves room for another attempt. It is the one
5893
    /// store read before `prepare_action`'s own retries: a CAS pod being
5894
    /// replaced refused it on the first attempt, the action failed on the
5895
    /// spot, and the scheduler counted that against the action's retries.
5896
280
    
async fn fetch_action_with_retry(&self, action_digest: DigestInfo) -> Result<Action, Error>0
{
5897
280
        let started = Instant::now();
5898
280
        let deadline = started.checked_add(self.max_download_timeout);
5899
280
        let mut attempt: u32 = 1;
5900
280
        let mut retries = PrepareRetries::new(record_prepare_outcome);
5901
        loop {
5902
284
            let 
err5
= match get_and_decode_digest::<Action>(
5903
284
                self.cas_store.as_ref(),
5904
284
                action_digest.into(),
5905
            )
5906
284
            .await
5907
            {
5908
279
                Ok(action) => {
5909
279
                    if attempt > 1 {
5910
1
                        info!(
5911
                            %action_digest,
5912
                            attempt,
5913
1
                            elapsed_s = started.elapsed().as_secs(),
5914
                            "start_action: action read recovered"
5915
                        );
5916
278
                    }
5917
279
                    retries.settle("recovered");
5918
279
                    return Ok(action);
5919
                }
5920
5
                Err(err) if is_transient_fetch_error(&err) => err,
5921
0
                Err(err) => {
5922
0
                    retries.settle("failed");
5923
0
                    return Err(err);
5924
                }
5925
            };
5926
5
            let pause = prepare_retry_pause(attempt, &action_digest);
5927
5
            if deadline.is_some_and(|deadline| Instant::now() + pause >= deadline) {
5928
1
                retries.settle("exhausted");
5929
1
                return Err(err
5930
1
                    .append(format!(
5931
1
                        "Reading action {action_digest} failed {attempt} time(s) over {}s; the next retry would start after max_download_timeout",
5932
1
                        started.elapsed().as_secs()
5933
1
                    ))
5934
1
                    .append(STORE_UNREACHABLE_ERROR_TIP));
5935
4
            }
5936
4
            warn!(
5937
                %action_digest,
5938
                attempt,
5939
4
                wait_ms = pause.as_millis(),
5940
                error = %err,
5941
                "start_action: action read failed, retrying"
5942
            );
5943
4
            retries.retried();
5944
4
            tokio::time::sleep(pause).await;
5945
4
            attempt += 1;
5946
        }
5947
280
    }
5948
5949
280
    fn create_action_info(
5950
280
        &self,
5951
280
        start_execute: StartExecute,
5952
280
        queued_timestamp: SystemTime,
5953
280
    ) -> impl Future<Output = Result<ActionInfo, Error>> + '_ {
5954
280
        self.metrics.create_action_info.wrap(async move {
5955
280
            let execute_request = start_execute
5956
280
                .execute_request
5957
280
                .err_tip(|| "Expected execute_request to exist in StartExecute")
?0
;
5958
280
            let action_digest: DigestInfo = execute_request
5959
280
                .action_digest
5960
280
                .clone()
5961
280
                .err_tip(|| "Expected action_digest to exist on StartExecute")
?0
5962
280
                .try_into()
?0
;
5963
280
            let load_start_timestamp = (self.callbacks.now_fn)();
5964
280
            let 
action279
= self
5965
280
                .fetch_action_with_retry(action_digest)
5966
280
                .await
5967
280
                .err_tip(|| "During start_action")
5968
280
                .map_err(tag_missing_input)
?1
;
5969
279
            let mut action_info = ActionInfo::try_from_action_and_execute_request(
5970
279
                execute_request,
5971
279
                action,
5972
279
                load_start_timestamp,
5973
279
                queued_timestamp,
5974
            )
5975
279
            .err_tip(|| "Could not create ActionInfo in create_and_add_action()")
?0
;
5976
            // With enforcement on, the scheduler's view of the properties
5977
            // replaces the client's: a hint or cold-start reservation the
5978
            // scheduler placed this action by only exists there, and what
5979
            // is enforced (and exported to the environment) has to be the
5980
            // number the ledger charged. Off, the worker keeps reading the
5981
            // client's copy as it always did, so an upgrade changes no
5982
            // action's environment on its own.
5983
279
            if self.execution_configuration.resource_enforcement.is_some()
5984
10
                && let Some(platform) = start_execute.platform
5985
            {
5986
10
                for property in platform.properties {
5987
10
                    action_info
5988
10
                        .platform_properties
5989
10
                        .insert(property.name, property.value);
5990
10
                }
5991
269
            }
5992
279
            Ok(action_info)
5993
280
        })
5994
280
    }
5995
5996
    /// Removes action directories under the root that no running action
5997
    /// owns and that have not been touched for `max_cleanup_wait`. A
5998
    /// cleanup that failed (the store was down, the process restarted
5999
    /// mid-removal) leaves its directory behind, and it counts against the
6000
    /// pod's disk until something removes it. A directory that will not go
6001
    /// is logged and counted and the sweep carries on, or one stubborn
6002
    /// entry would stop every sweep at the same place. Returns how many
6003
    /// were removed; the error is for the listing itself failing.
6004
    ///
6005
    /// A directory is named for its operation, and a retry of that
6006
    /// operation may be starting at any moment. The retry's
6007
    /// `wait_for_cleanup_if_needed` waits while the operation is marked as
6008
    /// cleaning, so the sweep takes that mark for the removal and reads
6009
    /// ownership and age under it; a mark already held is the action's own
6010
    /// cleanup, which finishes the job.
6011
5
    pub async fn sweep_orphaned_directories(self: &Arc<Self>) -> Result<usize, Error> {
6012
5
        let (_permit, dir_handle) = fs::read_dir(&self.root_action_directory)
6013
5
            .await
6014
5
            .err_tip(|| 
format!0
("Reading {} for orphans",
self.root_action_directory0
))
?0
6015
5
            .into_inner();
6016
5
        let mut dir_stream = ReadDirStream::new(dir_handle);
6017
5
        let now = SystemTime::now();
6018
5
        let mut removed = 0;
6019
12
        while let Some(
entry7
) = dir_stream.next().await {
6020
7
            let entry = entry.err_tip(|| "Iterating action root for orphans")
?0
;
6021
7
            let Some(name) = entry.file_name().to_str().map(str::to_owned) else {
6022
0
                continue;
6023
            };
6024
            // A running action's directory is not the sweep's to touch, and
6025
            // its cleanup waits on the mark, so the mark is taken only for
6026
            // an unowned directory and given back at once if the directory
6027
            // became owned in between: an action that starts and ends
6028
            // inside that window then cleans up as usual, not later.
6029
13
            let 
is_owned7
= || {
6030
13
                self.running_actions
6031
13
                    .lock()
6032
13
                    .keys()
6033
13
                    .any(|operation_id| 
operation_id.to_string()0
==
name0
)
6034
13
            };
6035
7
            if is_owned() {
6036
0
                continue;
6037
7
            }
6038
7
            let Some(
_cleaning_guard6
) = self.perform_cleanup(OperationId::from(name.as_str()))
6039
            else {
6040
1
                continue;
6041
            };
6042
6
            if is_owned() {
6043
0
                continue;
6044
6
            }
6045
6
            let path = entry.path();
6046
            // Not followed: a symlink under the root is not an action
6047
            // directory, whatever it points at.
6048
6
            let Ok(metadata) = fs::symlink_metadata(&path).await else {
6049
0
                continue;
6050
            };
6051
6
            if !metadata.is_dir() {
6052
3
                continue;
6053
3
            }
6054
            // Young directories may belong to an action between creation and
6055
            // registration; only settled ones are orphans.
6056
3
            let age = metadata
6057
3
                .modified()
6058
3
                .ok()
6059
3
                .and_then(|modified| now.duration_since(modified).ok())
6060
3
                .unwrap_or(Duration::MAX);
6061
3
            if age < self.max_cleanup_wait {
6062
0
                continue;
6063
3
            }
6064
3
            warn!(path = %path.display(), age_secs = age.as_secs(), "Removing orphaned action directory");
6065
3
            match fs::remove_dir_all(&path).await {
6066
3
                Ok(()) => {
6067
3
                    self.metrics.orphan_removals.inc();
6068
3
                    removed += 1;
6069
3
                }
6070
0
                Err(err) => {
6071
0
                    self.metrics.orphan_removal_failures.inc();
6072
0
                    warn!(path = %path.display(), ?err, "Could not remove orphaned action directory");
6073
                }
6074
            }
6075
        }
6076
5
        Ok(removed)
6077
5
    }
6078
6079
277
    fn cleanup_action(&self, operation_id: &OperationId) -> Result<(), Error> {
6080
        // The guard must be dropped before notifying: `send_modify` takes
6081
        // the watch channel's internal lock, and a `wait_for` closure that
6082
        // takes `running_actions` runs while holding that same lock, so
6083
        // notifying while holding `running_actions` is an ABBA deadlock
6084
        // (issue #2672). There is no lost wakeup in the gap: the removal
6085
        // happens-before the notify.
6086
277
        let result = {
6087
277
            let mut running_actions = self.running_actions.lock();
6088
277
            running_actions.remove(operation_id).err_tip(|| 
{0
6089
0
                format!(
6090
                    "Expected operation id '{operation_id}' to exist in RunningActionsManagerImpl"
6091
                )
6092
0
            })
6093
        };
6094
277
        self.disk_reservations_kb.lock().remove(operation_id);
6095
        // No need to copy anything, we just are telling the receivers an event happened.
6096
277
        self.action_done_tx.send_modify(|()| {});
6097
277
        result.map(|_| ())
6098
277
    }
6099
6100
    // Note: We do not capture metrics on this call, only `.kill_all()`.
6101
    // Important: When the future returns the process may still be running.
6102
210
    async fn kill_operation(action: Arc<RunningActionImpl>) {
6103
210
        warn!(
6104
210
            operation_id = ?action.operation_id,
6105
            "Sending kill to running operation",
6106
        );
6107
        // The token first: a phase waiting on it (a fetch, an upload) ends
6108
        // now, whether or not the command ever started.
6109
210
        action.kill_token.send_replace(true);
6110
210
        let kill_channel_tx = {
6111
210
            let mut action_state = action.state.lock();
6112
210
            action_state.kill_channel_tx.take()
6113
        };
6114
210
        if let Some(
kill_channel_tx209
) = kill_channel_tx
6115
209
            && kill_channel_tx.send(()).is_err()
6116
        {
6117
4
            error!(
6118
4
                operation_id = ?action.operation_id,
6119
                "Error sending kill to running operation",
6120
            );
6121
206
        }
6122
210
    }
6123
6124
    /// Marks the operation as being cleaned and returns the guard that
6125
    /// clears the mark, or `None` when another cleanup already holds it.
6126
    ///
6127
    /// The guard is built only after the lock is released: its `Drop` takes
6128
    /// the same lock, so a guard made under it and thrown away (as
6129
    /// `then_some` did, whatever the insert said) deadlocked the thread on
6130
    /// its own mutex and, one by one, every runtime thread that then came to
6131
    /// clean up or start an action, while the worker's keepalives kept it
6132
    /// looking alive. The orphan sweep made the double mark routine: it marks
6133
    /// every directory it visits, and a directory whose action is mid-cleanup
6134
    /// is already marked.
6135
571
    pub fn perform_cleanup(self: &Arc<Self>, operation_id: OperationId) -> Option<CleanupGuard> {
6136
571
        let inserted = self
6137
571
            .cleaning_up_operations
6138
571
            .lock()
6139
571
            .insert(operation_id.clone());
6140
571
        inserted.then(|| CleanupGuard {
6141
563
            manager: Arc::downgrade(self),
6142
563
            operation_id,
6143
563
        })
6144
571
    }
6145
6146
    /// Takes the cleanup mark, waiting for whoever holds it to let go. The
6147
    /// wait is armed before the mark is tried, so a release in between is
6148
    /// not missed.
6149
275
    pub async fn take_cleanup_mark(self: &Arc<Self>, operation_id: OperationId) -> CleanupGuard {
6150
        loop {
6151
276
            let notified = self.cleanup_complete_notify.notified();
6152
276
            tokio::pin!(notified);
6153
276
            notified.as_mut().enable();
6154
276
            if let Some(
guard275
) = self.perform_cleanup(operation_id.clone()) {
6155
275
                return guard;
6156
1
            }
6157
1
            notified.await;
6158
        }
6159
275
    }
6160
}
6161
6162
impl RunningActionsManagerImpl {
6163
    /// Starts the memory pressure monitor once, when a pressure kill is
6164
    /// configured. It holds a weak reference, so it ends with the manager.
6165
280
    fn ensure_memory_pressure_monitor(self: &Arc<Self>) {
6166
280
        let Some(
policy3
) = self
6167
280
            .execution_configuration
6168
280
            .resource_enforcement
6169
280
            .as_ref()
6170
280
            .and_then(|enforcement| enforcement.pressure_kill)
6171
        else {
6172
277
            return;
6173
        };
6174
3
        if self
6175
3
            .pressure_monitor_started
6176
3
            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
6177
3
            .is_err()
6178
        {
6179
0
            return;
6180
3
        }
6181
        #[cfg(target_os = "linux")]
6182
        {
6183
3
            let manager = Arc::downgrade(self);
6184
3
            background_spawn!(
6185
                "memory_pressure_monitor",
6186
3
                memory_pressure_monitor(manager, policy)
6187
            );
6188
        }
6189
        #[cfg(not(target_os = "linux"))]
6190
        {
6191
            let _ = policy;
6192
            warn!(
6193
                "memory_pressure_kill_percent is set, but memory.pressure is a Linux cgroup v2 file; the pressure kill stays off"
6194
            );
6195
        }
6196
280
    }
6197
}
6198
6199
/// Reads the worker cgroup's `memory.pressure` every few seconds and, once
6200
/// its `full avg10` has stayed at or above the policy's percent for the
6201
/// policy's duration, asks the running action with the largest last sample
6202
/// to end (see [`PressureWindow`] and [`pick_pressure_victim`]). The kill
6203
/// reaches the scheduler as a memory kill, so the action comes back with a
6204
/// larger reservation instead of thrashing this worker until it is evicted.
6205
/// Ends when the manager is dropped, or at once when no `memory.pressure`
6206
/// is readable.
6207
#[cfg(target_os = "linux")]
6208
3
async fn memory_pressure_monitor(manager: Weak<RunningActionsManagerImpl>, policy: PressureKill) {
6209
3
    let Some(
path0
) = crate::capacity::memory_pressure_file() else {
6210
3
        warn!(
6211
            "no readable memory.pressure for this worker's cgroup (cgroup v2 only); the memory pressure kill stays off"
6212
        );
6213
3
        return;
6214
    };
6215
0
    info!(
6216
0
        path = %path.display(),
6217
        full_avg10_percent = policy.full_avg10_percent,
6218
0
        after_s = policy.after.as_secs(),
6219
        "Memory pressure kill armed"
6220
    );
6221
0
    let mut window = PressureWindow::new(policy.full_avg10_percent, policy.after);
6222
    loop {
6223
0
        tokio::time::sleep(MEMORY_PRESSURE_SAMPLE_INTERVAL).await;
6224
0
        let Some(manager) = manager.upgrade() else {
6225
0
            return;
6226
        };
6227
0
        let Some(full_avg10) = std::fs::read_to_string(&path)
6228
0
            .ok()
6229
0
            .and_then(|text| crate::capacity::parse_pressure_full_avg10(&text))
6230
        else {
6231
0
            continue;
6232
        };
6233
0
        if !window.observe(full_avg10, Instant::now()) {
6234
0
            continue;
6235
0
        }
6236
0
        let running: Vec<Arc<RunningActionImpl>> = manager
6237
0
            .running_actions
6238
0
            .lock()
6239
0
            .values()
6240
0
            .filter_map(Weak::upgrade)
6241
0
            .collect();
6242
0
        let Some(victim) = pick_pressure_victim(
6243
0
            running.iter().map(|action| PressureCandidate {
6244
0
                item: action,
6245
0
                last_kb: action.memory_watch.last_memory_kb(),
6246
0
                reserved_kb: action.memory_watch.reserved_memory_kb(),
6247
0
                kill_requested: action.memory_watch.kill_requested(),
6248
0
            }),
6249
0
            policy
6250
0
                .largest_reservation_kb
6251
0
                .or(manager.pod_memory_limit_kb),
6252
        ) else {
6253
0
            warn!(
6254
                full_avg10,
6255
0
                running = running.len(),
6256
                "Worker under sustained memory pressure with no sampled action to kill"
6257
            );
6258
0
            continue;
6259
        };
6260
0
        warn!(
6261
0
            operation_id = ?victim.operation_id,
6262
            full_avg10,
6263
0
            observed_kb = victim.memory_watch.last_memory_kb(),
6264
0
            reserved_kb = victim.memory_watch.reserved_memory_kb(),
6265
0
            running = running.len(),
6266
            "Worker under sustained memory pressure; killing the action furthest over its reservation, or holding the most memory"
6267
        );
6268
0
        victim.request_memory_pressure_kill(whole_percent(full_avg10));
6269
    }
6270
3
}
6271
6272
impl RunningActionsManager for RunningActionsManagerImpl {
6273
    type RunningAction = RunningActionImpl;
6274
6275
0
    async fn create_and_add_action(
6276
0
        self: &Arc<Self>,
6277
0
        worker_id: String,
6278
0
        start_execute: StartExecute,
6279
280
    ) -> Result<Arc<RunningActionImpl>, Error> {
6280
280
        self.ensure_memory_pressure_monitor();
6281
280
        self.metrics
6282
280
            .create_and_add_action
6283
280
            .wrap(async move {
6284
280
                let capture_metadata = start_execute.request_metadata.clone();
6285
280
                let capture_worker = worker_id.clone();
6286
280
                let capture_digest = start_execute.execute_request.as_ref()
6287
280
                    .and_then(|request| request.action_digest.as_ref())
6288
280
                    .map(|digest| digest.hash.clone()).unwrap_or_default();
6289
280
                let queued_timestamp = start_execute
6290
280
                    .queued_timestamp
6291
280
                    .and_then(|time| 
time216
.
try_into216
().
ok216
())
6292
280
                    .unwrap_or(SystemTime::UNIX_EPOCH);
6293
280
                let operation_id = start_execute
6294
280
                    .operation_id.as_str().into();
6295
280
                let 
action_info279
= self.create_action_info(start_execute, queued_timestamp).await
?1
;
6296
279
                debug!(
6297
                    ?action_info,
6298
                    "Worker received action",
6299
                );
6300
                // Wait for any previous cleanup to complete before creating directory
6301
279
                self.wait_for_cleanup_if_needed(&operation_id).await
?1
;
6302
278
                let action_directory = self.make_action_directory(&operation_id).await
?0
;
6303
278
                let execution_metadata = ExecutionMetadata {
6304
278
                    worker: worker_id,
6305
278
                    queued_timestamp: action_info.insert_timestamp,
6306
278
                    worker_start_timestamp: action_info.load_timestamp,
6307
278
                    worker_completed_timestamp: SystemTime::UNIX_EPOCH,
6308
278
                    input_fetch_start_timestamp: SystemTime::UNIX_EPOCH,
6309
278
                    input_fetch_completed_timestamp: SystemTime::UNIX_EPOCH,
6310
278
                    execution_start_timestamp: SystemTime::UNIX_EPOCH,
6311
278
                    execution_completed_timestamp: SystemTime::UNIX_EPOCH,
6312
278
                    output_upload_start_timestamp: SystemTime::UNIX_EPOCH,
6313
278
                    output_upload_completed_timestamp: SystemTime::UNIX_EPOCH,
6314
278
                };
6315
278
                let timeout = if action_info.timeout.is_zero() || 
self.timeout_handled_externally8
{
6316
270
                    self.max_action_timeout
6317
                } else {
6318
8
                    action_info.timeout
6319
                };
6320
                // REAPI lets a server clamp a timeout over its maximum; the
6321
                // client asked for more time, not for a rejection.
6322
278
                let timeout = if timeout > self.max_action_timeout {
6323
1
                    info!(
6324
                        ?operation_id,
6325
1
                        requested_s = timeout.as_secs_f32(),
6326
1
                        max_s = self.max_action_timeout.as_secs_f32(),
6327
                        "Action timeout clamped to the worker maximum"
6328
                    );
6329
1
                    self.max_action_timeout
6330
                } else {
6331
277
                    timeout
6332
                };
6333
278
                let running_action = Arc::new(RunningActionImpl::new(
6334
278
                    execution_metadata,
6335
278
                    operation_id.clone(),
6336
278
                    action_directory,
6337
278
                    action_info,
6338
278
                    timeout,
6339
278
                    self.clone(),
6340
                ));
6341
                {
6342
278
                    let mut running_actions = self.running_actions.lock();
6343
                    // Check if action already exists and is still alive
6344
278
                    if let Some(
existing_weak0
) = running_actions.get(&operation_id)
6345
0
                        && let Some(_existing_action) = existing_weak.upgrade() {
6346
0
                            return Err(make_err!(
6347
0
                                Code::AlreadyExists,
6348
0
                                "Action with operation_id {} is already running",
6349
0
                                operation_id
6350
0
                            ));
6351
278
                    }
6352
278
                    running_actions.insert(operation_id.clone(), Arc::downgrade(&running_action));
6353
278
                    running_action
6354
278
                        .has_manager_entry
6355
278
                        .store(true, Ordering::Release);
6356
                }
6357
278
                if let Some(
config5
) = &self.execution_configuration.buck2_file_capture
6358
5
                    && Buck2FileCapture::eligible(capture_metadata.as_ref()) {
6359
                    // The directory exists already. A cancelled registration must
6360
                    // take the same Drop cleanup path as a prepared action.
6361
4
                    running_action.did_cleanup.store(false, Ordering::Release);
6362
4
                    match Buck2FileCapture::start(
6363
4
                        config,
6364
4
                        capture_metadata.as_ref(),
6365
4
                        &capture_worker,
6366
4
                        &operation_id.to_string(),
6367
4
                        &capture_digest,
6368
4
                    ).await {
6369
4
                        Ok(Some(capture)) => {
6370
4
                            self.buck2_captures.lock().insert(operation_id, capture);
6371
4
                        }
6372
0
                        Ok(None) => {}
6373
0
                        Err(err) => {
6374
0
                            warn!(%operation_id, ?err, "Buck2 container file capture could not start");
6375
                        }
6376
                    }
6377
274
                }
6378
278
                Ok(running_action)
6379
280
            })
6380
280
            .await
6381
280
    }
6382
6383
6
    async fn cache_action_result(
6384
6
        &self,
6385
6
        action_info: DigestInfo,
6386
6
        action_result: &mut ActionResult,
6387
6
        hasher: DigestHasherFunc,
6388
6
    ) -> Result<(), Error> {
6389
6
        self.metrics
6390
6
            .cache_action_result
6391
6
            .wrap(self.upload_action_results.cache_action_result(
6392
6
                action_info,
6393
6
                action_result,
6394
6
                hasher,
6395
6
            ))
6396
6
            .await
6397
6
    }
6398
6399
4
    async fn kill_operation(&self, operation_id: &OperationId) -> Result<(), Error> {
6400
4
        let running_action = {
6401
4
            let running_actions = self.running_actions.lock();
6402
4
            running_actions
6403
4
                .get(operation_id)
6404
4
                .and_then(Weak::upgrade)
6405
4
                .ok_or_else(|| 
make_input_err!0
("Failed to get running action {operation_id}"))
?0
6406
        };
6407
4
        Self::kill_operation(running_action).await;
6408
4
        Ok(())
6409
4
    }
6410
6411
    // Note: When the future returns the process should be fully killed and cleaned up.
6412
68
    
async fn kill_all(&self)0
{
6413
68
        self.metrics
6414
68
            .kill_all
6415
68
            .wrap_no_capture_result(async move {
6416
68
                let kill_operations: Vec<Arc<RunningActionImpl>> = {
6417
68
                    let running_actions = self.running_actions.lock();
6418
68
                    running_actions.values().filter_map(Weak::upgrade).collect()
6419
                };
6420
68
                let mut kill_futures: FuturesUnordered<_> = kill_operations
6421
68
                    .into_iter()
6422
68
                    .map(Self::kill_operation)
6423
68
                    .collect();
6424
274
                while kill_futures.next().await.is_some() 
{}206
6425
68
            })
6426
68
            .await;
6427
        // Wait for the running actions to drain. Deliberately NOT
6428
        // `wait_for(|()| ...)`: that closure runs while the watch channel's
6429
        // internal lock is held, so taking `running_actions` inside it
6430
        // deadlocks with anyone notifying the channel while holding
6431
        // `running_actions` (issue #2672). Checking outside the channel's
6432
        // lock loses no wakeup: `changed()` reports any version bump since
6433
        // the last check, and `cleanup_action` removes before it notifies.
6434
68
        let mut action_done_rx = self.action_done_tx.subscribe();
6435
        loop {
6436
179
            if self.running_actions.lock().is_empty() {
6437
68
                break;
6438
111
            }
6439
            // The only sender lives on `self`, so this cannot error; if it
6440
            // ever did, giving up the wait is still the safe answer.
6441
111
            if action_done_rx.changed().await.is_err() {
6442
0
                break;
6443
111
            }
6444
        }
6445
68
    }
6446
6447
    #[inline]
6448
2
    fn metrics(&self) -> &Arc<Metrics> {
6449
2
        &self.metrics
6450
2
    }
6451
6452
0
    fn memory_peaks(&self) -> Vec<(String, u64)> {
6453
0
        self.running_actions
6454
0
            .lock()
6455
0
            .iter()
6456
0
            .filter_map(|(operation_id, action)| {
6457
0
                let peak_kb = action.upgrade()?.memory_watch.peak_memory_kb();
6458
0
                (peak_kb > 0).then(|| (operation_id.to_string(), peak_kb))
6459
0
            })
6460
0
            .collect()
6461
0
    }
6462
}
6463
6464
#[derive(Debug, Default, MetricsComponent)]
6465
pub struct Metrics {
6466
    #[metric(help = "Stats about the create_and_add_action command.")]
6467
    create_and_add_action: AsyncCounterWrapper,
6468
    #[metric(help = "Stats about the cache_action_result command.")]
6469
    cache_action_result: AsyncCounterWrapper,
6470
    #[metric(help = "Stats about the kill_all command.")]
6471
    kill_all: AsyncCounterWrapper,
6472
    #[metric(help = "Stats about the create_action_info command.")]
6473
    create_action_info: AsyncCounterWrapper,
6474
    #[metric(help = "Stats about the make_work_directory command.")]
6475
    make_action_directory: AsyncCounterWrapper,
6476
    #[metric(help = "Stats about the prepare_action command.")]
6477
    prepare_action: AsyncCounterWrapper,
6478
    #[metric(help = "Stats about the execute command.")]
6479
    execute: AsyncCounterWrapper,
6480
    #[metric(help = "Stats about the upload_results command.")]
6481
    upload_results: AsyncCounterWrapper,
6482
    #[metric(help = "Stats about the cleanup command.")]
6483
    cleanup: AsyncCounterWrapper,
6484
    #[metric(help = "Stats about the get_finished_result command.")]
6485
    get_finished_result: AsyncCounterWrapper,
6486
    #[metric(help = "Number of times an action waited for cleanup to complete.")]
6487
    cleanup_waits: CounterWithTime,
6488
    #[metric(help = "Number of stale directories removed during action retries.")]
6489
    stale_removals: CounterWithTime,
6490
    #[metric(help = "Orphaned action directories removed by the periodic sweep.")]
6491
    orphan_removals: CounterWithTime,
6492
    #[metric(help = "Orphaned action directories the sweep could not remove.")]
6493
    orphan_removal_failures: CounterWithTime,
6494
    #[metric(help = "Number of timeouts while waiting for cleanup to complete.")]
6495
    cleanup_wait_timeouts: CounterWithTime,
6496
    #[metric(help = "Stats about the get_proto_command_from_store command.")]
6497
    get_proto_command_from_store: AsyncCounterWrapper,
6498
    #[metric(help = "Stats about the download_to_directory command.")]
6499
    download_to_directory: AsyncCounterWrapper,
6500
    #[metric(help = "Stats about the prepare_output_files command.")]
6501
    prepare_output_files: AsyncCounterWrapper,
6502
    #[metric(help = "Stats about the prepare_output_paths command.")]
6503
    prepare_output_paths: AsyncCounterWrapper,
6504
    #[metric(help = "Stats about the child_process command.")]
6505
    child_process: AsyncCounterWrapper,
6506
    #[metric(help = "Stats about the child_process_success_error_code command.")]
6507
    child_process_success_error_code: CounterWithTime,
6508
    #[metric(help = "Stats about the child_process_failure_error_code command.")]
6509
    child_process_failure_error_code: CounterWithTime,
6510
    #[metric(help = "Total time spent uploading stdout.")]
6511
    upload_stdout: AsyncCounterWrapper,
6512
    #[metric(help = "Total time spent uploading stderr.")]
6513
    upload_stderr: AsyncCounterWrapper,
6514
    #[metric(help = "Total number of task timeouts.")]
6515
    task_timeouts: CounterWithTime,
6516
    #[metric(help = "Actions killed for exceeding their memory reservation.")]
6517
    memory_reservation_kills: CounterWithTime,
6518
    #[metric(help = "Actions killed because the worker's cgroup stayed under memory pressure.")]
6519
    memory_pressure_kills: CounterWithTime,
6520
    #[metric(help = "Actions refused because their disk reservation exceeded the free space.")]
6521
    disk_guard_refusals: CounterWithTime,
6522
    #[metric(help = "Actions killed for writing past their disk reservation.")]
6523
    disk_reservation_kills: CounterWithTime,
6524
    #[metric(help = "Actions served by a pooled persistent worker process.")]
6525
    persistent_worker_dispatches: CounterWithTime,
6526
    #[metric(
6527
        help = "Actions that asked for a persistent worker and ran one-shot instead: pool at capacity, spawn failure or an unsupported protocol."
6528
    )]
6529
    persistent_worker_fallbacks: CounterWithTime,
6530
    #[metric(
6531
        help = "Stats about the input-directory cache (hits, misses, subtree reuse, evictions, size)."
6532
    )]
6533
    directory_cache: Option<Weak<crate::directory_cache::DirectoryCache>>,
6534
}
6535
6536
#[cfg(test)]
6537
mod tests {
6538
    use core::sync::atomic::AtomicU32;
6539
6540
    use nativelink_macro::nativelink_test;
6541
    use pretty_assertions::assert_eq;
6542
6543
    use super::*;
6544
6545
23
    fn test_digest(first_byte: u8) -> DigestInfo {
6546
23
        let mut hash = [0u8; 32];
6547
23
        hash[0] = first_byte;
6548
23
        DigestInfo::new(hash, 1)
6549
23
    }
6550
6551
    /// Runs `fetch_and_resolve` against a resolve step that fails with
6552
    /// `fail_with` for the first `failures` calls and succeeds after, and
6553
    /// returns the result with how often each step ran.
6554
3
    async fn drive(failures: u32, fail_with: Code) -> (Result<u32, Error>, u32, u32) {
6555
3
        let populates = AtomicU32::new(0);
6556
3
        let resolves = AtomicU32::new(0);
6557
3
        let result = fetch_and_resolve(
6558
3
            test_digest(0),
6559
7
            || async {
6560
7
                populates.fetch_add(1, Ordering::Relaxed);
6561
7
                Ok(())
6562
14
            },
6563
7
            || async {
6564
7
                let call = resolves.fetch_add(1, Ordering::Relaxed) + 1;
6565
7
                if call <= failures {
6566
6
                    return Err(make_err!(fail_with, "resolve attempt {call} failed"));
6567
1
                }
6568
1
                Ok(call)
6569
14
            },
6570
        )
6571
3
        .await;
6572
3
        (
6573
3
            result,
6574
3
            populates.load(Ordering::Relaxed),
6575
3
            resolves.load(Ordering::Relaxed),
6576
3
        )
6577
3
    }
6578
6579
    #[nativelink_test]
6580
    async fn fetch_and_resolve_retries_a_missing_blob() {
6581
        let (result, populates, resolves) = drive(2, Code::NotFound).await;
6582
        assert_eq!(result.unwrap(), 3);
6583
        assert_eq!(populates, 3);
6584
        assert_eq!(resolves, 3);
6585
    }
6586
6587
    #[nativelink_test]
6588
    async fn fetch_and_resolve_gives_up_after_the_last_attempt() {
6589
        let (result, populates, resolves) = drive(3, Code::NotFound).await;
6590
        assert_eq!(result.unwrap_err().code, Code::NotFound);
6591
        assert_eq!(populates, INPUT_FETCH_ATTEMPTS);
6592
        assert_eq!(resolves, INPUT_FETCH_ATTEMPTS);
6593
    }
6594
6595
    #[nativelink_test]
6596
    async fn fetch_and_resolve_returns_other_errors_at_once() {
6597
        let (result, populates, resolves) = drive(1, Code::Internal).await;
6598
        assert_eq!(result.unwrap_err().code, Code::Internal);
6599
        assert_eq!(populates, 1);
6600
        assert_eq!(resolves, 1);
6601
    }
6602
6603
    #[nativelink_test]
6604
    async fn fetch_and_resolve_stops_when_the_fetch_fails() {
6605
        let resolves = AtomicU32::new(0);
6606
        let result: Result<(), Error> = fetch_and_resolve(
6607
            test_digest(0),
6608
2
            || async 
{1
Err(make_err!(Code::Unavailable, "slow store is down"))1
},
6609
0
            || async {
6610
0
                resolves.fetch_add(1, Ordering::Relaxed);
6611
0
                Ok(())
6612
0
            },
6613
        )
6614
        .await;
6615
        assert_eq!(result.unwrap_err().code, Code::Unavailable);
6616
        assert_eq!(resolves.load(Ordering::Relaxed), 0);
6617
    }
6618
6619
    /// The hint is reported for the first sign of trouble and then not
6620
    /// again until a full interval has passed, however many signs follow.
6621
    #[test]
6622
1
    fn input_pool_hint_is_due_once_per_interval() {
6623
1
        let hint = InputPoolHint::default();
6624
1
        let start = Instant::now();
6625
1
        assert!(hint.due(start), "the first sign is reported");
6626
1
        assert!(!hint.due(start), "a second sign at once is not");
6627
1
        let almost = start + INPUT_POOL_HINT_INTERVAL - Duration::from_secs(1);
6628
1
        assert!(!hint.due(almost), "nor one before the interval has passed");
6629
1
        let next = start + INPUT_POOL_HINT_INTERVAL;
6630
1
        assert!(hint.due(next), "one a full interval later is reported");
6631
1
        assert!(
6632
1
            !hint.due(next + Duration::from_secs(1)),
6633
            "and starts the next interval"
6634
        );
6635
1
    }
6636
6637
    #[test]
6638
1
    fn input_download_pool_is_unbounded_only_when_unset() {
6639
1
        assert_eq!(
6640
1
            input_download_pool(0).available_permits(),
6641
            Semaphore::MAX_PERMITS
6642
        );
6643
1
        assert_eq!(input_download_pool(64).available_permits(), 64);
6644
1
        assert_eq!(
6645
1
            input_download_pool(usize::MAX).available_permits(),
6646
            Semaphore::MAX_PERMITS,
6647
            "a value past what a semaphore holds is clamped, not a panic"
6648
        );
6649
1
    }
6650
6651
    #[test]
6652
1
    fn input_fetch_pause_grows_with_the_attempt_and_stays_bounded() {
6653
1
        let step = Duration::from_millis(INPUT_FETCH_PAUSE_STEP_MS);
6654
6
        for first_byte in 
[0u8, 1, 19, 20, 127, 255]1
{
6655
6
            let digest = test_digest(first_byte);
6656
12
            for attempt in 
1..INPUT_FETCH_ATTEMPTS6
{
6657
12
                let pause = input_fetch_pause(attempt, &digest);
6658
12
                assert!(pause >= step * attempt, "{pause:?} for attempt {attempt}");
6659
12
                assert!(
6660
12
                    pause < step * (attempt + 1),
6661
                    "{pause:?} for attempt {attempt}"
6662
                );
6663
            }
6664
        }
6665
1
        assert_ne!(
6666
1
            input_fetch_pause(1, &test_digest(3)),
6667
1
            input_fetch_pause(1, &test_digest(7))
6668
        );
6669
1
    }
6670
6671
    #[test]
6672
1
    fn prepare_retry_pause_doubles_and_stays_under_the_cap() {
6673
1
        let cap = Duration::from_millis(PREPARE_RETRY_PAUSE_CAP_MS);
6674
5
        for first_byte in 
[0u8, 1, 19, 127, 255]1
{
6675
5
            let digest = test_digest(first_byte);
6676
50
            for attempt in 
1..=10u325
{
6677
50
                let base = Duration::from_millis(
6678
50
                    (PREPARE_RETRY_PAUSE_MS << attempt.saturating_sub(1).min(5))
6679
50
                        .min(PREPARE_RETRY_PAUSE_CAP_MS),
6680
                );
6681
50
                let pause = prepare_retry_pause(attempt, &digest);
6682
50
                assert!(pause <= base, "{pause:?} for attempt {attempt}");
6683
50
                assert!(pause > base * 3 / 4, "{pause:?} for attempt {attempt}");
6684
50
                assert!(pause <= cap, "{pause:?} for attempt {attempt}");
6685
            }
6686
        }
6687
1
        assert_eq!(
6688
1
            prepare_retry_pause(1, &test_digest(0)),
6689
1
            Duration::from_secs(1)
6690
        );
6691
1
        assert_eq!(
6692
1
            prepare_retry_pause(2, &test_digest(0)),
6693
1
            Duration::from_secs(2)
6694
        );
6695
1
        assert_eq!(
6696
1
            prepare_retry_pause(3, &test_digest(0)),
6697
1
            Duration::from_secs(4)
6698
        );
6699
1
        assert_eq!(prepare_retry_pause(9, &test_digest(0)), cap);
6700
1
        assert_ne!(
6701
1
            prepare_retry_pause(1, &test_digest(3)),
6702
1
            prepare_retry_pause(1, &test_digest(7))
6703
        );
6704
1
    }
6705
6706
    #[test]
6707
1
    fn transient_fetch_errors_are_the_retrier_codes() {
6708
7
        for code in [
6709
1
            Code::Unavailable,
6710
1
            Code::ResourceExhausted,
6711
1
            Code::Aborted,
6712
1
            Code::Internal,
6713
1
            Code::Unknown,
6714
1
            Code::DeadlineExceeded,
6715
1
            Code::DataLoss,
6716
1
        ] {
6717
7
            let err = make_err!(code, "store answered 503 ServerBusy");
6718
7
            assert!(is_transient_fetch_error(&err), "{code:?}");
6719
        }
6720
9
        for code in [
6721
1
            Code::NotFound,
6722
1
            Code::InvalidArgument,
6723
1
            Code::FailedPrecondition,
6724
1
            Code::PermissionDenied,
6725
1
            Code::Unauthenticated,
6726
1
            Code::Unimplemented,
6727
1
            Code::OutOfRange,
6728
1
            Code::AlreadyExists,
6729
1
            Code::Cancelled,
6730
1
        ] {
6731
9
            let err = make_err!(code, "store answered");
6732
9
            assert!(!is_transient_fetch_error(&err), "{code:?}");
6733
        }
6734
1
    }
6735
6736
    /// The worker's own disk failing maps to the same codes a store
6737
    /// failure does, but is never retried on this worker. The error
6738
    /// numbers are Linux's (ENOSPC, EPIPE), so that part runs on unix.
6739
    #[test]
6740
1
    fn an_operating_system_error_is_not_a_transient_fetch_error() {
6741
        #[cfg(target_family = "unix")]
6742
        {
6743
1
            let disk_full = Error::from(std::io::Error::from_raw_os_error(28));
6744
1
            assert_eq!(disk_full.code, Code::Unknown);
6745
1
            assert!(!is_transient_fetch_error(&disk_full), "{disk_full:?}");
6746
1
            let broken_pipe =
6747
1
                Error::from(std::io::Error::from_raw_os_error(32)).append("Writing an input");
6748
1
            assert_eq!(broken_pipe.code, Code::Internal);
6749
1
            assert!(!is_transient_fetch_error(&broken_pipe), "{broken_pipe:?}");
6750
        }
6751
        // What the standard library prints for any OS error, whatever the
6752
        // platform's number for it.
6753
1
        let os_error = Error::from(std::io::Error::other(
6754
            "No space left on device (os error 28)",
6755
        ));
6756
1
        assert_eq!(os_error.code, Code::Internal);
6757
1
        assert!(!is_transient_fetch_error(&os_error), "{os_error:?}");
6758
        // A store that reset the connection is not this worker's disk.
6759
1
        let reset = Error::from(std::io::Error::from(std::io::ErrorKind::ConnectionReset));
6760
1
        assert_eq!(reset.code, Code::Unavailable);
6761
1
        assert!(is_transient_fetch_error(&reset), "{reset:?}");
6762
1
    }
6763
6764
    /// Every retried fetch settles on one terminal outcome, dropped ones
6765
    /// included; a fetch never retried records nothing.
6766
    #[test]
6767
1
    fn prepare_retries_settle_once() {
6768
1
        let recorded = std::cell::RefCell::new(Vec::new());
6769
5
        let 
record1
= |outcome: &'static str| recorded.borrow_mut().push(outcome);
6770
6771
1
        let mut retries = PrepareRetries::new(record);
6772
1
        retries.retried();
6773
1
        retries.retried();
6774
1
        retries.settle("recovered");
6775
1
        assert_eq!(*recorded.borrow(), vec!["retried", "retried", "recovered"]);
6776
6777
1
        recorded.borrow_mut().clear();
6778
1
        PrepareRetries::new(record).settle("failed");
6779
1
        assert!(recorded.borrow().is_empty(), "no retry, nothing to settle");
6780
6781
1
        recorded.borrow_mut().clear();
6782
1
        {
6783
1
            let mut retries = PrepareRetries::new(record);
6784
1
            retries.retried();
6785
1
        }
6786
1
        assert_eq!(*recorded.borrow(), vec!["retried", "abandoned"]);
6787
1
    }
6788
}