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