/build/source/nativelink-scheduler/src/historical_resource_scheduler.rs
Line | Count | Source |
1 | | // Copyright 2026 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 | | //! Sizes actions from a periodically refreshed hints file by default. |
16 | | //! With learning enabled, sampled worker usage lands in a [`ResourceSketch`]; an action |
17 | | //! is reserved what the sketch has learned for its key, shaped with the |
18 | | //! headroom and the ladder configured here, or by the cold-start policy |
19 | | //! when the sketch has nothing. The sketch is written to a store so a |
20 | | //! restart keeps it; a hints file seeds an empty sketch once. |
21 | | |
22 | | use core::time::Duration; |
23 | | use std::collections::{BTreeSet, HashMap, HashSet}; |
24 | | use std::fs; |
25 | | use std::sync::Arc; |
26 | | use std::time::{SystemTime, UNIX_EPOCH}; |
27 | | |
28 | | use async_trait::async_trait; |
29 | | use bytes::Bytes; |
30 | | use nativelink_config::schedulers::{HistoricalResourceSpec, SizeClassSpec, TimeoutClassSpec}; |
31 | | use nativelink_error::{Code, Error, ResultExt, make_input_err}; |
32 | | use nativelink_metric::{ |
33 | | MetricFieldData, MetricKind, MetricPublishKnownKindData, MetricsComponent, RootMetricsComponent, |
34 | | }; |
35 | | use nativelink_util::action_messages::{ActionInfo, OperationId}; |
36 | | use nativelink_util::metrics::{ |
37 | | record_hint_resolution, record_hints_family, record_hints_snapshot, |
38 | | record_hints_snapshot_failure, |
39 | | }; |
40 | | use nativelink_util::operation_state_manager::{ |
41 | | ActionStateResult, ActionStateResultStream, ClientStateManager, OperationFilter, |
42 | | }; |
43 | | use nativelink_util::origin_event::{BAZEL_METADATA_KEY, request_metadata_from_baggage}; |
44 | | use nativelink_util::store_trait::{Store, StoreKey, StoreLike}; |
45 | | use nativelink_util::{background_spawn, spawn_blocking}; |
46 | | use opentelemetry::baggage::BaggageExt; |
47 | | use opentelemetry::context::Context; |
48 | | use parking_lot::Mutex; |
49 | | use serde::Deserialize; |
50 | | use tracing::{debug, info, warn}; |
51 | | use uuid::Uuid; |
52 | | |
53 | | use crate::known_platform_property_provider::KnownPlatformPropertyProvider; |
54 | | use crate::resource_sketch::{ |
55 | | Budget, Entry, Family, Policy, ResourceSketch, Shaped, SketchKey, Snapshot, Stats, |
56 | | }; |
57 | | |
58 | | /// The prefix the sketch snapshots are written under in `snapshot_store`, |
59 | | /// one key per scheduler replica; see [`snapshot_key`]. |
60 | | pub const SNAPSHOT_KEY_PREFIX: &str = "historical_resource_sketch/"; |
61 | | const SNAPSHOT_KEY_PREFIX_END: &str = "historical_resource_sketch0"; |
62 | | const MAX_SNAPSHOT_CANDIDATES: usize = 128; |
63 | | const MAX_REPLICA_ID_BYTES: usize = 256; |
64 | | |
65 | | /// The key the store is probed with at start; outside the prefix, so a |
66 | | /// listing never reads it as a snapshot. |
67 | | const SNAPSHOT_PROBE_KEY: &str = "historical_resource_sketch.probe"; |
68 | | |
69 | | /// The snapshot key of one replica. |
70 | | #[must_use] |
71 | 164 | pub fn snapshot_key(replica: &str) -> String { |
72 | 164 | format!("{SNAPSHOT_KEY_PREFIX}{replica}") |
73 | 164 | } |
74 | | |
75 | | /// The replica this process writes its snapshot as: the pod's hostname, |
76 | | /// else a fresh id. Every replica sees the usage of the workers connected |
77 | | /// to it and no other. A configured identity takes precedence over the hostname. |
78 | | #[must_use] |
79 | 6 | pub fn replica_id(configured: &str) -> String { |
80 | 6 | if !configured.is_empty() { |
81 | 2 | return configured.to_string(); |
82 | 4 | } |
83 | 4 | std::env::var("HOSTNAME") |
84 | 4 | .ok() |
85 | 4 | .filter(|name| !name0 .is_empty0 ()) |
86 | 4 | .unwrap_or_else(|| Uuid::new_v4().to_string()) |
87 | 6 | } |
88 | | |
89 | | /// One hint of the seed file. The v1 fields keep their meaning; the v2 |
90 | | /// fields are optional, so a v1 file still loads. `class` names a point on |
91 | | /// the configured ladder and stands in for any number the hint leaves out. |
92 | | #[derive(Debug, Clone, Deserialize)] |
93 | | struct HistoricalResourceHint { |
94 | | #[serde(default)] |
95 | | target_id: Option<String>, |
96 | | #[serde(default)] |
97 | | action_mnemonic: Option<String>, |
98 | | #[serde(default)] |
99 | | command_digest: Option<String>, |
100 | | /// Used by file-backed hints. Learning keys by command instead. |
101 | | #[serde(default)] |
102 | | action_digest: Option<String>, |
103 | | #[serde(default)] |
104 | | cpu_count: Option<u64>, |
105 | | #[serde(default)] |
106 | | memory_kb: Option<u64>, |
107 | | #[serde(default)] |
108 | | memory_mib: Option<u64>, |
109 | | #[serde(default)] |
110 | | disk_kb: Option<u64>, |
111 | | #[serde(default)] |
112 | | class: Option<String>, |
113 | | #[serde(default)] |
114 | | samples: Option<u64>, |
115 | | #[serde(default)] |
116 | | last_seen_s: Option<u64>, |
117 | | } |
118 | | |
119 | | /// `hash/size` and `hash-size` both name the same digest. |
120 | 4 | fn normalize_digest_key(digest: &str) -> String { |
121 | 4 | digest.trim().replace('/', "-") |
122 | 4 | } |
123 | | |
124 | | #[derive(Clone, Debug, Default, Hash, PartialEq, Eq)] |
125 | | struct HintKey { |
126 | | target_id: Option<String>, |
127 | | action_mnemonic: Option<String>, |
128 | | command_digest: Option<String>, |
129 | | action_digest: Option<String>, |
130 | | } |
131 | | |
132 | | impl HistoricalResourceHint { |
133 | 3 | fn keys(&self) -> Vec<HintKey> { |
134 | 3 | let mut keys = Vec::new(); |
135 | 3 | if self.target_id.is_some() || self.action_mnemonic1 .is_some1 () { |
136 | 2 | keys.push(HintKey { |
137 | 2 | target_id: self.target_id.clone(), |
138 | 2 | action_mnemonic: self.action_mnemonic.clone(), |
139 | 2 | ..HintKey::default() |
140 | 2 | }); |
141 | 2 | }1 |
142 | 6 | for (digest, action) in [ |
143 | 3 | (self.action_digest.as_deref(), true), |
144 | 3 | (self.command_digest.as_deref(), false), |
145 | 3 | ] { |
146 | 6 | if let Some(digest1 ) = digest.map(normalize_digest_key).filter(|d| !d1 .is_empty1 ()) { |
147 | 1 | keys.push(if action { |
148 | 1 | HintKey { |
149 | 1 | action_digest: Some(digest), |
150 | 1 | ..HintKey::default() |
151 | 1 | } |
152 | | } else { |
153 | 0 | HintKey { |
154 | 0 | command_digest: Some(digest), |
155 | 0 | ..HintKey::default() |
156 | 0 | } |
157 | | }); |
158 | 5 | } |
159 | | } |
160 | 3 | keys |
161 | 3 | } |
162 | | |
163 | 10.2k | fn memory_kb(&self) -> Option<u64> { |
164 | 10.2k | self.memory_kb.or_else(|| {6 |
165 | 6 | self.memory_mib |
166 | 6 | .map(|memory_mib| memory_mib0 .saturating_mul0 (1024)) |
167 | 6 | }) |
168 | 10.2k | } |
169 | | |
170 | | /// The sketch key this hint seeds: the target, else the mnemonic, else |
171 | | /// the command digest. `None` for a hint that names only an action |
172 | | /// digest, which the sketch does not key by. |
173 | 10.2k | fn sketch_key(&self) -> Option<SketchKey> { |
174 | 10.2k | let target = self.target_id.as_deref().filter(|s| !s10.2k .is_empty10.2k ()); |
175 | 10.2k | let mnemonic = self.action_mnemonic.as_deref().filter(|s| !s10.2k .is_empty10.2k ()); |
176 | 10.2k | let command = self |
177 | 10.2k | .command_digest |
178 | 10.2k | .as_deref() |
179 | 10.2k | .map(normalize_digest_key) |
180 | 10.2k | .filter(|d| !d3 .is_empty3 ()); |
181 | 10.2k | match (target, mnemonic, command) { |
182 | 10.2k | (Some(target), mnemonic, _) => Some(SketchKey::Target { |
183 | 10.2k | target: Arc::from(target), |
184 | 10.2k | mnemonic: mnemonic.map(Arc::from), |
185 | 10.2k | }), |
186 | 6 | (None, Some(mnemonic), _) => Some(SketchKey::Mnemonic { |
187 | 6 | mnemonic: Arc::from(mnemonic), |
188 | 6 | }), |
189 | 2 | (None, None, Some(digest)) => Some(SketchKey::Command { |
190 | 2 | digest: Arc::from(digest.as_str()), |
191 | 2 | }), |
192 | 2 | (None, None, None) => None, |
193 | | } |
194 | 10.2k | } |
195 | | } |
196 | | |
197 | | #[derive(Debug, Deserialize)] |
198 | | #[serde(untagged)] |
199 | | enum HistoricalResourceHintFile { |
200 | | List(Vec<HistoricalResourceHint>), |
201 | | Object { hints: Vec<HistoricalResourceHint> }, |
202 | | } |
203 | | |
204 | | impl HistoricalResourceHintFile { |
205 | 17 | fn into_hints(self) -> Vec<HistoricalResourceHint> { |
206 | 17 | match self { |
207 | 17 | Self::List(hints14 ) | Self::Object { hints3 } => hints, |
208 | | } |
209 | 17 | } |
210 | | } |
211 | | |
212 | | /// The numbers an action leaves `add_action` with, before they become |
213 | | /// minimum properties. Zero means the dimension is not set. |
214 | | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] |
215 | | struct Numbers { |
216 | | cpu_count: u64, |
217 | | memory_kb: u64, |
218 | | disk_kb: u64, |
219 | | } |
220 | | |
221 | | impl From<&SizeClassSpec> for Numbers { |
222 | 7 | fn from(class: &SizeClassSpec) -> Self { |
223 | 7 | Self { |
224 | 7 | cpu_count: class.cpu_count, |
225 | 7 | memory_kb: class.memory_kb, |
226 | 7 | disk_kb: class.disk_kb, |
227 | 7 | } |
228 | 7 | } |
229 | | } |
230 | | |
231 | | impl From<&Shaped> for Numbers { |
232 | 13 | fn from(shaped: &Shaped) -> Self { |
233 | 13 | Self { |
234 | 13 | cpu_count: shaped.cpu_count, |
235 | 13 | memory_kb: shaped.memory_kb, |
236 | 13 | disk_kb: shaped.disk_kb, |
237 | 13 | } |
238 | 13 | } |
239 | | } |
240 | | |
241 | | /// The dimensions this fleet reserves: the ones its ladder or its cold |
242 | | /// start names. A measurement may cover any dimension the worker samples, |
243 | | /// and a minimum on a dimension no worker advertises makes the action |
244 | | /// unsatisfiable, so a number on a dimension the fleet does not reserve is |
245 | | /// dropped when the hint is shaped. A configuration with neither a ladder |
246 | | /// nor a cold start reserves whatever was measured, as it always did. |
247 | | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
248 | | struct Reserved { |
249 | | cpu: bool, |
250 | | memory: bool, |
251 | | disk: bool, |
252 | | } |
253 | | |
254 | | impl Reserved { |
255 | 50 | fn from_spec(spec: &HistoricalResourceSpec) -> Self { |
256 | 50 | let cold = spec.cold_start.unwrap_or_default(); |
257 | 50 | let sized = !spec.classes.is_empty() |
258 | 44 | || cold.cpu_count > 0 |
259 | 42 | || cold.memory_kb > 0 |
260 | 42 | || cold.disk_kb > 0; |
261 | 50 | if !sized { |
262 | 42 | return Self { |
263 | 42 | cpu: true, |
264 | 42 | memory: true, |
265 | 42 | disk: true, |
266 | 42 | }; |
267 | 8 | } |
268 | | Self { |
269 | 8 | cpu: cold.cpu_count > 0 || spec.classes.iter()6 .any6 (|c| c.cpu_count6 > 0), |
270 | 8 | memory: cold.memory_kb > 0 || spec.classes.iter()7 .any7 (|c| c.memory_kb6 > 0), |
271 | 10 | disk: cold.disk_kb > 08 || spec.classes.iter()7 .any7 (|c| c.disk_kb > 0), |
272 | | } |
273 | 50 | } |
274 | | } |
275 | | |
276 | 51 | fn now_s() -> u64 { |
277 | 51 | SystemTime::now() |
278 | 51 | .duration_since(UNIX_EPOCH) |
279 | 51 | .map_or(0, |d| d.as_secs()) |
280 | 51 | } |
281 | | |
282 | | /// A complete file generation is swapped under one short lock. File reads, |
283 | | /// parsing and disposal of the old generation happen outside Execute. |
284 | | struct FileHints { |
285 | | path: String, |
286 | | classes: Vec<SizeClassSpec>, |
287 | | reserved: Reserved, |
288 | | hints: Mutex<HashMap<HintKey, Numbers>>, |
289 | | } |
290 | | |
291 | | impl FileHints { |
292 | 4 | fn refresh(&self) { |
293 | 4 | let parsed = fs::read_to_string(&self.path) |
294 | 4 | .map_err(|e| e0 .to_string0 ()) |
295 | 4 | .and_then(|contents| { |
296 | 4 | serde_json::from_str::<HistoricalResourceHintFile>(&contents) |
297 | 4 | .map_err(|e| e1 .to_string1 ()) |
298 | 4 | }); |
299 | 4 | let mut hints3 = match parsed { |
300 | 3 | Ok(file) => file.into_hints(), |
301 | 1 | Err(error) => { |
302 | 1 | warn!(%error, "Could not refresh resource hints; keeping previous generation"); |
303 | 1 | return; |
304 | | } |
305 | | }; |
306 | 3 | hints.sort_by_key(|hint| core::cmp::Reverse(hint.keys()0 .len0 ())); |
307 | 3 | let mut next = HashMap::new(); |
308 | 3 | for hint in hints { |
309 | 3 | let class = hint |
310 | 3 | .class |
311 | 3 | .as_ref() |
312 | 3 | .and_then(|name| self.classes.iter()0 .find0 (|c| &c.name0 == name0 )) |
313 | 3 | .map_or_else(Numbers::default, Numbers::from); |
314 | 3 | let numbers = Numbers { |
315 | 3 | cpu_count: if self.reserved.cpu { |
316 | 3 | hint.cpu_count.unwrap_or(class.cpu_count) |
317 | | } else { |
318 | 0 | 0 |
319 | | }, |
320 | 3 | memory_kb: if self.reserved.memory { |
321 | 3 | hint.memory_kb().unwrap_or(class.memory_kb) |
322 | | } else { |
323 | 0 | 0 |
324 | | }, |
325 | 3 | disk_kb: if self.reserved.disk { |
326 | 3 | hint.disk_kb.unwrap_or(class.disk_kb) |
327 | | } else { |
328 | 0 | 0 |
329 | | }, |
330 | | }; |
331 | 3 | for key in hint.keys() { |
332 | 3 | next.insert(key, numbers); |
333 | 3 | } |
334 | | } |
335 | 3 | let old = { core::mem::replace(&mut *self.hints.lock(), next) }; |
336 | 3 | drop(old); |
337 | 4 | } |
338 | | |
339 | 3 | fn new(spec: &HistoricalResourceSpec) -> Arc<Self> { |
340 | 3 | let this = Arc::new(Self { |
341 | 3 | path: spec.hints_file.clone(), |
342 | 3 | classes: spec.classes.clone(), |
343 | 3 | reserved: Reserved::from_spec(spec), |
344 | 3 | hints: Mutex::new(HashMap::new()), |
345 | 3 | }); |
346 | 3 | if this.path.is_empty() { |
347 | 1 | return this; |
348 | 2 | } |
349 | 2 | this.refresh(); |
350 | 2 | if spec.refresh_interval_s > 0 { |
351 | 1 | let weak = Arc::downgrade(&this); |
352 | 1 | let interval = Duration::from_secs(spec.refresh_interval_s); |
353 | 1 | background_spawn!("historical_resource_file_refresh", async move { |
354 | | loop { |
355 | 3 | tokio::time::sleep(interval).await; |
356 | 2 | let Some(this) = weak.upgrade() else { |
357 | 0 | break; |
358 | | }; |
359 | 0 | if let Err(error) = |
360 | 2 | spawn_blocking!("refresh_resource_hints", move || this.refresh()).await |
361 | | { |
362 | 0 | warn!(?error, "Resource hints refresh task failed"); |
363 | 2 | } |
364 | | } |
365 | 0 | }); |
366 | 1 | } |
367 | 2 | this |
368 | 3 | } |
369 | | |
370 | 53 | fn lookup( |
371 | 53 | &self, |
372 | 53 | action: &ActionInfo, |
373 | 53 | target: Option<&String>, |
374 | 53 | mnemonic: Option<&String>, |
375 | 53 | ) -> Option<Numbers> { |
376 | 53 | let keys = [ |
377 | 53 | target.zip(mnemonic).map(|(t, m)| HintKey { |
378 | 52 | target_id: Some(t.clone()), |
379 | 52 | action_mnemonic: Some(m.clone()), |
380 | 52 | ..HintKey::default() |
381 | 52 | }), |
382 | 53 | target.map(|t| HintKey { |
383 | 52 | target_id: Some(t.clone()), |
384 | 52 | ..HintKey::default() |
385 | 52 | }), |
386 | 53 | Some(HintKey { |
387 | 53 | action_digest: Some(action.digest().to_string()), |
388 | 53 | ..HintKey::default() |
389 | 53 | }), |
390 | 53 | Some(HintKey { |
391 | 53 | command_digest: Some(action.command_digest.to_string()), |
392 | 53 | ..HintKey::default() |
393 | 53 | }), |
394 | 53 | mnemonic.map(|m| HintKey { |
395 | 52 | action_mnemonic: Some(m.clone()), |
396 | 52 | ..HintKey::default() |
397 | 52 | }), |
398 | | ]; |
399 | 53 | let hints = self.hints.lock(); |
400 | 53 | keys.iter() |
401 | 53 | .flatten() |
402 | 105 | .find_map53 (|key| hints.get(key).copied()) |
403 | 53 | } |
404 | | } |
405 | | |
406 | | pub struct HistoricalResourceScheduler { |
407 | | cpu_property_name: String, |
408 | | memory_property_name: String, |
409 | | disk_property_name: String, |
410 | | cold_start: Numbers, |
411 | | classes: Vec<SizeClassSpec>, |
412 | | default_class: Option<String>, |
413 | | class_by_mnemonic: HashMap<String, String>, |
414 | | class_by_timeout: Vec<TimeoutClassSpec>, |
415 | | scheduler: Option<Arc<dyn KnownPlatformPropertyProvider>>, |
416 | | known_properties: Mutex<HashMap<String, Vec<String>>>, |
417 | | sketch: Arc<ResourceSketch>, |
418 | | file_hints: Option<Arc<FileHints>>, |
419 | | } |
420 | | |
421 | | impl core::fmt::Debug for HistoricalResourceScheduler { |
422 | 0 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { |
423 | 0 | f.debug_struct("HistoricalResourceScheduler") |
424 | 0 | .field("cpu_property_name", &self.cpu_property_name) |
425 | 0 | .field("memory_property_name", &self.memory_property_name) |
426 | 0 | .field("cold_start", &self.cold_start) |
427 | 0 | .field("classes", &self.classes) |
428 | 0 | .field("default_class", &self.default_class) |
429 | 0 | .field("known_properties", &self.known_properties) |
430 | 0 | .field("sketch", &self.sketch) |
431 | 0 | .finish_non_exhaustive() |
432 | 0 | } |
433 | | } |
434 | | |
435 | | impl HistoricalResourceScheduler { |
436 | | /// The sketch a spec describes: its headroom, ladder and reserved |
437 | | /// dimensions as the shaping policy, its budget and window. |
438 | | #[must_use] |
439 | 47 | pub fn sketch_from_spec(spec: &HistoricalResourceSpec) -> ResourceSketch { |
440 | 47 | let reserved = Reserved::from_spec(spec); |
441 | 47 | ResourceSketch::new( |
442 | 47 | Policy { |
443 | 47 | headroom_percent: spec.headroom_percent, |
444 | 47 | classes: spec.classes.clone(), |
445 | 47 | reserve_cpu: reserved.cpu, |
446 | 47 | reserve_memory: reserved.memory, |
447 | 47 | reserve_disk: reserved.disk, |
448 | 47 | }, |
449 | 47 | if spec.learn_from_workers { |
450 | 43 | Budget::from_bytes(spec.memory_budget_bytes) |
451 | | } else { |
452 | | // File-backed mode never receives worker samples. Do not |
453 | | // preallocate the learned cache when learning is disabled. |
454 | 4 | Budget { |
455 | 4 | target: 1, |
456 | 4 | mnemonic: 1, |
457 | 4 | command: 1, |
458 | 4 | probation: 1, |
459 | 4 | } |
460 | | }, |
461 | 47 | Duration::from_secs(spec.window_days.saturating_mul(86_400)), |
462 | | ) |
463 | 47 | } |
464 | | |
465 | | /// Wraps `scheduler` with sizing from `sketch`. An empty sketch is |
466 | | /// seeded from `spec.hints_file` when one is named; a sketch that was |
467 | | /// loaded from a snapshot keeps what it has. |
468 | | #[must_use] |
469 | 24 | pub fn new( |
470 | 24 | spec: &HistoricalResourceSpec, |
471 | 24 | scheduler: Arc<dyn KnownPlatformPropertyProvider>, |
472 | 24 | sketch: Arc<ResourceSketch>, |
473 | 24 | ) -> Self { |
474 | 24 | let this = Self { |
475 | 24 | cpu_property_name: spec.cpu_property_name.clone(), |
476 | 24 | memory_property_name: spec.memory_property_name.clone(), |
477 | 24 | disk_property_name: spec.disk_property_name.clone(), |
478 | 24 | cold_start: spec |
479 | 24 | .cold_start |
480 | 24 | .as_ref() |
481 | 24 | .map_or_else(Numbers::default, |c| Numbers { |
482 | 2 | cpu_count: c.cpu_count, |
483 | 2 | memory_kb: c.memory_kb, |
484 | 2 | disk_kb: c.disk_kb, |
485 | 2 | }), |
486 | 24 | classes: spec.classes.clone(), |
487 | 24 | default_class: spec.default_class.clone(), |
488 | 24 | class_by_mnemonic: spec.class_by_mnemonic.clone(), |
489 | 24 | class_by_timeout: spec.class_by_timeout.clone(), |
490 | 24 | scheduler: Some(scheduler), |
491 | 24 | known_properties: Mutex::new(HashMap::new()), |
492 | 24 | sketch, |
493 | 24 | file_hints: (!spec.learn_from_workers).then(|| FileHints::new3 (spec3 )), |
494 | | }; |
495 | 24 | if spec.learn_from_workers && !spec.hints_file.is_empty()21 { |
496 | 14 | let stats = this.sketch.stats(); |
497 | 14 | if stats.entries.iter().sum::<usize>() == 0 { |
498 | 14 | this.seed_from_file(&spec.hints_file); |
499 | 14 | } else { |
500 | 0 | info!( |
501 | | hints_file = %spec.hints_file, |
502 | | "A snapshot was loaded; the hints file is not read" |
503 | | ); |
504 | | } |
505 | 10 | } |
506 | 24 | this |
507 | 24 | } |
508 | | |
509 | | /// What this layer sizes from. |
510 | | #[must_use] |
511 | 0 | pub const fn sketch(&self) -> &Arc<ResourceSketch> { |
512 | 0 | &self.sketch |
513 | 0 | } |
514 | | |
515 | | /// `snapshot_store` must take a string key: a stack that routes by |
516 | | /// digest (`size_partitioning`) refuses one. A probe at start writes a |
517 | | /// key beside the snapshots and reads it back, so such a stack fails at |
518 | | /// start with a message rather than at the first write a minute later. |
519 | 3 | pub async fn probe_snapshot_store(store: &Store, name: &str) -> Result<(), Error> { |
520 | | const HELP: &str = "it must take a string key: memory, filesystem, redis, s3, gcs or azure, or verify, compression, shard and fast_slow over one; not size_partitioning"; |
521 | 3 | let bytes = Bytes::from_static(b"resource sketch probe"); |
522 | 3 | store |
523 | 3 | .update_oneshot(StoreKey::Str(SNAPSHOT_PROBE_KEY.into()), bytes.clone()) |
524 | 3 | .await |
525 | 3 | .err_tip(|| {2 |
526 | 2 | format!( |
527 | | "historical_resource.snapshot_store {name:?} refused the probe write; {HELP}" |
528 | | ) |
529 | 2 | })?; |
530 | 1 | let read = store |
531 | 1 | .get_part_unchunked(StoreKey::Str(SNAPSHOT_PROBE_KEY.into()), 0, None) |
532 | 1 | .await |
533 | 1 | .err_tip(|| {0 |
534 | 0 | format!( |
535 | | "historical_resource.snapshot_store {name:?} could not read the probe back; {HELP}" |
536 | | ) |
537 | 0 | })?; |
538 | 1 | if read != bytes { |
539 | 0 | return Err(make_input_err!( |
540 | 0 | "historical_resource.snapshot_store {name:?} read a different value back for the probe key; {HELP}" |
541 | 0 | )); |
542 | 1 | } |
543 | 1 | Ok(()) |
544 | 3 | } |
545 | | |
546 | 22 | async fn read_snapshot( |
547 | 22 | sketch: &ResourceSketch, |
548 | 22 | store: &Store, |
549 | 22 | key: &str, |
550 | 22 | ) -> Result<Snapshot, Error> { |
551 | 22 | let bytes16 = store |
552 | 22 | .get_part_unchunked( |
553 | 22 | StoreKey::Str(key.into()), |
554 | 22 | 0, |
555 | 22 | Some(sketch.snapshot_byte_limit()), |
556 | 22 | ) |
557 | 22 | .await?6 ; |
558 | 16 | let budget = sketch.budget(); |
559 | 16 | Snapshot::decode(&bytes, budget.target + budget.mnemonic + budget.command) |
560 | 22 | } |
561 | | |
562 | | /// Prefer this replica's snapshot. A Deployment replacement has a new |
563 | | /// hostname: when its key is absent, restore one newest compatible history |
564 | | /// from the same store prefix. Never merge or add together replicas' samples. |
565 | 16 | pub async fn load_snapshots(sketch: &ResourceSketch, store: &Store, own_key: &str) { |
566 | 16 | match Self::read_snapshot(sketch, store, own_key).await { |
567 | 10 | Ok(snapshot) => { |
568 | 10 | match sketch.load(snapshot, now_s()) { |
569 | 10 | Ok(entries) => info!(entries, "Restored this replica's resource sketch"), |
570 | 0 | Err(error) => warn!(?error, "Resource sketch not loaded; using seed"), |
571 | | } |
572 | 10 | return; |
573 | | } |
574 | 6 | Err(error) if error.code == Code::NotFound => {} |
575 | 0 | Err(error) => { |
576 | 0 | warn!( |
577 | | ?error, |
578 | | "Could not read this replica's resource sketch; using seed" |
579 | | ); |
580 | 0 | return; |
581 | | } |
582 | | } |
583 | | |
584 | | // Limit both the number and length of retained keys. Redis SCAN may |
585 | | // repeat keys, so duplicates must not consume the discovery allowance. |
586 | 6 | let mut keys = BTreeSet::new(); |
587 | 6 | let mut overflow = false; |
588 | 6 | let listed = store |
589 | 6 | .list( |
590 | 6 | StoreKey::Str(SNAPSHOT_KEY_PREFIX.into()) |
591 | 6 | ..StoreKey::Str(SNAPSHOT_KEY_PREFIX_END.into()), |
592 | 153 | |key| { |
593 | 153 | if let StoreKey::Str(key) = key |
594 | 153 | && key.len() <= SNAPSHOT_KEY_PREFIX.len() + MAX_REPLICA_ID_BYTES |
595 | | { |
596 | 153 | keys.insert(key.to_string()); |
597 | 153 | if keys.len() > MAX_SNAPSHOT_CANDIDATES { |
598 | 1 | overflow = true; |
599 | 1 | return false; |
600 | 152 | } |
601 | 0 | } |
602 | 152 | true |
603 | 153 | }, |
604 | | ) |
605 | 6 | .await; |
606 | 6 | if let Err(error0 ) = listed { |
607 | 0 | warn!( |
608 | | ?error, |
609 | | "Cannot discover resource snapshots; use a stable replica_id on stores without listing support. Using seed" |
610 | | ); |
611 | 0 | return; |
612 | 6 | } |
613 | 6 | if overflow { |
614 | 1 | warn!( |
615 | | "Too many resource snapshots to discover safely; set a stable replica_id or retire old snapshot keys. Using seed" |
616 | | ); |
617 | 1 | return; |
618 | 5 | } |
619 | 5 | let mut candidates = Vec::new(); |
620 | 24 | for key in keys5 { |
621 | 24 | if let Ok(header) = store |
622 | 24 | .get_part_unchunked( |
623 | 24 | StoreKey::Str(key.as_str().into()), |
624 | | 0, |
625 | 24 | Some(Snapshot::HEADER_BYTES), |
626 | | ) |
627 | 24 | .await |
628 | 24 | && let Ok(written_at20 ) = Snapshot::compatible_written_at(&header) |
629 | 20 | { |
630 | 20 | candidates.push((written_at, key)); |
631 | 20 | }4 |
632 | | } |
633 | 5 | candidates.sort_unstable(); |
634 | 6 | for (_, key) in candidates5 .into_iter5 ().rev5 () { |
635 | 6 | let loaded = Self::read_snapshot(sketch, store, &key) |
636 | 6 | .await |
637 | 6 | .and_then(|snapshot| sketch5 .load5 (snapshot5 , now_s5 ())); |
638 | 6 | match loaded { |
639 | 5 | Ok(entries) => { |
640 | 5 | info!( |
641 | | entries, |
642 | | snapshot_key = key, |
643 | | "Restored newest compatible resource sketch after replica replacement" |
644 | | ); |
645 | 5 | return; |
646 | | } |
647 | 1 | Err(error) => warn!( |
648 | | ?error, |
649 | | "Restart snapshot was unreadable; trying the next newest" |
650 | | ), |
651 | | } |
652 | | } |
653 | 16 | } |
654 | | |
655 | | /// Every `interval`: ages the sketch (the two half-windows |
656 | | /// are enforced here, store or not), writes it to `store` under `key` |
657 | | /// when there is one, and reports its size. Without a store, or with |
658 | | /// an interval of zero, the tick is a minute and the write is skipped. |
659 | 4 | pub fn spawn_snapshot_writer( |
660 | 4 | sketch: Arc<ResourceSketch>, |
661 | 4 | store: Option<Store>, |
662 | 4 | interval: Duration, |
663 | 4 | key: String, |
664 | 4 | ) { |
665 | 4 | let write = store.filter(|_| !interval1 .is_zero1 ()); |
666 | 4 | let tick = if interval.is_zero() { |
667 | 4 | Duration::from_mins(1) |
668 | | } else { |
669 | 0 | interval |
670 | | }; |
671 | 4 | background_spawn!("historical_resource_sketch_snapshot", async move {0 |
672 | 0 | let mut last_stats = Stats::default(); |
673 | 0 | let mut last_written_s: Option<u64> = None; |
674 | | loop { |
675 | 0 | tokio::time::sleep(tick).await; |
676 | 0 | let now = now_s(); |
677 | 0 | let dropped = sketch.age_out(now); |
678 | 0 | if dropped > 0 { |
679 | 0 | debug!( |
680 | | dropped, |
681 | | "Resource sketch keys not seen for the window were dropped" |
682 | | ); |
683 | 0 | } |
684 | 0 | if let Some(store) = &write { |
685 | 0 | let bytes = sketch.encode_snapshot(now); |
686 | 0 | let len = u64::try_from(bytes.len()).unwrap_or(u64::MAX); |
687 | 0 | match store |
688 | 0 | .update_oneshot(StoreKey::Str(key.as_str().into()), Bytes::from(bytes)) |
689 | 0 | .await |
690 | | { |
691 | 0 | Ok(()) => { |
692 | 0 | last_written_s = Some(now); |
693 | 0 | record_hints_snapshot(len, 0); |
694 | 0 | } |
695 | 0 | Err(err) => { |
696 | 0 | warn!( |
697 | | ?err, |
698 | | key, |
699 | | "Could not write the resource sketch snapshot; retrying next interval" |
700 | | ); |
701 | 0 | record_hints_snapshot_failure(); |
702 | 0 | record_hints_snapshot( |
703 | 0 | len, |
704 | 0 | last_written_s.map_or(0, |at| now.saturating_sub(at)), |
705 | | ); |
706 | | } |
707 | | } |
708 | 0 | } |
709 | 0 | let stats = sketch.stats(); |
710 | 0 | for family in Family::ALL { |
711 | 0 | let i = match family { |
712 | 0 | Family::Target => 0, |
713 | 0 | Family::Mnemonic => 1, |
714 | 0 | Family::Command => 2, |
715 | | }; |
716 | 0 | record_hints_family( |
717 | 0 | family.as_str(), |
718 | 0 | u64::try_from(stats.entries[i]).unwrap_or(u64::MAX), |
719 | 0 | u64::try_from(stats.capacity[i]).unwrap_or(u64::MAX), |
720 | 0 | stats.evictions[i].saturating_sub(last_stats.evictions[i]), |
721 | 0 | stats.samples[i].saturating_sub(last_stats.samples[i]), |
722 | | ); |
723 | | } |
724 | 0 | last_stats = stats; |
725 | | } |
726 | | }); |
727 | 4 | } |
728 | | |
729 | | /// Seeds the sketch from a hints file: one entry per hint, at the |
730 | | /// numbers the hint states (its class resolved through the ladder), |
731 | | /// marked seeded so the first real sample replaces it. |
732 | 14 | fn seed_from_file(&self, path: &str) { |
733 | 14 | let contents = match fs::read_to_string(path) { |
734 | 14 | Ok(contents) => contents, |
735 | 0 | Err(err) => { |
736 | 0 | warn!(?err, hints_file = %path, "Could not read the hints file; nothing seeded"); |
737 | 0 | return; |
738 | | } |
739 | | }; |
740 | 14 | let hints = match serde_json::from_str::<HistoricalResourceHintFile>(&contents) { |
741 | 14 | Ok(file) => file.into_hints(), |
742 | 0 | Err(err) => { |
743 | 0 | warn!(?err, hints_file = %path, "Could not parse the hints file; nothing seeded"); |
744 | 0 | return; |
745 | | } |
746 | | }; |
747 | 14 | let now = now_s(); |
748 | 14 | let (mut seeded, mut skipped) = (0usize, 0usize); |
749 | 10.2k | for hint in hints14 { |
750 | 10.2k | let numbers = self.numbers_from_hint(&hint); |
751 | 10.2k | let Some(key10.2k ) = hint.sketch_key().filter(|_| numbers10.2k != Numbers::default()10.2k ) else { |
752 | 2 | skipped += 1; |
753 | 2 | continue; |
754 | | }; |
755 | 10.2k | let samples = hint |
756 | 10.2k | .samples |
757 | 10.2k | .and_then(|n| u32::try_from0 (n0 ).ok0 ()) |
758 | 10.2k | .unwrap_or(1); |
759 | 10.2k | let entry = Entry::seeded( |
760 | 10.2k | numbers.cpu_count, |
761 | 10.2k | numbers.memory_kb, |
762 | 10.2k | numbers.disk_kb, |
763 | 10.2k | samples, |
764 | 10.2k | hint.last_seen_s.unwrap_or(now), |
765 | | ); |
766 | 10.2k | if self.sketch.seed(key, entry) { |
767 | 10.2k | seeded += 1; |
768 | 10.2k | }0 |
769 | | } |
770 | 14 | info!(hints_file = %path, seeded, skipped, "Seeded the resource sketch from the hints file"); |
771 | 14 | } |
772 | | |
773 | | /// The Bazel request metadata for the current action, if the client |
774 | | /// sent any: `(target_id, action_mnemonic)`, each empty string as `None`. |
775 | 75 | fn current_metadata() -> (Option<String>, Option<String>) { |
776 | 75 | let ctx = Context::current(); |
777 | 75 | let Some(metadata65 ) = ctx |
778 | 75 | .baggage() |
779 | 75 | .get(BAZEL_METADATA_KEY) |
780 | 75 | .and_then(|value| request_metadata_from_baggage65 (value.as_str()65 ).ok65 ()) |
781 | | else { |
782 | 10 | return (None, None); |
783 | | }; |
784 | 65 | ( |
785 | 65 | non_empty_string(metadata.target_id), |
786 | 65 | non_empty_string(metadata.action_mnemonic), |
787 | 65 | ) |
788 | 75 | } |
789 | | |
790 | | /// The key ladder, most specific first: `(target, mnemonic)`, then |
791 | | /// `target` alone, then the command digest, then the mnemonic alone. |
792 | | /// The command digest is what reaches clients that send no |
793 | | /// `RequestMetadata`, and the same command regardless of who runs it. |
794 | 22 | fn hint_for_action( |
795 | 22 | &self, |
796 | 22 | action_info: &ActionInfo, |
797 | 22 | target_id: Option<&String>, |
798 | 22 | action_mnemonic: Option<&String>, |
799 | 22 | ) -> Option<(Entry, &'static str)> { |
800 | 22 | let ladder = [ |
801 | | ( |
802 | 22 | target_id |
803 | 22 | .zip(action_mnemonic) |
804 | 22 | .map(|(target, mnemonic)| SketchKey::Target { |
805 | 12 | target: Arc::from(target.as_str()), |
806 | 12 | mnemonic: Some(Arc::from(mnemonic.as_str())), |
807 | 12 | }), |
808 | | "target", |
809 | | ), |
810 | | ( |
811 | 22 | target_id.map(|target| SketchKey::Target { |
812 | 12 | target: Arc::from(target.as_str()), |
813 | 12 | mnemonic: None, |
814 | 12 | }), |
815 | 22 | "target", |
816 | | ), |
817 | 22 | ( |
818 | 22 | Some(SketchKey::Command { |
819 | 22 | digest: Arc::from(action_info.command_digest.to_string().as_str()), |
820 | 22 | }), |
821 | 22 | "command_digest", |
822 | 22 | ), |
823 | | ( |
824 | 22 | action_mnemonic.map(|mnemonic| SketchKey::Mnemonic { |
825 | 13 | mnemonic: Arc::from(mnemonic.as_str()), |
826 | 13 | }), |
827 | 22 | "mnemonic", |
828 | | ), |
829 | | ]; |
830 | 22 | let now = now_s(); |
831 | 72 | for (key, source) in ladder22 { |
832 | 72 | if let Some(key47 ) = key |
833 | 47 | && let Some(entry14 ) = self.sketch.lookup(&key, now) |
834 | | { |
835 | 14 | let source = if entry.seeded { "seed"9 } else { source5 }; |
836 | 14 | return Some((entry, source)); |
837 | 58 | } |
838 | | } |
839 | 8 | None |
840 | 22 | } |
841 | | |
842 | 9 | fn class(&self, name: &str) -> Option<&SizeClassSpec> { |
843 | 14 | self.classes.iter()9 .find9 (|class| class.name == name) |
844 | 9 | } |
845 | | |
846 | | /// A seed hint's numbers: what it states, with its class filling |
847 | | /// anything it leaves out. An unknown class contributes nothing. |
848 | 10.2k | fn numbers_from_hint(&self, hint: &HistoricalResourceHint) -> Numbers { |
849 | 10.2k | let from_class = hint |
850 | 10.2k | .class |
851 | 10.2k | .as_deref() |
852 | 10.2k | .and_then(|name| self3 .class3 (name3 )) |
853 | 10.2k | .map(Numbers::from) |
854 | 10.2k | .unwrap_or_default(); |
855 | 10.2k | Numbers { |
856 | 10.2k | cpu_count: hint.cpu_count.unwrap_or(from_class.cpu_count), |
857 | 10.2k | memory_kb: hint.memory_kb().unwrap_or(from_class.memory_kb), |
858 | 10.2k | disk_kb: hint.disk_kb.unwrap_or(from_class.disk_kb), |
859 | 10.2k | } |
860 | 10.2k | } |
861 | | |
862 | | /// Raises the action's minimums to `numbers`; never lowers a value the |
863 | | /// client or an earlier step set. |
864 | 66 | fn apply_minimums(&self, action_info: &mut ActionInfo, numbers: Numbers) { |
865 | 66 | apply_minimum_platform_property( |
866 | 66 | &mut action_info.platform_properties, |
867 | 66 | &self.cpu_property_name, |
868 | 66 | numbers.cpu_count, |
869 | | ); |
870 | 66 | apply_minimum_platform_property( |
871 | 66 | &mut action_info.platform_properties, |
872 | 66 | &self.memory_property_name, |
873 | 66 | numbers.memory_kb, |
874 | | ); |
875 | 66 | apply_minimum_platform_property( |
876 | 66 | &mut action_info.platform_properties, |
877 | 66 | &self.disk_property_name, |
878 | 66 | numbers.disk_kb, |
879 | | ); |
880 | 66 | } |
881 | | |
882 | | /// The cold-start policy for an action the sketch has nothing for: a |
883 | | /// class by mnemonic, else by timeout, else the default class; without |
884 | | /// classes, the raw `cold_start` numbers. Only absent properties are |
885 | | /// filled. |
886 | 9 | fn cold_start_numbers( |
887 | 9 | &self, |
888 | 9 | action_info: &ActionInfo, |
889 | 9 | action_mnemonic: Option<&String>, |
890 | 9 | ) -> Numbers { |
891 | 9 | let by_mnemonic = action_mnemonic.and_then(|m| self.class_by_mnemonic4 .get4 (m4 )); |
892 | 9 | let by_timeout = if action_info.timeout.is_zero() { |
893 | 0 | None |
894 | | } else { |
895 | 9 | self.class_by_timeout |
896 | 9 | .iter() |
897 | 9 | .filter(|rule| Duration::from_secs(rule.min_timeout_s)3 <= action_info.timeout3 ) |
898 | 9 | .max_by_key(|rule| rule.min_timeout_s) |
899 | 9 | .map(|rule| &rule.class) |
900 | | }; |
901 | | // The rules were checked against the ladder at load; the first |
902 | | // candidate that names a class on it wins. |
903 | 9 | [by_mnemonic, by_timeout, self.default_class.as_ref()] |
904 | 9 | .into_iter() |
905 | 9 | .flatten() |
906 | 9 | .find_map(|name| self6 .class6 (name6 )) |
907 | 9 | .map_or(self.cold_start, Numbers::from) |
908 | 9 | } |
909 | | |
910 | | /// The cold-start rules and the default class must name classes on the |
911 | | /// ladder, or the action they were written for gets nothing. The budget |
912 | | /// must hold at least a few keys. |
913 | 7 | pub fn validate(spec: &HistoricalResourceSpec) -> Result<(), Error> { |
914 | 7 | if spec.replica_id.len() > MAX_REPLICA_ID_BYTES { |
915 | 1 | return Err(make_input_err!( |
916 | 1 | "historical_resource.replica_id must be at most {MAX_REPLICA_ID_BYTES} bytes" |
917 | 1 | )); |
918 | 6 | } |
919 | 6 | let known = |name: &str| spec.classes.iter()1 .any1 (|class| class.name1 == name1 ); |
920 | 6 | let mut rules = spec |
921 | 6 | .class_by_mnemonic |
922 | 6 | .iter() |
923 | 6 | .map(|(mnemonic, class)| (format!1 ("class_by_mnemonic[{mnemonic}]"), class1 .as_str1 ())) |
924 | 6 | .chain(spec.class_by_timeout.iter().map(|rule| {0 |
925 | 0 | ( |
926 | 0 | format!("class_by_timeout[{}]", rule.min_timeout_s), |
927 | 0 | rule.class.as_str(), |
928 | 0 | ) |
929 | 0 | })) |
930 | 6 | .chain( |
931 | 6 | spec.default_class |
932 | 6 | .iter() |
933 | 6 | .map(|class| ("default_class"0 .to_string0 (), class0 .as_str0 ())), |
934 | | ); |
935 | 6 | if let Some((rule1 , class1 )) = rules.find(|(_, class)| !known(class)1 ) { |
936 | 1 | return Err(make_input_err!( |
937 | 1 | "historical_resource.{rule} names size class {class:?}, which classes does not define" |
938 | 1 | )); |
939 | 5 | } |
940 | 5 | if spec.memory_budget_bytes < 1 << 20 { |
941 | 0 | return Err(make_input_err!( |
942 | 0 | "historical_resource.memory_budget_bytes must be at least 1 MiB, got {}", |
943 | 0 | spec.memory_budget_bytes |
944 | 0 | )); |
945 | 5 | } |
946 | 5 | if spec.window_days == 0 { |
947 | 0 | return Err(make_input_err!( |
948 | 0 | "historical_resource.window_days must be above zero" |
949 | 0 | )); |
950 | 5 | } |
951 | 5 | Ok(()) |
952 | 7 | } |
953 | | |
954 | 7 | fn apply_defaults(&self, action_info: &mut ActionInfo, numbers: Numbers) { |
955 | 7 | apply_default_platform_property( |
956 | 7 | &mut action_info.platform_properties, |
957 | 7 | &self.cpu_property_name, |
958 | 7 | numbers.cpu_count, |
959 | | ); |
960 | 7 | apply_default_platform_property( |
961 | 7 | &mut action_info.platform_properties, |
962 | 7 | &self.memory_property_name, |
963 | 7 | numbers.memory_kb, |
964 | | ); |
965 | 7 | apply_default_platform_property( |
966 | 7 | &mut action_info.platform_properties, |
967 | 7 | &self.disk_property_name, |
968 | 7 | numbers.disk_kb, |
969 | | ); |
970 | 7 | } |
971 | | |
972 | 0 | async fn inner_get_known_properties(&self, instance_name: &str) -> Result<Vec<String>, Error> { |
973 | | { |
974 | 0 | let known_properties = self.known_properties.lock(); |
975 | 0 | if let Some(property_manager) = known_properties.get(instance_name) { |
976 | 0 | return Ok(property_manager.clone()); |
977 | 0 | } |
978 | | } |
979 | 0 | let known_platform_property_provider = self |
980 | 0 | .scheduler |
981 | 0 | .as_ref() |
982 | 0 | .err_tip(|| "Inner scheduler does not implement KnownPlatformPropertyProvider for HistoricalResourceScheduler")?; |
983 | 0 | let mut known_properties = HashSet::<String>::from_iter( |
984 | 0 | known_platform_property_provider |
985 | 0 | .get_known_properties(instance_name) |
986 | 0 | .await?, |
987 | | ); |
988 | 0 | known_properties.insert(self.cpu_property_name.clone()); |
989 | 0 | known_properties.insert(self.memory_property_name.clone()); |
990 | 0 | known_properties.insert(self.disk_property_name.clone()); |
991 | 0 | let final_known_properties: Vec<String> = known_properties.into_iter().collect(); |
992 | 0 | self.known_properties |
993 | 0 | .lock() |
994 | 0 | .insert(instance_name.to_string(), final_known_properties.clone()); |
995 | 0 | Ok(final_known_properties) |
996 | 0 | } |
997 | | } |
998 | | |
999 | 130 | fn non_empty_string(value: String) -> Option<String> { |
1000 | 130 | if value.is_empty() { None1 } else { Some(value)129 } |
1001 | 130 | } |
1002 | | |
1003 | 198 | fn apply_minimum_platform_property( |
1004 | 198 | platform_properties: &mut HashMap<String, String>, |
1005 | 198 | property_name: &str, |
1006 | 198 | minimum_value: u64, |
1007 | 198 | ) { |
1008 | 198 | if minimum_value == 0 { |
1009 | 121 | return; |
1010 | 77 | } |
1011 | 77 | let should_update = platform_properties |
1012 | 77 | .get(property_name) |
1013 | 77 | .and_then(|value| value.parse::<u64>()5 .ok5 ()) |
1014 | 77 | .is_none_or(|current_value| current_value5 < minimum_value5 ); |
1015 | 77 | if should_update { |
1016 | 76 | platform_properties.insert(property_name.to_string(), minimum_value.to_string()); |
1017 | 76 | }1 |
1018 | 198 | } |
1019 | | |
1020 | 21 | fn apply_default_platform_property( |
1021 | 21 | platform_properties: &mut HashMap<String, String>, |
1022 | 21 | property_name: &str, |
1023 | 21 | default_value: u64, |
1024 | 21 | ) { |
1025 | 21 | if default_value == 0 || platform_properties17 .contains_key17 (property_name17 ) { |
1026 | 6 | return; |
1027 | 15 | } |
1028 | 15 | platform_properties.insert(property_name.to_string(), default_value.to_string()); |
1029 | 21 | } |
1030 | | |
1031 | | #[async_trait] |
1032 | | impl KnownPlatformPropertyProvider for HistoricalResourceScheduler { |
1033 | 0 | async fn get_known_properties(&self, instance_name: &str) -> Result<Vec<String>, Error> { |
1034 | | self.inner_get_known_properties(instance_name).await |
1035 | 0 | } |
1036 | | } |
1037 | | |
1038 | | #[async_trait] |
1039 | | impl ClientStateManager for HistoricalResourceScheduler { |
1040 | | async fn add_action( |
1041 | | &self, |
1042 | | client_operation_id: OperationId, |
1043 | | mut action_info: Arc<ActionInfo>, |
1044 | 75 | ) -> Result<Box<dyn ActionStateResult>, Error> { |
1045 | | let (target_id, action_mnemonic) = Self::current_metadata(); |
1046 | | // An entry that shapes to all zero (nothing measured on a dimension |
1047 | | // the fleet reserves) reserves nothing, so it is no hint: the |
1048 | | // cold-start policy applies and the metric says so. |
1049 | | let hint = if let Some(file) = &self.file_hints { |
1050 | | file.lookup(&action_info, target_id.as_ref(), action_mnemonic.as_ref()) |
1051 | 53 | .map(|numbers| (numbers, "file")) |
1052 | | } else { |
1053 | | self.hint_for_action(&action_info, target_id.as_ref(), action_mnemonic.as_ref()) |
1054 | 14 | .map(|(entry, source)| (self.sketch.shape(&entry), source)) |
1055 | 14 | .filter(|(shaped, _)| !shaped.is_empty()) |
1056 | 13 | .map(|(shaped, source)| (Numbers::from(&shaped), source)) |
1057 | | } |
1058 | 66 | .filter(|(numbers, _)| *numbers != Numbers::default()); |
1059 | | if let Some((shaped, source)) = hint { |
1060 | | self.apply_minimums(Arc::make_mut(&mut action_info), shaped); |
1061 | | record_hint_resolution(source); |
1062 | | } else { |
1063 | | let numbers = self.cold_start_numbers(&action_info, action_mnemonic.as_ref()); |
1064 | | if numbers == Numbers::default() { |
1065 | | record_hint_resolution("none"); |
1066 | | } else { |
1067 | | self.apply_defaults(Arc::make_mut(&mut action_info), numbers); |
1068 | | record_hint_resolution("cold_start"); |
1069 | | } |
1070 | | } |
1071 | | self.scheduler |
1072 | | .as_ref() |
1073 | | .err_tip(|| "Inner scheduler not available for HistoricalResourceScheduler")? |
1074 | | .add_action(client_operation_id, action_info) |
1075 | | .await |
1076 | 75 | } |
1077 | | |
1078 | | async fn filter_operations<'a>( |
1079 | | &'a self, |
1080 | | filter: OperationFilter, |
1081 | 0 | ) -> Result<ActionStateResultStream<'a>, Error> { |
1082 | | self.scheduler |
1083 | | .as_ref() |
1084 | | .err_tip(|| "Inner scheduler not available for HistoricalResourceScheduler")? |
1085 | | .filter_operations(filter) |
1086 | | .await |
1087 | 0 | } |
1088 | | } |
1089 | | |
1090 | | impl MetricsComponent for HistoricalResourceScheduler { |
1091 | 0 | fn publish( |
1092 | 0 | &self, |
1093 | 0 | kind: MetricKind, |
1094 | 0 | field_metadata: MetricFieldData, |
1095 | 0 | ) -> Result<MetricPublishKnownKindData, nativelink_metric::Error> { |
1096 | 0 | match &self.scheduler { |
1097 | 0 | Some(scheduler) => scheduler.publish(kind, field_metadata), |
1098 | 0 | None => Ok(MetricPublishKnownKindData::Component), |
1099 | | } |
1100 | 0 | } |
1101 | | } |
1102 | | |
1103 | | impl RootMetricsComponent for HistoricalResourceScheduler {} |