Coverage Report

Created: 2026-11-13 23:29

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-scheduler/src/default_scheduler_factory.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::time::Duration;
16
use std::sync::Arc;
17
use std::time::SystemTime;
18
19
use nativelink_config::schedulers::{
20
    ExperimentalSimpleSchedulerBackend, PropertyType, SchedulerSpec, SimpleSpec,
21
};
22
use nativelink_config::stores::EvictionPolicy;
23
use nativelink_error::{Error, ResultExt, make_input_err};
24
use nativelink_proto::com::github::trace_machina::nativelink::events::OriginEvent;
25
use nativelink_store::redis_store::{RedisStore, StandardRedisManager};
26
use nativelink_store::store_manager::StoreManager;
27
use nativelink_util::instant_wrapper::InstantWrapper;
28
use redis::aio::ConnectionManager;
29
use tokio::sync::{Notify, mpsc};
30
use tracing::warn;
31
32
use crate::cache_lookup_scheduler::CacheLookupScheduler;
33
use crate::grpc_scheduler::GrpcScheduler;
34
use crate::historical_resource_scheduler::{HistoricalResourceScheduler, replica_id, snapshot_key};
35
use crate::known_platform_property_provider::KnownPlatformPropertyProvider;
36
use crate::memory_awaited_action_db::MemoryAwaitedActionDb;
37
use crate::property_modifier_scheduler::PropertyModifierScheduler;
38
use crate::resource_sketch::ResourceSketch;
39
use crate::simple_scheduler::SimpleScheduler;
40
use crate::store_awaited_action_db::StoreAwaitedActionDb;
41
use crate::worker_scheduler::WorkerScheduler;
42
43
/// Default timeout for recently completed actions in seconds.
44
/// If this changes, remember to change the documentation in the config.
45
const DEFAULT_RETAIN_COMPLETED_FOR_S: u32 = 60;
46
47
/// The action scheduler, the worker scheduler, and the resource sketch of a
48
/// `historical_resource` layer when the configuration has one (the admin
49
/// API serves it).
50
pub type SchedulerFactoryResults = (
51
    Option<Arc<dyn KnownPlatformPropertyProvider>>,
52
    Option<Arc<dyn WorkerScheduler>>,
53
    Option<Arc<ResourceSketch>>,
54
);
55
56
0
pub async fn scheduler_factory(
57
0
    spec: &SchedulerSpec,
58
0
    store_manager: &StoreManager,
59
0
    maybe_origin_event_tx: Option<&mpsc::Sender<OriginEvent>>,
60
5
) -> Result<SchedulerFactoryResults, Error> {
61
5
    inner_scheduler_factory(spec, store_manager, maybe_origin_event_tx, None).await
62
5
}
63
64
/// `resource_sketch` is the sketch of the `historical_resource` layer above
65
/// this spec, if any, handed down so the worker scheduler feeds it.
66
10
async fn inner_scheduler_factory(
67
10
    spec: &SchedulerSpec,
68
10
    store_manager: &StoreManager,
69
10
    maybe_origin_event_tx: Option<&mpsc::Sender<OriginEvent>>,
70
10
    resource_sketch: Option<Arc<ResourceSketch>>,
71
10
) -> Result<SchedulerFactoryResults, Error> {
72
10
    let scheduler: SchedulerFactoryResults = match spec {
73
5
        SchedulerSpec::Simple(spec) => {
74
5
            simple_scheduler_factory(
75
5
                spec,
76
5
                store_manager,
77
5
                SystemTime::now,
78
5
                maybe_origin_event_tx,
79
5
                resource_sketch,
80
5
            )
81
5
            .await
?0
82
        }
83
0
        SchedulerSpec::Grpc(spec) => (Some(Arc::new(GrpcScheduler::new(spec)?)), None, None),
84
0
        SchedulerSpec::CacheLookup(spec) => {
85
0
            let ac_store = store_manager
86
0
                .get_store(&spec.ac_store)
87
0
                .err_tip(|| format!("'ac_store': '{}' does not exist", spec.ac_store))?;
88
0
            let (action_scheduler, worker_scheduler, sketch) = Box::pin(inner_scheduler_factory(
89
0
                &spec.scheduler,
90
0
                store_manager,
91
0
                maybe_origin_event_tx,
92
0
                resource_sketch,
93
0
            ))
94
0
            .await
95
0
            .err_tip(|| "In nested CacheLookupScheduler construction")?;
96
0
            let cache_lookup_scheduler = Arc::new(CacheLookupScheduler::new(
97
0
                ac_store,
98
0
                action_scheduler.err_tip(|| "Nested scheduler is not an action scheduler")?,
99
0
            )?);
100
0
            (Some(cache_lookup_scheduler), worker_scheduler, sketch)
101
        }
102
0
        SchedulerSpec::PropertyModifier(spec) => {
103
0
            let (action_scheduler, worker_scheduler, sketch) = Box::pin(inner_scheduler_factory(
104
0
                &spec.scheduler,
105
0
                store_manager,
106
0
                maybe_origin_event_tx,
107
0
                resource_sketch,
108
0
            ))
109
0
            .await
110
0
            .err_tip(|| "In nested PropertyModifierScheduler construction")?;
111
0
            let property_modifier_scheduler = Arc::new(PropertyModifierScheduler::new(
112
0
                spec,
113
0
                action_scheduler.err_tip(|| "Nested scheduler is not an action scheduler")?,
114
            ));
115
0
            (Some(property_modifier_scheduler), worker_scheduler, sketch)
116
        }
117
5
        SchedulerSpec::HistoricalResource(spec) => {
118
5
            HistoricalResourceScheduler::validate(spec)
?0
;
119
            // A number the scheduler writes into a property the nested
120
            // scheduler does not treat as a minimum is matched as an exact
121
            // string, and no worker advertises that string.
122
5
            if let SchedulerSpec::Simple(simple) = spec.scheduler.as_ref()
123
5
                && let Some(
declared0
) = &simple.supported_platform_properties
124
            {
125
0
                let cold = spec.cold_start.unwrap_or_default();
126
0
                let dimensions = [
127
                    (
128
0
                        &spec.cpu_property_name,
129
0
                        spec.classes.iter().any(|c| c.cpu_count > 0) || cold.cpu_count > 0,
130
                    ),
131
                    (
132
0
                        &spec.memory_property_name,
133
0
                        spec.classes.iter().any(|c| c.memory_kb > 0) || cold.memory_kb > 0,
134
                    ),
135
                    (
136
0
                        &spec.disk_property_name,
137
0
                        spec.classes.iter().any(|c| c.disk_kb > 0) || cold.disk_kb > 0,
138
                    ),
139
                ];
140
0
                for (name, used) in dimensions {
141
0
                    if used && declared.get(name) != Some(&PropertyType::Minimum) {
142
0
                        return Err(make_input_err!(
143
0
                            "historical_resource reserves {name} but the nested scheduler does not declare it as a minimum property; add it to supported_platform_properties as minimum"
144
0
                        ));
145
0
                    }
146
                }
147
5
            }
148
            // When learning is enabled, the worker
149
            // scheduler below feeds it, this layer reads it, and the admin
150
            // API lists it. A snapshot in the named store is what a restart
151
            // starts from; the hints file only seeds an empty sketch.
152
5
            let sketch = Arc::new(HistoricalResourceScheduler::sketch_from_spec(spec));
153
5
            let mut snapshot_store = match (&spec.snapshot_store, spec.learn_from_workers) {
154
3
                (Some(name), true) => {
155
3
                    let store = store_manager.get_store(name);
156
3
                    if store.is_none() {
157
1
                        warn!(
158
                            snapshot_store = name,
159
                            "Historical resource snapshot store is not defined in this scheduler's stores; continuing with memory-only learning until restart"
160
                        );
161
2
                    }
162
3
                    store
163
                }
164
2
                _ => None,
165
            };
166
5
            let snapshot_key = snapshot_key(&replica_id(&spec.replica_id));
167
5
            if let Some(
store2
) = &snapshot_store {
168
2
                match HistoricalResourceScheduler::probe_snapshot_store(
169
2
                    store,
170
2
                    spec.snapshot_store.as_deref().unwrap_or_default(),
171
                )
172
2
                .await
173
                {
174
                    Ok(()) => {
175
1
                        HistoricalResourceScheduler::load_snapshots(&sketch, store, &snapshot_key)
176
1
                            .await;
177
                    }
178
1
                    Err(error) => {
179
1
                        warn!(
180
                            ?error,
181
                            "Historical resource snapshot store probe failed; continuing with memory-only learning until restart"
182
                        );
183
1
                        snapshot_store = None;
184
                    }
185
                }
186
3
            }
187
5
            let (action_scheduler, worker_scheduler, _) = Box::pin(inner_scheduler_factory(
188
5
                &spec.scheduler,
189
5
                store_manager,
190
5
                maybe_origin_event_tx,
191
5
                spec.learn_from_workers.then(|| 
sketch4
.
clone4
()),
192
            ))
193
5
            .await
194
5
            .err_tip(|| "In nested HistoricalResourceScheduler construction")
?0
;
195
5
            let historical_resource_scheduler = Arc::new(HistoricalResourceScheduler::new(
196
5
                spec,
197
5
                action_scheduler.err_tip(|| "Nested scheduler is not an action scheduler")
?0
,
198
5
                sketch.clone(),
199
            ));
200
5
            if spec.learn_from_workers {
201
4
                HistoricalResourceScheduler::spawn_snapshot_writer(
202
4
                    sketch.clone(),
203
4
                    snapshot_store,
204
4
                    Duration::from_secs(spec.snapshot_interval_s),
205
4
                    snapshot_key,
206
4
                );
207
4
            
}1
208
5
            (
209
5
                Some(historical_resource_scheduler),
210
5
                worker_scheduler,
211
5
                spec.learn_from_workers.then_some(sketch),
212
5
            )
213
        }
214
    };
215
216
10
    Ok(scheduler)
217
10
}
218
219
5
async fn simple_scheduler_factory(
220
5
    spec: &SimpleSpec,
221
5
    store_manager: &StoreManager,
222
5
    now_fn: fn() -> SystemTime,
223
5
    maybe_origin_event_tx: Option<&mpsc::Sender<OriginEvent>>,
224
5
    resource_sketch: Option<Arc<ResourceSketch>>,
225
5
) -> Result<SchedulerFactoryResults, Error> {
226
5
    if let Some(
policy0
) = &spec.memory_escalation
227
0
        && policy.percent <= 100
228
    {
229
0
        return Err(make_input_err!(
230
0
            "memory_escalation.percent must be above 100 to grow the reservation, got {}",
231
0
            policy.percent
232
0
        ));
233
5
    }
234
5
    if let Some(
hold0
) = &spec.reservation_hold
235
0
        && (hold.hold_after_s == 0 || hold.max_hold_s == 0)
236
    {
237
0
        return Err(make_input_err!(
238
0
            "reservation_hold.hold_after_s and max_hold_s must be above zero, got {} and {}: a zero wait holds for any action on its first miss, and a zero hold expires in the pass that took it and wakes the next",
239
0
            hold.hold_after_s,
240
0
            hold.max_hold_s
241
0
        ));
242
5
    }
243
5
    if let Some(
tripwire0
) = &spec.worker_outcome_tripwire
244
0
        && (tripwire.min_failures == 0
245
0
            || tripwire.window < tripwire.min_failures
246
0
            || tripwire.failure_percent > 100
247
0
            || tripwire.max_drained_percent > 100)
248
    {
249
0
        return Err(make_input_err!(
250
0
            "worker_outcome_tripwire needs min_failures above zero and at most window, and percentages at most 100, got window {}, min_failures {}, failure_percent {}, max_drained_percent {}",
251
0
            tripwire.window,
252
0
            tripwire.min_failures,
253
0
            tripwire.failure_percent,
254
0
            tripwire.max_drained_percent
255
0
        ));
256
5
    }
257
5
    match spec
258
5
        .experimental_backend
259
5
        .as_ref()
260
5
        .unwrap_or(&ExperimentalSimpleSchedulerBackend::Memory)
261
    {
262
        ExperimentalSimpleSchedulerBackend::Memory => {
263
5
            let task_change_notify = Arc::new(Notify::new());
264
5
            let awaited_action_db = memory_awaited_action_db_factory(
265
5
                spec.retain_completed_for_s,
266
5
                &task_change_notify,
267
                SystemTime::now,
268
            );
269
5
            let (action_scheduler, worker_scheduler) = SimpleScheduler::new_with_resource_sketch(
270
5
                spec,
271
5
                awaited_action_db,
272
5
                task_change_notify,
273
5
                maybe_origin_event_tx.cloned(),
274
5
                resource_sketch,
275
5
            );
276
5
            Ok((Some(action_scheduler), Some(worker_scheduler), None))
277
        }
278
0
        ExperimentalSimpleSchedulerBackend::Redis(redis_config) => {
279
0
            let store = store_manager
280
0
                .get_store(redis_config.redis_store.as_ref())
281
0
                .err_tip(|| {
282
0
                    format!(
283
                        "'redis_store': '{}' does not exist",
284
                        redis_config.redis_store
285
                    )
286
0
                })?;
287
0
            let task_change_notify = Arc::new(Notify::new());
288
0
            let store = store
289
0
                .into_inner()
290
0
                .as_any_arc()
291
0
                .downcast::<RedisStore<ConnectionManager, StandardRedisManager<ConnectionManager>>>(
292
                )
293
0
                .map_err(|_| {
294
0
                    make_input_err!(
295
                        "Could not downcast to redis store in RedisAwaitedActionDb::new"
296
                    )
297
0
                })?;
298
0
            let awaited_action_db = StoreAwaitedActionDb::new(
299
0
                store,
300
0
                task_change_notify.clone(),
301
0
                now_fn,
302
                Default::default,
303
                // Passed through as 0, the store wrote every completed
304
                // record with no expiry (the Lua update treats 0 as
305
                // forever), so the scheduler's Redis kept every action ever
306
                // run and the search index grew without bound.
307
0
                retain_completed_for_s(spec.retain_completed_for_s),
308
                // Same normalisation SimpleScheduler applies, so the
309
                // keepalive stops being kept exactly when the timeout that
310
                // reads it comes due.
311
0
                if spec.client_action_timeout_s == 0 {
312
0
                    crate::simple_scheduler::DEFAULT_CLIENT_ACTION_TIMEOUT_S
313
                } else {
314
0
                    spec.client_action_timeout_s
315
                },
316
0
                spec.enable_active_action_count_metric,
317
            )
318
0
            .await
319
0
            .err_tip(|| "In state_manager_factory::redis_state_manager")?;
320
0
            let (action_scheduler, worker_scheduler) = SimpleScheduler::new_with_resource_sketch(
321
0
                spec,
322
0
                awaited_action_db,
323
0
                task_change_notify,
324
0
                maybe_origin_event_tx.cloned(),
325
0
                resource_sketch,
326
0
            );
327
0
            Ok((Some(action_scheduler), Some(worker_scheduler), None))
328
        }
329
    }
330
5
}
331
332
/// How long a completed record is kept, whichever backend holds it: the
333
/// configured value, or the same default for both when it is unset.
334
#[must_use]
335
190
pub const fn retain_completed_for_s(configured: u32) -> u32 {
336
190
    if configured == 0 {
337
170
        DEFAULT_RETAIN_COMPLETED_FOR_S
338
    } else {
339
20
        configured
340
    }
341
190
}
342
343
188
pub fn memory_awaited_action_db_factory<I, NowFn>(
344
188
    configured_retain_completed_for_s: u32,
345
188
    task_change_notify: &Arc<Notify>,
346
188
    now_fn: NowFn,
347
188
) -> MemoryAwaitedActionDb<I, NowFn>
348
188
where
349
188
    I: InstantWrapper,
350
188
    NowFn: Fn() -> I + Clone + Send + Sync + 'static,
351
{
352
188
    MemoryAwaitedActionDb::new(
353
188
        &EvictionPolicy {
354
188
            max_seconds: retain_completed_for_s(configured_retain_completed_for_s),
355
188
            ..Default::default()
356
188
        },
357
188
        task_change_notify.clone(),
358
188
        now_fn,
359
    )
360
188
}