Coverage Report

Created: 2026-09-18 20:40

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-config/src/schedulers.rs
Line
Count
Source
1
// Copyright 2024 The NativeLink Authors. 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 std::collections::HashMap;
16
17
#[cfg(feature = "dev-schema")]
18
use schemars::JsonSchema;
19
use serde::{Deserialize, Serialize};
20
21
use crate::serde_utils::{
22
    convert_duration_with_shellexpand, convert_duration_with_shellexpand_and_negative,
23
    convert_numeric_with_shellexpand, convert_string_with_shellexpand,
24
};
25
use crate::stores::{GrpcEndpoint, Retry, StoreRefName};
26
27
#[derive(Deserialize, Serialize, Debug)]
28
#[serde(rename_all = "snake_case")]
29
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
30
pub enum SchedulerSpec {
31
    Simple(SimpleSpec),
32
    Grpc(GrpcSpec),
33
    CacheLookup(CacheLookupSpec),
34
    PropertyModifier(PropertyModifierSpec),
35
    HistoricalResource(HistoricalResourceSpec),
36
}
37
38
/// When the scheduler matches tasks to workers that are capable of running
39
/// the task, this value will be used to determine how the property is treated.
40
#[derive(Deserialize, Serialize, Debug, Clone, Copy, Hash, Eq, PartialEq)]
41
#[serde(rename_all = "snake_case")]
42
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
43
pub enum PropertyType {
44
    /// Requires the platform property to be a u64 and when the scheduler looks
45
    /// for appropriate worker nodes that are capable of executing the task,
46
    /// the task will not run on a node that has less than this value.
47
    Minimum,
48
49
    /// Requires the platform property to be a string and when the scheduler
50
    /// looks for appropriate worker nodes that are capable of executing the
51
    /// task, the task will not run on a node that does not have this property
52
    /// set to the value with exact string match.
53
    Exact,
54
55
    /// Does not restrict on this value and instead will be passed to the worker
56
    /// as an informational piece.
57
    /// TODO(palfrey) In the future this will be used by the scheduler and worker
58
    /// to cause the scheduler to prefer certain workers over others, but not
59
    /// restrict them based on these values.
60
    Priority,
61
62
    //// Allows jobs to be requested with said key, but without requiring workers
63
    //// to have that key
64
    Ignore,
65
}
66
67
/// When a worker is being searched for to run a job, this will be used
68
/// on how to choose which worker should run the job when multiple
69
/// workers are able to run the task.
70
#[derive(Copy, Clone, Deserialize, Serialize, Debug, Default)]
71
#[serde(rename_all = "snake_case")]
72
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
73
pub enum WorkerAllocationStrategy {
74
    /// Prefer workers that have been least recently used to run a job.
75
    #[default]
76
    LeastRecentlyUsed,
77
    /// Prefer workers that have been most recently used to run a job.
78
    MostRecentlyUsed,
79
}
80
81
// defaults to every 10s
82
13
const fn default_worker_match_logging_interval_s() -> i64 {
83
13
    10
84
13
}
85
86
// defaults to every 5s
87
15
const fn default_fallback_match_interval_s() -> i64 {
88
15
    5
89
15
}
90
91
#[derive(Deserialize, Serialize, Debug, Default)]
92
#[serde(deny_unknown_fields)]
93
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
94
pub struct SimpleSpec {
95
    /// A list of supported platform properties mapped to how these properties
96
    /// are used when the scheduler looks for worker nodes capable of running
97
    /// the task.
98
    ///
99
    /// For example, a value of:
100
    /// ```json
101
    /// { "cpu_count": "minimum", "cpu_arch": "exact" }
102
    /// ```
103
    /// With a job that contains:
104
    /// ```json
105
    /// { "cpu_count": "8", "cpu_arch": "arm" }
106
    /// ```
107
    /// Will result in the scheduler filtering out any workers that do not have
108
    /// `"cpu_arch" = "arm"` and filter out any workers that have less than 8 CPU
109
    /// cores available.
110
    ///
111
    /// The property names here must match the property keys provided by the
112
    /// worker nodes when they join the pool. In other words, the workers will
113
    /// publish their capabilities to the scheduler when they join the worker
114
    /// pool. If the worker fails to notify the scheduler of its (for example)
115
    /// `"cpu_arch"`, the scheduler will never send any jobs to it, if all jobs
116
    /// have the `"cpu_arch"` label. We have no special treatment of any platform
117
    /// property labels other and entirely driven by worker configs and this
118
    /// config.
119
    ///
120
    /// Properties that are not listed here are matched dynamically: workers
121
    /// that declare the key must match the value exactly, and workers that do
122
    /// not declare the key are not restricted by it. List a property here to
123
    /// enforce stricter matching.
124
    pub supported_platform_properties: Option<HashMap<String, PropertyType>>,
125
126
    /// The amount of time to retain completed actions for in case
127
    /// a `WaitExecution` is called after the action has completed.
128
    /// Default: 60 seconds
129
    #[serde(default, deserialize_with = "convert_duration_with_shellexpand")]
130
    pub retain_completed_for_s: u32,
131
132
    /// Mark operations as completed with error if no client has updated them
133
    /// within this duration.
134
    /// Default: 60 seconds
135
    #[serde(default, deserialize_with = "convert_duration_with_shellexpand")]
136
    pub client_action_timeout_s: u64,
137
138
    /// Periodically count the actions in each stage and report them as the
139
    /// `execution.active.count` metric.
140
    ///
141
    /// Only has an effect when scheduler state lives in a store, which today
142
    /// means Redis; the in-memory scheduler always maintains this count because
143
    /// it already holds the state in process. The count is a query against the
144
    /// scheduler store every 15 seconds from every scheduler replica, so it
145
    /// adds load to the same backend that serves action scheduling. Leave it
146
    /// off unless you want the metric.
147
    ///
148
    /// Every replica reports the same store-wide totals, so aggregate the
149
    /// series across replicas with `max`, not `sum`.
150
    /// Default: false
151
    #[serde(default)]
152
    pub enable_active_action_count_metric: bool,
153
154
    /// Remove workers from pool once the worker has not responded in this
155
    /// amount of time in seconds.
156
    /// Default: 5 seconds
157
    #[serde(default, deserialize_with = "convert_duration_with_shellexpand")]
158
    pub worker_timeout_s: u64,
159
160
    /// Maximum time (seconds) an action can stay in Executing state without
161
    /// any worker update before being timed out and re-queued.
162
    /// This applies regardless of worker keepalive status, catching cases
163
    /// where a worker is alive (sending keepalives) but stuck on a specific
164
    /// action. Set to 0 to disable (relies only on `worker_timeout_s`).
165
    ///
166
    /// Default: 0 (disabled)
167
    #[serde(default, deserialize_with = "convert_duration_with_shellexpand")]
168
    pub max_action_executing_timeout_s: u64,
169
170
    /// Evict a worker that has not reported back on an operation it was
171
    /// told to kill within this many seconds. A healthy worker
172
    /// acknowledges a kill in moments; one that cannot is wedged, and its
173
    /// keepalives would otherwise keep `worker_timeout_s` from ever firing
174
    /// while the dead operation holds its slot. Eviction requeues the
175
    /// worker's other operations.
176
    /// Default: 60 seconds
177
    #[serde(default, deserialize_with = "convert_duration_with_shellexpand")]
178
    pub unacknowledged_kill_timeout_s: u64,
179
180
    /// If a job returns an internal error or times out this many times when
181
    /// attempting to run on a worker the scheduler will return the last error
182
    /// to the client. Jobs will be retried and this configuration is to help
183
    /// prevent one rogue job from infinitely retrying and taking up a lot of
184
    /// resources when the task itself is the one causing the server to go
185
    /// into a bad state.
186
    /// Default: 3
187
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
188
    pub max_job_retries: usize,
189
190
    /// The strategy used to assign workers jobs.
191
    #[serde(default)]
192
    pub allocation_strategy: WorkerAllocationStrategy,
193
194
    /// The storage backend to use for the scheduler.
195
    /// Default: memory
196
    pub experimental_backend: Option<ExperimentalSimpleSchedulerBackend>,
197
198
    /// Every N seconds, do logging of worker matching
199
    /// e.g. "worker busy", "can't find any worker"
200
    /// Defaults to 10s. Can be set to -1 to disable
201
    #[serde(
202
        default = "default_worker_match_logging_interval_s",
203
        deserialize_with = "convert_duration_with_shellexpand_and_negative"
204
    )]
