Coverage Report

Created: 2026-07-21 15:28

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
12
const fn default_worker_match_logging_interval_s() -> i64 {
83
12
    10
84
12
}
85
86
// defaults to every 5s
87
14
const fn default_fallback_match_interval_s() -> i64 {
88
14
    5
89
14
}
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
    pub supported_platform_properties: Option<HashMap<String, PropertyType>>,
120
121
    /// The amount of time to retain completed actions for in case
122
    /// a `WaitExecution` is called after the action has completed.
123
    /// Default: 60 seconds
124
    #[serde(default, deserialize_with = "convert_duration_with_shellexpand")]
125
    pub retain_completed_for_s: u32,
126
127
    /// Mark operations as completed with error if no client has updated them
128
    /// within this duration.
129
    /// Default: 60 seconds
130
    #[serde(default, deserialize_with = "convert_duration_with_shellexpand")]
131
    pub client_action_timeout_s: u64,
132
133
    /// Remove workers from pool once the worker has not responded in this
134
    /// amount of time in seconds.
135
    /// Default: 5 seconds
136
    #[serde(default, deserialize_with = "convert_duration_with_shellexpand")]
137
    pub worker_timeout_s: u64,
138
139
    /// Maximum time (seconds) an action can stay in Executing state without
140
    /// any worker update before being timed out and re-queued.
141
    /// This applies regardless of worker keepalive status, catching cases
142
    /// where a worker is alive (sending keepalives) but stuck on a specific
143
    /// action. Set to 0 to disable (relies only on `worker_timeout_s`).
144
    ///
145
    /// Default: 0 (disabled)
146
    #[serde(default, deserialize_with = "convert_duration_with_shellexpand")]
147
    pub max_action_executing_timeout_s: u64,
148
149
    /// If a job returns an internal error or times out this many times when
150
    /// attempting to run on a worker the scheduler will return the last error
151
    /// to the client. Jobs will be retried and this configuration is to help
152
    /// prevent one rogue job from infinitely retrying and taking up a lot of
153
    /// resources when the task itself is the one causing the server to go
154
    /// into a bad state.
155
    /// Default: 3
156
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
157
    pub max_job_retries: usize,
158
159
    /// The strategy used to assign workers jobs.
160
    #[serde(default)]
161
    pub allocation_strategy: WorkerAllocationStrategy,
162
163
    /// The storage backend to use for the scheduler.
164
    /// Default: memory
165
    pub experimental_backend: Option<ExperimentalSimpleSchedulerBackend>,
166
167
    /// Every N seconds, do logging of worker matching
168
    /// e.g. "worker busy", "can't find any worker"
169
    /// Defaults to 10s. Can be set to -1 to disable
170
    #[serde(
171
        default = "default_worker_match_logging_interval_s",
172
        deserialize_with = "convert_duration_with_shellexpand_and_negative"
173
    )]
174
    pub worker_match_logging_interval_s: i64,
175
176
    /// Every N seconds, run a worker matching pass even if no task or worker
177
    /// change notification arrived. This is a safety net for missed
178
    /// notifications and for scheduler backends with eventually consistent
179
    /// searches (for example Redis), where an operation that was re-queued
180
    /// may not be visible to the search triggered by its own notification.
181
    /// Without this, such an operation can stay queued until an unrelated
182
    /// event triggers another matching pass.
183
    /// Defaults to 5s. Zero or any negative value disables it.
184
    #[serde(
185
        default = "default_fallback_match_interval_s",
186
        deserialize_with = "convert_duration_with_shellexpand_and_negative"
187
    )]
188
    pub fallback_match_interval_s: i64,
