/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 | | } |