205
    pub worker_match_logging_interval_s: i64,
206
207
    /// Every N seconds, run a worker matching pass even if no task or worker
208
    /// change notification arrived. This is a safety net for missed
209
    /// notifications and for scheduler backends with eventually consistent
210
    /// searches (for example Redis), where an operation that was re-queued
211
    /// may not be visible to the search triggered by its own notification.
212
    /// Without this, such an operation can stay queued until an unrelated
213
    /// event triggers another matching pass.
214
    /// Defaults to 5s. Zero or any negative value disables it.
215
    #[serde(
216
        default = "default_fallback_match_interval_s",
217
        deserialize_with = "convert_duration_with_shellexpand_and_negative"
218
    )]
219
    pub fallback_match_interval_s: i64,
220
}
221
222
#[derive(Deserialize, Serialize, Debug)]
223
#[serde(rename_all = "snake_case")]
224
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
225
pub enum ExperimentalSimpleSchedulerBackend {
226
    /// Use an in-memory store for the scheduler.
227
    Memory,
228
    /// Use a redis store for the scheduler.
229
    Redis(ExperimentalRedisSchedulerBackend),
230
}
231
232
#[derive(Deserialize, Serialize, Debug, Default)]
233
#[serde(deny_unknown_fields)]
234
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
235
pub struct ExperimentalRedisSchedulerBackend {
236
    /// A reference to the redis store to use for the scheduler.
237
    /// Note: This MUST resolve to a `RedisSpec`.
238
    pub redis_store: StoreRefName,
239
}
240
241
/// A scheduler that forwards requests to an upstream scheduler. This
242
/// is useful to use when doing some kind of local action cache or CAS away from
243
/// the main cluster of workers. In general, it's more efficient to point the
244
/// build at the main scheduler directly though.
245
#[derive(Deserialize, Serialize, Debug)]
246
#[serde(deny_unknown_fields)]
247
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
248
pub struct GrpcSpec {
249
    /// The upstream scheduler to forward requests to.
250
    pub endpoint: GrpcEndpoint,
251
252
    /// Retry configuration to use when a network request fails.
253
    #[serde(default)]
254
    pub retry: Retry,
255
256
    /// Limit the number of simultaneous upstream requests to this many. A
257
    /// value of zero is treated as unlimited. If the limit is reached the
258
    /// request is queued.
259
    /// Default: unlimited
260
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
261
    pub max_concurrent_requests: usize,
262
263
    /// The number of connections to make to each specified endpoint to balance
264
    /// the load over multiple TCP connections.
265
    /// Default: 1.
266
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
267
    pub connections_per_endpoint: usize,
268
}
269
270
#[derive(Deserialize, Serialize, Debug)]
271
#[serde(deny_unknown_fields)]
272
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
273
pub struct CacheLookupSpec {
274
    /// The reference to the action cache store used to return cached
275
    /// actions from rather than running them again.
276
    /// To prevent unintended issues, this store should probably be a `CompletenessCheckingSpec`.
277
    pub ac_store: StoreRefName,
278
279
    /// The nested scheduler to use if cache lookup fails.
280
    pub scheduler: Box<SchedulerSpec>,
281
}
282
283
#[derive(Deserialize, Serialize, Debug, Clone)]
284
#[serde(deny_unknown_fields)]
285
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
286
pub struct PlatformPropertyAddition {
287
    /// The name of the property to add.
288
    pub name: String,
289
    /// The value to assign to the property.
290
    pub value: String,
291
}
292
293
#[derive(Deserialize, Serialize, Debug, Clone)]
294
#[serde(deny_unknown_fields)]
295
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
296
pub struct PlatformPropertyReplacement {
297
    /// The name of the property to replace.
298
    pub name: String,
299
    /// The value to match against, if unset then any instance matches.
300
    #[serde(default)]
301
    pub value: Option<String>,
302
    /// The new name of the property.
303
    pub new_name: String,
304
    /// The value to assign to the property, if unset will remain the same.
305
    #[serde(default)]
306
    pub new_value: Option<String>,
307
}
308
309
#[derive(Deserialize, Serialize, Debug, Clone)]
310
#[serde(rename_all = "snake_case")]
311
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
312
pub enum PropertyModification {
313
    /// Add a property to the action properties.
314
    Add(PlatformPropertyAddition),
315
    /// Remove a named property from the action.
316
    Remove(String),
317
    /// If a property is found, then replace it with another one.
318
    Replace(PlatformPropertyReplacement),
319
}
320
321
#[derive(Deserialize, Serialize, Debug)]
322
#[serde(deny_unknown_fields)]
323
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
324
pub struct PropertyModifierSpec {
325
    /// A list of modifications to perform to incoming actions for the nested
326
    /// scheduler. These are performed in order and blindly, so removing a
327
    /// property that doesn't exist is fine and overwriting an existing property
328
    /// is also fine. If adding properties that do not exist in the nested
329
    /// scheduler is not supported and will likely cause unexpected behaviour.
330
    pub modifications: Vec<PropertyModification>,
331
332
    /// The nested scheduler to use after modifying the properties.
333
    pub scheduler: Box<SchedulerSpec>,
334
}
335
336
0
const fn default_historical_resource_refresh_interval_s() -> u64 {
337
0
    30
338
0
}
339
340
0
fn default_historical_resource_cpu_property_name() -> String {
341
0
    "cpu_count".to_string()
342
0
}
343
344
0
fn default_historical_resource_memory_property_name() -> String {
345
0
    "memory_kb".to_string()
346
0
}
347
348
#[derive(Deserialize, Serialize, Debug)]
349
#[serde(deny_unknown_fields)]
350
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
351
pub struct HistoricalResourceSpec {
352
    /// JSON file containing historical resource hints keyed by Bazel
353
    /// `RequestMetadata` `target_id` and/or `action_mnemonic`.
354
    ///
355
    /// Supported file shapes:
356
    /// ```json
357
    /// [
358
    ///   { "target_id": "//pkg:test", "action_mnemonic": "TestRunner", "cpu_count": 2, "memory_kb": 12582912 }
359
    /// ]
360
    /// ```
361
    /// or:
362
    /// ```json
363
    /// { "hints": [ ... ] }
364
    /// ```
365
    #[serde(deserialize_with = "convert_string_with_shellexpand")]
366
    pub hints_file: String,
367
368
    /// Reload interval for `hints_file`. Set to 0 to load once.
369
    /// Default: 30 seconds
370
    #[serde(
371
        default = "default_historical_resource_refresh_interval_s",
372
        deserialize_with = "convert_duration_with_shellexpand"
373
    )]
374
    pub refresh_interval_s: u64,
375
376
    /// Platform property name used for CPU minimums.
377
    /// Default: `cpu_count`
378
    #[serde(
379
        default = "default_historical_resource_cpu_property_name",
380
        deserialize_with = "convert_string_with_shellexpand"
381
    )]
382
    pub cpu_property_name: String,
383
384
    /// Platform property name used for memory minimums, expressed in KiB.
385
    /// Default: `memory_kb`
386
    #[serde(
387
        default = "default_historical_resource_memory_property_name",
388
        deserialize_with = "convert_string_with_shellexpand"
389
    )]
390
    pub memory_property_name: String,
391
392
    /// The nested scheduler to use after applying resource hints.
393
    pub scheduler: Box<SchedulerSpec>,
394
}