/build/source/nativelink-sim/src/world.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 | | //! The production components of one run, wired in-process: the |
16 | | //! `SimpleScheduler` on the chosen backend, the `ExecutionServer` and the |
17 | | //! `WorkerApiServer` in front of it, `LocalWorker`s over the in-process |
18 | | //! network, and client tasks that drive `Execute` the way Bazel does. |
19 | | |
20 | | use core::sync::atomic::{AtomicU8, AtomicU32, AtomicU64, Ordering}; |
21 | | use core::time::Duration; |
22 | | use std::collections::HashMap; |
23 | | use std::sync::Arc; |
24 | | |
25 | | use futures::StreamExt; |
26 | | use nativelink_config::cas_server::{ |
27 | | EndpointConfig, ExecutionConfig, LocalWorkerConfig, WithInstanceName, WorkerApiConfig, |
28 | | WorkerProperty, |
29 | | }; |
30 | | use nativelink_config::schedulers::SimpleSpec; |
31 | | use nativelink_config::stores::{EvictionPolicy, MemorySpec, RedisSpec, StoreSpec}; |
32 | | use nativelink_error::{Code, Error, ResultExt}; |
33 | | use nativelink_proto::build::bazel::remote::execution::v2::execution_server::Execution; |
34 | | use nativelink_proto::build::bazel::remote::execution::v2::{ |
35 | | Action as ActionProto, Command, Directory, ExecuteOperationMetadata, ExecuteRequest, |
36 | | ExecuteResponse, ExecutionPolicy, Platform, WaitExecutionRequest, digest_function, platform, |
37 | | }; |
38 | | use nativelink_proto::google::longrunning::{Operation, operation}; |
39 | | use nativelink_redis_tester::FakeRedisBackend; |
40 | | use nativelink_scheduler::known_platform_property_provider::KnownPlatformPropertyProvider; |
41 | | use nativelink_scheduler::memory_awaited_action_db::{MemoryAwaitedActionDb, OperationIdFn}; |
42 | | use nativelink_scheduler::simple_scheduler::SimpleScheduler; |
43 | | use nativelink_scheduler::store_awaited_action_db::StoreAwaitedActionDb; |
44 | | use nativelink_scheduler::worker_scheduler::WorkerScheduler; |
45 | | use nativelink_service::execution_server::ExecutionServer; |
46 | | use nativelink_service::worker_api_server::{NowFn, WorkerApiServer}; |
47 | | use nativelink_store::ac_utils::serialize_and_upload_message; |
48 | | use nativelink_store::default_store_factory::store_factory; |
49 | | use nativelink_store::redis_store::{RedisStore, RedisSubscriptionManager}; |
50 | | use nativelink_store::store_manager::StoreManager; |
51 | | use nativelink_util::action_messages::OperationId; |
52 | | use nativelink_util::background_spawn; |
53 | | use nativelink_util::common::DigestInfo; |
54 | | use nativelink_util::digest_hasher::DigestHasherFunc; |
55 | | use nativelink_util::operation_state_manager::{ |
56 | | ClientStateManager, OperationFilter, OperationStageFlags, OrderDirection, |
57 | | }; |
58 | | use nativelink_util::shutdown_guard::ShutdownGuard; |
59 | | use nativelink_util::store_trait::{SchedulerStore, Store, StoreLike}; |
60 | | use nativelink_worker::local_worker::LocalWorker; |
61 | | use prost::Message as _; |
62 | | use tokio::sync::{Notify, broadcast, watch}; |
63 | | use tokio::task::JoinHandle; |
64 | | use tonic::{Code as TonicCode, Request}; |
65 | | |
66 | | use crate::actions::{Action, Registry, SimActionsManager}; |
67 | | use crate::clock::{SimClock, SimInstant}; |
68 | | use crate::net::{InProcessWorkerClient, Link, Net}; |
69 | | use crate::trace::{Event, Outcome, Stage, Trace}; |
70 | | |
71 | | const INSTANCE_NAME: &str = "sim"; |
72 | | const SCHEDULER_NAME: &str = "sim"; |
73 | | const CAS_NAME: &str = "cas"; |
74 | | |
75 | | /// Which `AwaitedActionDb` the scheduler runs on. |
76 | | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] |
77 | | pub enum Backend { |
78 | | Memory, |
79 | | /// `StoreAwaitedActionDb` over a `RedisStore` on the fake Redis. |
80 | | FakeRedis, |
81 | | } |
82 | | |
83 | | /// One kind of simulated worker. |
84 | | #[derive(Clone, Debug)] |
85 | | pub struct WorkerSpec { |
86 | | pub(crate) max_inflight: u64, |
87 | | pub(crate) properties: Vec<(String, String)>, |
88 | | /// The worker's endpoint timeout; it keeps alive every half of it. |
89 | | pub(crate) keepalive_timeout_s: f32, |
90 | | } |
91 | | |
92 | | impl Default for WorkerSpec { |
93 | 130 | fn default() -> Self { |
94 | 130 | Self { |
95 | 130 | max_inflight: 1, |
96 | 130 | properties: Vec::new(), |
97 | 130 | keepalive_timeout_s: 5.0, |
98 | 130 | } |
99 | 130 | } |
100 | | } |
101 | | |
102 | | impl WorkerSpec { |
103 | | #[must_use] |
104 | 86 | pub const fn max_inflight(mut self, max_inflight: u64) -> Self { |
105 | 86 | self.max_inflight = max_inflight; |
106 | 86 | self |
107 | 86 | } |
108 | | |
109 | | #[must_use] |
110 | 2 | pub fn property(mut self, name: &str, value: &str) -> Self { |
111 | 2 | self.properties.push((name.to_string(), value.to_string())); |
112 | 2 | self |
113 | 2 | } |
114 | | |
115 | | #[must_use] |
116 | 0 | pub const fn keepalive_timeout_s(mut self, seconds: f32) -> Self { |
117 | 0 | self.keepalive_timeout_s = seconds; |
118 | 0 | self |
119 | 0 | } |
120 | | } |
121 | | |
122 | | const CLIENT_RUNNING: u8 = 0; |
123 | | const CLIENT_DONE: u8 = 1; |
124 | | const CLIENT_GONE: u8 = 2; |
125 | | |
126 | | struct Client { |
127 | | state: Arc<AtomicU8>, |
128 | | gone: watch::Sender<bool>, |
129 | | action: Action, |
130 | | } |
131 | | |
132 | | struct Worker { |
133 | | task: JoinHandle<Result<(), Error>>, |
134 | | manager: Arc<SimActionsManager>, |
135 | | spec: WorkerSpec, |
136 | | // Held so the worker's shutdown channel stays open. |
137 | | _shutdown: broadcast::Sender<ShutdownGuard>, |
138 | | } |
139 | | |
140 | | struct Scheduler { |
141 | | client: Arc<SimpleScheduler>, |
142 | | /// The fake Redis listener this incarnation reached the store through. |
143 | | redis_port: Option<u16>, |
144 | | // Held so the worker side lives as long as the servers in `Net` do. |
145 | | _worker: Arc<dyn WorkerScheduler>, |
146 | | } |
147 | | |
148 | | /// Everything one run of a scenario on one backend owns. |
149 | | pub struct World { |
150 | | backend: Backend, |
151 | | spec: SimpleSpec, |
152 | | net: Arc<Net>, |
153 | | trace: Trace, |
154 | | registry: Arc<Registry>, |
155 | | store_manager: Arc<StoreManager>, |
156 | | cas: Store, |
157 | | redis: Option<FakeRedisBackend<RedisSubscriptionManager>>, |
158 | | scheduler: Option<Scheduler>, |
159 | | workers: Vec<Worker>, |
160 | | clients: HashMap<String, Client>, |
161 | | next_worker_id: Arc<AtomicU64>, |
162 | | next_operation_id: Arc<AtomicU64>, |
163 | | /// Matching passes, and the most store commands between two of them. |
164 | | passes: Arc<AtomicU64>, |
165 | | max_commands_per_pass: Arc<AtomicU64>, |
166 | | /// Passes before this time are not measured. |
167 | | measure_from_ms: Arc<AtomicU64>, |
168 | | seed: u64, |
169 | | } |
170 | | |
171 | | impl core::fmt::Debug for World { |
172 | 0 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { |
173 | 0 | f.debug_struct("World") |
174 | 0 | .field("backend", &self.backend) |
175 | 0 | .finish_non_exhaustive() |
176 | 0 | } |
177 | | } |
178 | | |
179 | | impl World { |
180 | 182 | pub(crate) async fn new( |
181 | 182 | backend: Backend, |
182 | 182 | spec: SimpleSpec, |
183 | 182 | seed: u64, |
184 | 182 | trace: Trace, |
185 | 182 | ) -> Result<Self, Error> { |
186 | 182 | let registry = Arc::new(Registry::default()); |
187 | 182 | let net = Net::new(registry.clone(), trace.clone()); |
188 | 182 | let store_manager = Arc::new(StoreManager::new()); |
189 | 182 | store_manager.add_store( |
190 | 182 | CAS_NAME, |
191 | 182 | store_factory( |
192 | 182 | &StoreSpec::Memory(MemorySpec::default()), |
193 | 182 | &store_manager, |
194 | 182 | None, |
195 | 182 | ) |
196 | 182 | .await?0 , |
197 | 0 | )?; |
198 | 182 | let cas = store_manager |
199 | 182 | .get_store(CAS_NAME) |
200 | 182 | .err_tip(|| "The CAS was just added")?0 ; |
201 | 182 | let redis = match backend { |
202 | 114 | Backend::Memory => None, |
203 | 68 | Backend::FakeRedis => Some(FakeRedisBackend::new()), |
204 | | }; |
205 | 182 | let mut world = Self { |
206 | 182 | backend, |
207 | 182 | spec, |
208 | 182 | net, |
209 | 182 | trace, |
210 | 182 | registry, |
211 | 182 | store_manager, |
212 | 182 | cas, |
213 | 182 | redis, |
214 | 182 | scheduler: None, |
215 | 182 | workers: Vec::new(), |
216 | 182 | clients: HashMap::new(), |
217 | 182 | next_worker_id: Arc::new(AtomicU64::new(1)), |
218 | 182 | next_operation_id: Arc::new(AtomicU64::new(1)), |
219 | 182 | passes: Arc::new(AtomicU64::new(0)), |
220 | 182 | max_commands_per_pass: Arc::new(AtomicU64::new(0)), |
221 | 182 | measure_from_ms: Arc::new(AtomicU64::new(0)), |
222 | 182 | seed, |
223 | 182 | }; |
224 | 182 | world.scheduler_up().await?0 ; |
225 | 182 | Ok(world) |
226 | 182 | } |
227 | | |
228 | 0 | pub const fn backend(&self) -> Backend { |
229 | 0 | self.backend |
230 | 0 | } |
231 | | |
232 | 182 | pub const fn spec(&self) -> &SimpleSpec { |
233 | 182 | &self.spec |
234 | 182 | } |
235 | | |
236 | 190 | pub const fn trace(&self) -> &Trace { |
237 | 190 | &self.trace |
238 | 190 | } |
239 | | |
240 | 188 | pub const fn redis(&self) -> Option<&FakeRedisBackend<RedisSubscriptionManager>> { |
241 | 188 | self.redis.as_ref() |
242 | 188 | } |
243 | | |
244 | 182 | pub fn passes(&self) -> u64 { |
245 | 182 | self.passes.load(Ordering::Acquire) |
246 | 182 | } |
247 | | |
248 | 182 | pub fn max_commands_per_pass(&self) -> u64 { |
249 | 182 | self.max_commands_per_pass.load(Ordering::Acquire) |
250 | 182 | } |
251 | | |
252 | | /// Measures store commands per pass only from `ms` on. |
253 | 180 | pub(crate) fn measure_from(&self, ms: u64) { |
254 | 180 | self.measure_from_ms.store(ms, Ordering::Release); |
255 | 180 | } |
256 | | |
257 | 182 | pub(crate) fn worker_specs(&self) -> Vec<(String, WorkerSpec)> { |
258 | 182 | self.workers |
259 | 182 | .iter() |
260 | 182 | .enumerate() |
261 | 1.19k | .map182 (|(i, w)| (worker_name(i), w.spec.clone())) |
262 | 182 | .collect() |
263 | 182 | } |
264 | | |
265 | | /// Builds the scheduler and its servers and starts serving. |
266 | 187 | pub(crate) async fn scheduler_up(&mut self) -> Result<(), Error> { |
267 | 187 | if self.scheduler.is_some() { |
268 | 0 | return Ok(()); |
269 | 187 | } |
270 | 187 | let notify = Arc::new(Notify::new()); |
271 | 187 | let passes = self.passes.clone(); |
272 | 187 | let max_commands = self.max_commands_per_pass.clone(); |
273 | 187 | let measure_from_ms = self.measure_from_ms.clone(); |
274 | 187 | let redis = self.redis.clone(); |
275 | 35.0k | let on_pass187 = move || { |
276 | 35.0k | passes.fetch_add(1, Ordering::AcqRel); |
277 | 35.0k | if let Some(redis5.54k ) = &redis { |
278 | 5.54k | let commands: u64 = redis.command_counts().values().sum(); |
279 | 5.54k | redis.reset_command_counts(); |
280 | 5.54k | if SimClock::now_ms() >= measure_from_ms.load(Ordering::Acquire) { |
281 | 3.10k | max_commands.fetch_max(commands, Ordering::AcqRel); |
282 | 3.10k | }2.44k |
283 | 29.5k | } |
284 | 35.0k | tokio::time::sleep(Duration::from_millis(1)) |
285 | 35.0k | }; |
286 | 187 | let mut redis_port = None; |
287 | 187 | let (client, worker) = match self.backend { |
288 | 116 | Backend::Memory => SimpleScheduler::new_with_fleet_exchange_interval( |
289 | 116 | &self.spec, |
290 | 116 | MemoryAwaitedActionDb::new_with_operation_id_fn( |
291 | 116 | &EvictionPolicy::default(), |
292 | 116 | notify.clone(), |
293 | | SimInstant::now, |
294 | 116 | OperationIdFn::new(operation_ids(self.next_operation_id.clone())), |
295 | | ), |
296 | 116 | on_pass, |
297 | 116 | notify, |
298 | | SimInstant::now, |
299 | 116 | None, |
300 | 116 | None, |
301 | 116 | Duration::from_secs(5), |
302 | | ), |
303 | | Backend::FakeRedis => { |
304 | 71 | let fake = self |
305 | 71 | .redis |
306 | 71 | .clone() |
307 | 71 | .err_tip(|| "FakeRedis backend without a fake")?0 ; |
308 | | // A listener per incarnation, so stopping this scheduler can |
309 | | // take its connections with it. |
310 | 71 | let port = fake.clone().run().await; |
311 | 71 | redis_port = Some(port); |
312 | 71 | let store = RedisStore::new_standard(RedisSpec { |
313 | 71 | addresses: vec![format!("redis://127.0.0.1:{port}")], |
314 | 71 | experimental_pub_sub_channel: Some("sim_channel".to_string()), |
315 | 71 | ..Default::default() |
316 | 71 | }) |
317 | 71 | .await?0 ; |
318 | 71 | fake.set_subscription_manager(store.subscription_manager().await?0 ); |
319 | 71 | let db = StoreAwaitedActionDb::new( |
320 | 71 | store, |
321 | 71 | notify.clone(), |
322 | 71 | SimInstant::now, |
323 | 71 | operation_ids(self.next_operation_id.clone()), |
324 | 71 | self.spec.retain_completed_for_s, |
325 | 71 | self.spec.client_action_timeout_s, |
326 | 71 | false, |
327 | 71 | ) |
328 | 71 | .await?0 ; |
329 | 71 | SimpleScheduler::new_with_fleet_exchange_interval( |
330 | 71 | &self.spec, |
331 | 71 | db, |
332 | 71 | on_pass, |
333 | 71 | notify, |
334 | | SimInstant::now, |
335 | 71 | None, |
336 | 71 | None, |
337 | 71 | Duration::from_secs(5), |
338 | | ) |
339 | | } |
340 | | }; |
341 | | |
342 | 187 | let mut providers: HashMap<String, Arc<dyn KnownPlatformPropertyProvider>> = HashMap::new(); |
343 | 187 | providers.insert(SCHEDULER_NAME.to_string(), client.clone()); |
344 | 187 | let execution = ExecutionServer::new( |
345 | 187 | &[WithInstanceName { |
346 | 187 | instance_name: INSTANCE_NAME.to_string(), |
347 | 187 | config: ExecutionConfig { |
348 | 187 | cas_store: CAS_NAME.to_string(), |
349 | 187 | scheduler: SCHEDULER_NAME.to_string(), |
350 | 187 | experimental_buck2_invocation_isolation: false, |
351 | 187 | execute_stream_heartbeat_s: 0, |
352 | 187 | }, |
353 | 187 | }], |
354 | 187 | &providers, |
355 | 187 | &self.store_manager, |
356 | 0 | )? |
357 | 187 | .with_now_fn(SimClock::now); |
358 | | |
359 | 187 | let mut schedulers: HashMap<String, Arc<dyn WorkerScheduler>> = HashMap::new(); |
360 | 187 | schedulers.insert(SCHEDULER_NAME.to_string(), worker.clone()); |
361 | 187 | let config = WorkerApiConfig { |
362 | 187 | scheduler: SCHEDULER_NAME.to_string(), |
363 | 187 | disable_kill_revoked_operations: false, |
364 | 187 | kill_revoked_operations_interval_s: 0, |
365 | 187 | }; |
366 | 187 | let now_fn: Arc<NowFn> = Arc::new(Box::new(since_epoch)); |
367 | 187 | WorkerApiServer::spawn_timers(&config, &schedulers, &now_fn); |
368 | 187 | let next_worker_id = self.next_worker_id.clone(); |
369 | 187 | let worker_api = |
370 | 187 | WorkerApiServer::new_with_now_fn(&config, &schedulers, Box::new(since_epoch), [0; 6])?0 |
371 | 1.98k | .with_worker_id_fn187 (Box::new187 (move || { |
372 | 1.98k | format!("{}", next_worker_id.fetch_add(1, Ordering::AcqRel)) |
373 | 1.98k | })); |
374 | | |
375 | 187 | self.net.serve(Arc::new(worker_api), execution); |
376 | 187 | self.scheduler = Some(Scheduler { |
377 | 187 | client, |
378 | 187 | redis_port, |
379 | 187 | _worker: worker, |
380 | 187 | }); |
381 | 187 | self.trace.record(Event::SchedulerUp); |
382 | 187 | Ok(()) |
383 | 187 | } |
384 | | |
385 | | /// Stops serving and drops this process's scheduler; every connection |
386 | | /// to it closes. The store, Redis or memory, is what survives. |
387 | | /// What a dead process leaves behind is only the store: its tasks may |
388 | | /// linger in this runtime, but they lose the servers and, on Redis, |
389 | | /// every connection to the store. |
390 | 187 | pub(crate) fn scheduler_down(&mut self) { |
391 | 187 | if let Some(scheduler) = self.scheduler.take() { |
392 | 187 | self.net.unserve(); |
393 | 187 | if let (Some(redis71 ), Some(port71 )) = (&self.redis, scheduler.redis_port) { |
394 | 71 | redis.retire_listener(port); |
395 | 116 | } |
396 | 187 | self.trace.record(Event::SchedulerDown); |
397 | 0 | } |
398 | 187 | } |
399 | | |
400 | 226 | pub(crate) fn add_workers(&mut self, count: usize, spec: &WorkerSpec) { |
401 | 226 | for _ in 0..count { |
402 | 1.19k | let index = self.workers.len(); |
403 | 1.19k | let name = worker_name(index); |
404 | 1.19k | let config = LocalWorkerConfig { |
405 | 1.19k | name: name.clone(), |
406 | 1.19k | max_inflight_tasks: spec.max_inflight, |
407 | 1.19k | platform_properties: spec |
408 | 1.19k | .properties |
409 | 1.19k | .iter() |
410 | 1.19k | .map(|(k, v)| (k6 .clone6 (), WorkerProperty::Values(6 vec!6 [v.clone()]))) |
411 | 1.19k | .collect(), |
412 | 1.19k | worker_api_endpoint: EndpointConfig { |
413 | 1.19k | timeout: Some(spec.keepalive_timeout_s), |
414 | 1.19k | ..Default::default() |
415 | 1.19k | }, |
416 | 1.19k | ..Default::default() |
417 | | }; |
418 | 1.19k | let manager = Arc::new(SimActionsManager::new(&name, self.net.clone())); |
419 | 1.19k | let net = self.net.clone(); |
420 | 1.19k | let connect_name = name.clone(); |
421 | | // A fixed sequence per worker, so backoffs repeat run to run. |
422 | 1.19k | let jitter = AtomicU32::new( |
423 | 1.19k | u32::try_from(self.seed.wrapping_add(index as u64 * 7919) % 1000).unwrap_or(0), |
424 | | ); |
425 | 1.19k | let worker = LocalWorker::new_with_connection_factory_and_actions_manager( |
426 | 1.19k | Arc::new(config), |
427 | 1.19k | manager.clone(), |
428 | 1.99k | Box::new1.19k (move || { |
429 | 1.99k | let client = InProcessWorkerClient::connect(&connect_name, &net); |
430 | 1.99k | Box::pin(async move { client }) |
431 | 1.99k | }), |
432 | 1.19k | Box::new(|duration| Box::pin807 (tokio::time::sleep807 (duration807 ))), |
433 | | ) |
434 | 1.19k | .with_jitter_fn(Box::new(move || jitter807 .fetch_add807 (379, Ordering::AcqRel807 ))); |
435 | 1.19k | let (shutdown, shutdown_rx) = broadcast::channel(1); |
436 | 1.19k | let trace = self.trace.clone(); |
437 | 1.19k | let stopped_name = name.clone(); |
438 | 1.19k | let task = background_spawn!("sim_worker", async move { |
439 | 1.19k | let result0 = worker.run(shutdown_rx).await; |
440 | 0 | trace.record(Event::WorkerStopped { |
441 | 0 | worker: stopped_name, |
442 | 0 | }); |
443 | 0 | result |
444 | 0 | }); |
445 | 1.19k | self.trace.record(Event::WorkerStarted { worker: name }); |
446 | 1.19k | self.workers.push(Worker { |
447 | 1.19k | task, |
448 | 1.19k | manager, |
449 | 1.19k | spec: spec.clone(), |
450 | 1.19k | _shutdown: shutdown, |
451 | 1.19k | }); |
452 | | } |
453 | 226 | } |
454 | | |
455 | | /// The worker process dies: its task is aborted and its connection |
456 | | /// closes. |
457 | 71 | pub(crate) fn kill_worker(&self, index: usize) { |
458 | 71 | if let Some(worker) = self.workers.get(index) { |
459 | 71 | if worker.task.is_finished() { |
460 | 4 | return; |
461 | 67 | } |
462 | 67 | worker.task.abort(); |
463 | 67 | self.net.cut(&worker_name(index)); |
464 | 67 | self.trace.record(Event::WorkerStopped { |
465 | 67 | worker: worker_name(index), |
466 | 67 | }); |
467 | 0 | } |
468 | 71 | } |
469 | | |
470 | 69 | pub(crate) fn cut_worker(&self, index: usize) { |
471 | 69 | self.net.cut(&worker_name(index)); |
472 | 69 | } |
473 | | |
474 | 112 | pub(crate) fn set_link(&self, index: usize, link: Link) { |
475 | 112 | self.net.set_link(&worker_name(index), link); |
476 | 112 | } |
477 | | |
478 | 15.9k | pub(crate) async fn execute(&mut self, op: &str, action: &Action) -> Result<(), Error> { |
479 | 15.9k | let action_digest = self.upload(action).await?0 ; |
480 | 15.9k | self.trace.record(Event::Submit { |
481 | 15.9k | op: op.to_string(), |
482 | 15.9k | action: action.label(), |
483 | 15.9k | priority: action.priority, |
484 | 15.9k | }); |
485 | 15.9k | let request = ExecuteRequest { |
486 | 15.9k | instance_name: INSTANCE_NAME.to_string(), |
487 | 15.9k | skip_cache_lookup: !action.cacheable, |
488 | 15.9k | action_digest: Some(action_digest.into()), |
489 | 15.9k | execution_policy: Some(ExecutionPolicy { |
490 | 15.9k | priority: action.priority, |
491 | 15.9k | }), |
492 | 15.9k | results_cache_policy: None, |
493 | 15.9k | digest_function: digest_function::Value::Sha256.into(), |
494 | 15.9k | inline_stdout: false, |
495 | 15.9k | inline_stderr: false, |
496 | 15.9k | inline_output_files: Vec::new(), |
497 | 15.9k | }; |
498 | 15.9k | let state = Arc::new(AtomicU8::new(CLIENT_RUNNING)); |
499 | 15.9k | let (gone, gone_rx) = watch::channel(false); |
500 | 15.9k | drop(background_spawn!( |
501 | | "sim_client", |
502 | 15.9k | run_client( |
503 | 15.9k | self.net.clone(), |
504 | 15.9k | op.to_string(), |
505 | 15.9k | request, |
506 | 15.9k | state.clone(), |
507 | 15.9k | gone_rx |
508 | | ) |
509 | | )); |
510 | 15.9k | self.clients.insert( |
511 | 15.9k | op.to_string(), |
512 | 15.9k | Client { |
513 | 15.9k | state, |
514 | 15.9k | gone, |
515 | 15.9k | action: action.clone(), |
516 | 15.9k | }, |
517 | | ); |
518 | 15.9k | Ok(()) |
519 | 15.9k | } |
520 | | |
521 | 4.05k | pub(crate) fn client_gone(&self, op: &str) { |
522 | 4.05k | if let Some(client4.03k ) = self.clients.get(op) { |
523 | 4.03k | client.gone.send_replace(true); |
524 | 4.03k | }22 |
525 | 4.05k | } |
526 | | |
527 | 15.9k | pub(crate) fn action_of(&self, op: &str) -> Option<Action> { |
528 | 15.9k | self.clients.get(op).map(|c| c.action.clone()) |
529 | 15.9k | } |
530 | | |
531 | 15.9k | async fn upload(&self, action: &Action) -> Result<DigestInfo, Error> { |
532 | 15.9k | let command = Command { |
533 | 15.9k | arguments: vec!["sim".to_string(), action.label()], |
534 | 15.9k | ..Default::default() |
535 | 15.9k | }; |
536 | 15.9k | let command_digest = serialize_and_upload_message( |
537 | 15.9k | &command, |
538 | 15.9k | self.cas.as_pin(), |
539 | 15.9k | &mut DigestHasherFunc::Sha256.hasher(), |
540 | 15.9k | ) |
541 | 15.9k | .await?0 ; |
542 | 15.9k | let input_root_digest = serialize_and_upload_message( |
543 | 15.9k | &Directory::default(), |
544 | 15.9k | self.cas.as_pin(), |
545 | 15.9k | &mut DigestHasherFunc::Sha256.hasher(), |
546 | 15.9k | ) |
547 | 15.9k | .await?0 ; |
548 | 15.9k | let proto = ActionProto { |
549 | 15.9k | command_digest: Some(command_digest.into()), |
550 | 15.9k | input_root_digest: Some(input_root_digest.into()), |
551 | 15.9k | timeout: action.timeout.map(prost_types_duration), |
552 | 15.9k | do_not_cache: !action.cacheable, |
553 | 15.9k | platform: (!action.properties.is_empty()).then(|| Platform { |
554 | 2 | properties: action |
555 | 2 | .properties |
556 | 2 | .iter() |
557 | 2 | .map(|(name, value)| platform::Property { |
558 | 2 | name: name.clone(), |
559 | 2 | value: value.clone(), |
560 | 2 | }) |
561 | 2 | .collect(), |
562 | 2 | }), |
563 | 15.9k | ..Default::default() |
564 | | }; |
565 | 15.9k | let digest = serialize_and_upload_message( |
566 | 15.9k | &proto, |
567 | 15.9k | self.cas.as_pin(), |
568 | 15.9k | &mut DigestHasherFunc::Sha256.hasher(), |
569 | 15.9k | ) |
570 | 15.9k | .await?0 ; |
571 | 15.9k | self.registry.insert(digest, action.clone()); |
572 | 15.9k | Ok(digest) |
573 | 15.9k | } |
574 | | |
575 | | /// Clients still waiting on an operation. |
576 | 30.5k | fn clients_waiting(&self) -> usize { |
577 | 30.5k | self.clients |
578 | 30.5k | .values() |
579 | 10.1M | .filter30.5k (|c| c.state.load(Ordering::Acquire) == CLIENT_RUNNING) |
580 | 30.5k | .count() |
581 | 30.5k | } |
582 | | |
583 | | /// Executions the live workers have not finished. |
584 | 4.69k | fn executions_running(&self) -> usize { |
585 | 4.69k | self.workers |
586 | 4.69k | .iter() |
587 | 21.7k | .filter4.69k (|w| !w.task.is_finished()) |
588 | 20.2k | .map4.69k (|w| w.manager.running()) |
589 | 4.69k | .sum() |
590 | 4.69k | } |
591 | | |
592 | | /// Advances until every client has its answer, no worker is running |
593 | | /// anything and nothing has reached the trace for `QUIET`, or `budget` |
594 | | /// runs out. Returns whether it got quiet. |
595 | 182 | pub(crate) async fn settle(&self, clock: &SimClock, budget: Duration) -> bool { |
596 | | const QUIET: Duration = Duration::from_secs(2); |
597 | 182 | let poll = Duration::from_millis(100); |
598 | 182 | let mut waited = Duration::ZERO; |
599 | 182 | let mut quiet_for = Duration::ZERO; |
600 | 182 | let mut seen = self.trace.len(); |
601 | | loop { |
602 | 30.5k | let idle = self.clients_waiting() == 0 && self.executions_running() == 04.51k ; |
603 | 30.5k | let records = self.trace.len(); |
604 | 30.5k | if idle && records == seen3.80k { |
605 | 3.65k | if quiet_for >= QUIET { |
606 | 176 | return true; |
607 | 3.47k | } |
608 | 26.9k | } else { |
609 | 26.9k | quiet_for = Duration::ZERO; |
610 | 26.9k | } |
611 | 30.3k | seen = records; |
612 | 30.3k | if waited >= budget { |
613 | 6 | return false; |
614 | 30.3k | } |
615 | 30.3k | clock.advance(poll).await; |
616 | 30.3k | waited += poll; |
617 | 30.3k | if idle { |
618 | 3.62k | quiet_for += poll; |
619 | 26.7k | } |
620 | | } |
621 | 182 | } |
622 | | |
623 | | /// Records what the scheduler has executing next to what the workers |
624 | | /// run. |
625 | 182 | pub(crate) async fn record_ledger(&self) -> Result<(), Error> { |
626 | 182 | let scheduler_executing = match &self.scheduler { |
627 | 182 | Some(scheduler) => { |
628 | 182 | let stream = scheduler |
629 | 182 | .client |
630 | 182 | .filter_operations(OperationFilter { |
631 | 182 | stages: OperationStageFlags::Executing, |
632 | 182 | // The Redis backend only lists in this order. |
633 | 182 | order_by_priority_direction: Some(OrderDirection::Desc), |
634 | 182 | ..Default::default() |
635 | 182 | }) |
636 | 182 | .await?0 ; |
637 | 182 | stream.count().await |
638 | | } |
639 | 0 | None => 0, |
640 | | }; |
641 | 182 | self.trace.record(Event::Ledger { |
642 | 182 | scheduler_executing, |
643 | 182 | workers_running: self.executions_running(), |
644 | 182 | }); |
645 | 182 | Ok(()) |
646 | 182 | } |
647 | | |
648 | | /// Stops every task this world started, so the runtime can go. |
649 | 182 | pub(crate) fn shutdown(&mut self) { |
650 | 15.9k | for client in self.clients182 .values182 () { |
651 | 15.9k | client.gone.send_replace(true); |
652 | 15.9k | } |
653 | 1.19k | for (index, worker) in self.workers.iter()182 .enumerate182 () { |
654 | 1.19k | worker.task.abort(); |
655 | 1.19k | self.net.cut(&worker_name(index)); |
656 | 1.19k | } |
657 | 182 | self.scheduler_down(); |
658 | 182 | } |
659 | | } |
660 | | |
661 | | /// Operation ids in order, `op-1` first, carried over a restart so a new |
662 | | /// scheduler never reuses one. |
663 | 187 | fn operation_ids(next: Arc<AtomicU64>) -> impl Fn() -> OperationId + Clone + Send + Sync + 'static { |
664 | 16.1k | move || OperationId::from(format!("op-{}", next.fetch_add(1, Ordering::AcqRel))) |
665 | 187 | } |
666 | | |
667 | | /// The virtual time since the Unix epoch, as `WorkerApiServer` reads it. |
668 | | #[allow(clippy::unnecessary_wraps, reason = "the shape `NowFn` takes")] |
669 | 110k | fn since_epoch() -> Result<Duration, Error> { |
670 | 110k | Ok(SimClock::now() |
671 | 110k | .duration_since(std::time::UNIX_EPOCH) |
672 | 110k | .unwrap_or_default()) |
673 | 110k | } |
674 | | |
675 | 4.00k | pub(crate) fn worker_name(index: usize) -> String { |
676 | 4.00k | format!("w{index}") |
677 | 4.00k | } |
678 | | |
679 | 0 | fn prost_types_duration(duration: Duration) -> prost_types::Duration { |
680 | 0 | prost_types::Duration { |
681 | 0 | seconds: i64::try_from(duration.as_secs()).unwrap_or(i64::MAX), |
682 | 0 | nanos: i32::try_from(duration.subsec_nanos()).unwrap_or(0), |
683 | 0 | } |
684 | 0 | } |
685 | | |
686 | 40.9k | fn stage_of(operation: &Operation) -> Stage { |
687 | 40.9k | operation |
688 | 40.9k | .metadata |
689 | 40.9k | .as_ref() |
690 | 40.9k | .and_then(|any| ExecuteOperationMetadata::decode(any.value.as_slice()).ok()) |
691 | 40.9k | .map_or(Stage::Unknown, |metadata| Stage::from_proto(metadata.stage)) |
692 | 40.9k | } |
693 | | |
694 | 9.96k | fn outcome_of(operation: &Operation) -> Outcome { |
695 | 9.96k | match &operation.result { |
696 | 0 | Some(operation::Result::Error(status)) => Outcome::Failed(status.code), |
697 | 9.96k | Some(operation::Result::Response(any)) => { |
698 | 9.96k | match ExecuteResponse::decode(any.value.as_slice()) { |
699 | 9.96k | Ok(response) => match response.status { |
700 | 9.96k | Some(status2 ) if status.code != 02 => Outcome::Failed(status.code)2 , |
701 | 9.96k | _ => Outcome::Exited(response.result.map_or(0, |r| r.exit_code)), |
702 | | }, |
703 | 0 | Err(_) => Outcome::Failed(Code::Internal as i32), |
704 | | } |
705 | | } |
706 | 0 | None => Outcome::Failed(Code::Unknown as i32), |
707 | | } |
708 | 9.96k | } |
709 | | |
710 | | /// How many times a client retries a call that failed outright before it |
711 | | /// gives up, as Bazel's `--remote_retries` does. |
712 | | const CLIENT_RETRIES: u32 = 5; |
713 | | const CLIENT_RETRY_DELAY: Duration = Duration::from_secs(1); |
714 | | |
715 | | /// One client operation: `Execute`, then read the stream to the end. A |
716 | | /// stream that ends or fails early is re-subscribed with `WaitExecution`; |
717 | | /// one the server no longer knows is executed again. |
718 | 15.9k | async fn run_client( |
719 | 15.9k | net: Arc<Net>, |
720 | 15.9k | op: String, |
721 | 15.9k | request: ExecuteRequest, |
722 | 15.9k | state: Arc<AtomicU8>, |
723 | 15.9k | mut gone: watch::Receiver<bool>, |
724 | 15.9k | ) { |
725 | 15.9k | let trace = net.trace().clone(); |
726 | 15.9k | let mut name: Option<String> = None; |
727 | 15.9k | let mut failures = 0; |
728 | 15.9k | let mut last_stage = None; |
729 | | loop { |
730 | 16.6k | if *gone.borrow() { |
731 | 0 | break; |
732 | 16.6k | } |
733 | 16.6k | let Some(server16.5k ) = net.execution() else { |
734 | 3 | if wait_or_gone(&mut gone, CLIENT_RETRY_DELAY).await { |
735 | 0 | break; |
736 | 3 | } |
737 | 3 | continue; |
738 | | }; |
739 | 16.5k | let mut epoch = net.epoch(); |
740 | 16.5k | let start_epoch = *epoch.borrow_and_update(); |
741 | 16.5k | let response = match &name { |
742 | 16.1k | None => server.execute(Request::new(request.clone())).await, |
743 | 409 | Some(name) => { |
744 | 409 | server |
745 | 409 | .wait_execution(Request::new(WaitExecutionRequest { name: name.clone() })) |
746 | 409 | .await |
747 | | } |
748 | | }; |
749 | 16.5k | let mut stream16.3k = match response { |
750 | 16.3k | Ok(response) => response.into_inner(), |
751 | 206 | Err(status) => { |
752 | 206 | if name.is_some() && status.code() == TonicCode::NotFound { |
753 | 206 | trace.record(Event::ClientResubmit { op: op.clone() }); |
754 | 206 | name = None; |
755 | 206 | last_stage = None; |
756 | 206 | continue; |
757 | 0 | } |
758 | 0 | failures += 1; |
759 | 0 | if failures > CLIENT_RETRIES { |
760 | 0 | trace.record(Event::ClientDone { |
761 | 0 | op: op.clone(), |
762 | 0 | outcome: Outcome::Failed(status.code() as i32), |
763 | 0 | }); |
764 | 0 | state.store(CLIENT_DONE, Ordering::Release); |
765 | 0 | return; |
766 | 0 | } |
767 | 0 | trace.record(Event::ClientReconnect { |
768 | 0 | op: op.clone(), |
769 | 0 | code: status.code() as i32, |
770 | 0 | }); |
771 | 0 | if wait_or_gone(&mut gone, CLIENT_RETRY_DELAY).await { |
772 | 0 | break; |
773 | 0 | } |
774 | 0 | continue; |
775 | | } |
776 | | }; |
777 | | loop { |
778 | 47.3k | tokio::select! { |
779 | 47.3k | item40.9k = stream.next() => match item40.9k { |
780 | 40.9k | Some(Ok(operation)) => { |
781 | 40.9k | name = Some(operation.name.clone()); |
782 | 40.9k | let seen = stage_of(&operation); |
783 | 40.9k | if last_stage != Some(seen) { |
784 | 35.4k | trace.record(Event::ClientStage { op: op.clone(), stage: seen }); |
785 | 35.4k | last_stage = Some(seen); |
786 | 35.4k | }5.52k |
787 | 40.9k | if operation.done { |
788 | 9.96k | trace.record(Event::ClientDone { |
789 | 9.96k | op: op.clone(), |
790 | 9.96k | outcome: outcome_of(&operation), |
791 | 9.96k | }); |
792 | 9.96k | state.store(CLIENT_DONE, Ordering::Release); |
793 | 9.96k | return; |
794 | 30.9k | } |
795 | | } |
796 | 6 | Some(Err(status)) => { |
797 | 6 | trace.record(Event::ClientReconnect { op: op.clone(), code: status.code() as i32 }); |
798 | 6 | break; |
799 | | } |
800 | | None => { |
801 | 0 | trace.record(Event::ClientReconnect { op: op.clone(), code: -1 }); |
802 | 0 | break; |
803 | | } |
804 | | }, |
805 | 47.3k | changed4.01k = gone.changed() => { |
806 | 4.01k | if changed.is_err() || *gone.borrow() { |
807 | 4.01k | drop(stream); |
808 | 4.01k | trace.record(Event::ClientGone { op: op.clone() }); |
809 | 4.01k | state.store(CLIENT_GONE, Ordering::Release); |
810 | 4.01k | return; |
811 | 0 | } |
812 | | } |
813 | 47.3k | changed403 = epoch.changed() => { |
814 | 403 | if changed.is_err() || *epoch.borrow() != start_epoch { |
815 | 403 | trace.record(Event::ClientReconnect { op: op.clone(), code: Code::Unavailable as i32 }); |
816 | 403 | break; |
817 | 0 | } |
818 | | } |
819 | | } |
820 | | } |
821 | 409 | last_stage = None; |
822 | 409 | if wait_or_gone(&mut gone, CLIENT_RETRY_DELAY).await { |
823 | 0 | break; |
824 | 409 | } |
825 | | } |
826 | 0 | trace.record(Event::ClientGone { op }); |
827 | 0 | state.store(CLIENT_GONE, Ordering::Release); |
828 | 13.9k | } |
829 | | |
830 | | /// Sleeps `delay`; true when the client left meanwhile. |
831 | 412 | async fn wait_or_gone(gone: &mut watch::Receiver<bool>, delay: Duration) -> bool { |
832 | 412 | tokio::select! { |
833 | 412 | () = tokio::time::sleep(delay) => *gone.borrow(), |
834 | 412 | changed0 = gone.changed() => changed0 .is_err0 () || *gone.borrow()0 , |
835 | | } |
836 | 412 | } |