189
}
190
191
#[derive(Deserialize, Serialize, Debug)]
192
#[serde(rename_all = "snake_case")]
193
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
194
pub enum ExperimentalSimpleSchedulerBackend {
195
    /// Use an in-memory store for the scheduler.
196
    Memory,
197
    /// Use a redis store for the scheduler.
198
    Redis(ExperimentalRedisSchedulerBackend),
199
}
200
201
#[derive(Deserialize, Serialize, Debug, Default)]
202
#[serde(deny_unknown_fields)]
203
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
204
pub struct ExperimentalRedisSchedulerBackend {
205
    /// A reference to the redis store to use for the scheduler.
206
    /// Note: This MUST resolve to a `RedisSpec`.
207
    pub redis_store: StoreRefName,
208
}
209
210
/// A scheduler that forwards requests to an upstream scheduler. This
211
/// is useful to use when doing some kind of local action cache or CAS away from
212
/// the main cluster of workers. In general, it's more efficient to point the
213
/// build at the main scheduler directly though.
214
#[derive(Deserialize, Serialize, Debug)]
215
#[serde(deny_unknown_fields)]
216
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
217
pub struct GrpcSpec {
218
    /// The upstream scheduler to forward requests to.
219
    pub endpoint: GrpcEndpoint,
220
221
    /// Retry configuration to use when a network request fails.
222
    #[serde(default)]
223
    pub retry: Retry,
224
225
    /// Limit the number of simultaneous upstream requests to this many. A
226
    /// value of zero is treated as unlimited. If the limit is reached the
227
    /// request is queued.
228
    /// Default: unlimited
229
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
230
    pub max_concurrent_requests: usize,
231
232
    /// The number of connections to make to each specified endpoint to balance
233
    /// the load over multiple TCP connections.
234
    /// Default: 1.
235
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
236
    pub connections_per_endpoint: usize,
237
}
238
239
#[derive(Deserialize, Serialize, Debug)]
240
#[serde(deny_unknown_fields)]
241
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
242
pub struct CacheLookupSpec {
243
    /// The reference to the action cache store used to return cached
244
    /// actions from rather than running them again.
245
    /// To prevent unintended issues, this store should probably be a `CompletenessCheckingSpec`.
246
    pub ac_store: StoreRefName,
247
248
    /// The nested scheduler to use if cache lookup fails.
249
    pub scheduler: Box<SchedulerSpec>,
250
}
251
252
#[derive(Deserialize, Serialize, Debug, Clone)]
253
#[serde(deny_unknown_fields)]
254
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
255
pub struct PlatformPropertyAddition {
256
    /// The name of the property to add.
257
    pub name: String,
258
    /// The value to assign to the property.
259
    pub value: String,
260
}
261
262
#[derive(Deserialize, Serialize, Debug, Clone)]
263
#[serde(deny_unknown_fields)]
264
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
265
pub struct PlatformPropertyReplacement {
266
    /// The name of the property to replace.
267
    pub name: String,
268
    /// The value to match against, if unset then any instance matches.
269
    #[serde(default)]
270
    pub value: Option<String>,
271
    /// The new name of the property.
272
    pub new_name: String,
273
    /// The value to assign to the property, if unset will remain the same.
274
    #[serde(default)]
275
    pub new_value: Option<String>,
276
}
277
278
#[derive(Deserialize, Serialize, Debug, Clone)]
279
#[serde(rename_all = "snake_case")]
280
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
281
pub enum PropertyModification {
282
    /// Add a property to the action properties.
283
    Add(PlatformPropertyAddition),
284
    /// Remove a named property from the action.
285
    Remove(String),
286
    /// If a property is found, then replace it with another one.
287
    Replace(PlatformPropertyReplacement),
288
}
289
290
#[derive(Deserialize, Serialize, Debug)]
291
#[serde(deny_unknown_fields)]
292
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
293
pub struct PropertyModifierSpec {
294
    /// A list of modifications to perform to incoming actions for the nested
295
    /// scheduler. These are performed in order and blindly, so removing a
296
    /// property that doesn't exist is fine and overwriting an existing property
297
    /// is also fine. If adding properties that do not exist in the nested
298
    /// scheduler is not supported and will likely cause unexpected behaviour.
299
    pub modifications: Vec<PropertyModification>,
300
301
    /// The nested scheduler to use after modifying the properties.
302
    pub scheduler: Box<SchedulerSpec>,
303
}
304
305
0
const fn default_historical_resource_refresh_interval_s() -> u64 {
306
0
    30
307
0
}
308
309
0
fn default_historical_resource_cpu_property_name() -> String {
310
0
    "cpu_count".to_string()
311
0
}
312
313
0
fn default_historical_resource_memory_property_name() -> String {
314
0
    "memory_kb".to_string()
315
0
}
316
317
#[derive(Deserialize, Serialize, Debug)]
318
#[serde(deny_unknown_fields)]
319
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
320
pub struct HistoricalResourceSpec {
321
    /// JSON file containing historical resource hints keyed by Bazel
322
    /// `RequestMetadata` `target_id` and/or `action_mnemonic`.
323
    ///
324
    /// Supported file shapes:
325
    /// ```json
326
    /// [
327
    ///   { "target_id": "//pkg:test", "action_mnemonic": "TestRunner", "cpu_count": 2, "memory_kb": 12582912 }
328
    /// ]
329
    /// ```
330
    /// or:
331
    /// ```json
332
    /// { "hints": [ ... ] }
333
    /// ```
334
    #[serde(deserialize_with = "convert_string_with_shellexpand")]
335
    pub hints_file: String,
336
337
    /// Reload interval for `hints_file`. Set to 0 to load once.
338
    /// Default: 30 seconds
339
    #[serde(
340
        default = "default_historical_resource_refresh_interval_s",
341
        deserialize_with = "convert_duration_with_shellexpand"
342
    )]
343
    pub refresh_interval_s: u64,
344
345
    /// Platform property name used for CPU minimums.
346
    /// Default: `cpu_count`
347
    #[serde(
348
        default = "default_historical_resource_cpu_property_name",
349
        deserialize_with = "convert_string_with_shellexpand"
350
    )]
351
    pub cpu_property_name: String,
352
353
    /// Platform property name used for memory minimums, expressed in KiB.
354
    /// Default: `memory_kb`
355
    #[serde(
356
        default = "default_historical_resource_memory_property_name",
357
        deserialize_with = "convert_string_with_shellexpand"
358
    )]
359
    pub memory_property_name: String,
360
361
    /// The nested scheduler to use after applying resource hints.
362
    pub scheduler: Box<SchedulerSpec>,
363
}