/build/source/nativelink-scheduler/src/simple_scheduler_state_manager.rs
Line | Count | Source |
1 | | // Copyright 2024 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 | | use core::ops::Bound; |
16 | | use core::time::Duration; |
17 | | use std::collections::HashMap; |
18 | | use std::string::ToString; |
19 | | use std::sync::{Arc, Weak}; |
20 | | use std::time::SystemTime; |
21 | | |
22 | | use async_lock::Mutex; |
23 | | use async_trait::async_trait; |
24 | | use futures::future::BoxFuture; |
25 | | use futures::{StreamExt, TryStreamExt, stream}; |
26 | | use nativelink_error::{Code, Error, ResultExt, make_err}; |
27 | | use nativelink_metric::MetricsComponent; |
28 | | use nativelink_proto::com::github::trace_machina::nativelink::remote_execution::AttemptEndReason; |
29 | | use nativelink_util::action_messages::{ |
30 | | ActionInfo, ActionResult, ActionStage, ActionState, ActionUniqueQualifier, ExecutionMetadata, |
31 | | LOST_RESULT_ERROR_TIP, OperationId, STORE_UNREACHABLE_ERROR_TIP, WorkerId, |
32 | | }; |
33 | | use nativelink_util::instant_wrapper::InstantWrapper; |
34 | | use nativelink_util::metrics::{ |
35 | | AgeBuckets, EXECUTION_METRICS, EXECUTION_RESULT, EXECUTION_STAGE, ExecutionResult, |
36 | | ExecutionStage, record_awaited_action_orphan, record_dispatch_requeue, record_queue_retired, |
37 | | }; |
38 | | use nativelink_util::operation_state_manager::{ |
39 | | ActionStateResult, ActionStateResultStream, ClientStateManager, DispatchFacts, Escalation, |
40 | | MatchingEngineStateManager, OperationFilter, OperationStageFlags, OrderDirection, |
41 | | UpdateOperationType, WorkerStateManager, |
42 | | }; |
43 | | use nativelink_util::origin_event::OriginMetadata; |
44 | | use nativelink_util::platform_properties::PlatformProperties; |
45 | | use opentelemetry::KeyValue; |
46 | | use tracing::{debug, info, trace, warn}; |
47 | | |
48 | | use super::awaited_action_db::{ |
49 | | AwaitedAction, AwaitedActionDb, AwaitedActionSubscriber, PeerScheduler, |
50 | | SortedAwaitedActionState, |
51 | | }; |
52 | | use crate::worker_registry::{ORPHANED_ACTION_TIMEOUT, SharedWorkerRegistry, WorkerLiveness}; |
53 | | |
54 | | /// An attempt the executing sweep ended, for the scheduler to report. |
55 | | #[derive(Debug, Clone, PartialEq, Eq)] |
56 | | pub struct SweptAttempt { |
57 | | pub operation_id: OperationId, |
58 | | pub worker_id: WorkerId, |
59 | | pub reason: AttemptEndReason, |
60 | | pub detail: String, |
61 | | } |
62 | | |
63 | | /// What one executing sweep did. |
64 | | #[derive(Debug, Default, Clone, PartialEq, Eq)] |
65 | | pub struct ExecutingSweepReport { |
66 | | /// Retired as overdue, through `timeout_operation_id`. |
67 | | pub retired: u64, |
68 | | /// Sent back to the queue because their worker no longer ran them. |
69 | | pub requeued: u64, |
70 | | /// Could not be moved this sweep; they are tried again on the next. |
71 | | pub failed: u64, |
72 | | /// How long each executing action the sweep read had been executing, |
73 | | /// counted before any was retired or requeued. |
74 | | pub ages: AgeBuckets, |
75 | | /// The attempts it retired or requeued. |
76 | | pub ended: Vec<SweptAttempt>, |
77 | | } |
78 | | |
79 | | /// Why the scheduler times out an executing action. |
80 | | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
81 | | pub enum TimeoutCause { |
82 | | /// The action's `Action.timeout` passed, plus the grace a live worker |
83 | | /// has to report its own result. |
84 | | ActionTimeout { timeout: Duration, grace: Duration }, |
85 | | /// The worker sent no update on the action for this long. |
86 | | NoWorkerUpdate(Duration), |
87 | | } |
88 | | |
89 | | impl core::fmt::Display for TimeoutCause { |
90 | 13 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { |
91 | 13 | match self { |
92 | 1 | Self::ActionTimeout { timeout, grace } => write!( |
93 | 1 | f, |
94 | | "exceeded its Action.timeout of {} seconds, and its worker had not reported within {} seconds after that", |
95 | 1 | timeout.as_secs_f32(), |
96 | 1 | grace.as_secs_f32(), |
97 | | ), |
98 | 12 | Self::NoWorkerUpdate(ceiling) => write!( |
99 | 12 | f, |
100 | | "timed out after {} seconds with no update from its worker", |
101 | 12 | ceiling.as_secs_f32(), |
102 | | ), |
103 | | } |
104 | 13 | } |
105 | | } |
106 | | |
107 | | /// Maximum number of times an update to the database |
108 | | /// can fail before giving up. |
109 | | const MAX_UPDATE_RETRIES: usize = 5; |
110 | | |
111 | | /// Requeues the same action asking for more: the next dispatch carries the |
112 | | /// raised reservation and the worker holds it to that. The last step also |
113 | | /// asks for the worker's whole CPU, so it runs alone. |
114 | 23 | fn raise_reservation(awaited_action: &mut AwaitedAction, escalation: &Escalation) { |
115 | 23 | let mut action_info = (**awaited_action.action_info()).clone(); |
116 | 23 | action_info |
117 | 23 | .platform_properties |
118 | 23 | .insert(escalation.property.clone(), escalation.value.to_string()); |
119 | 23 | if let Some((cpu_property0 , cpu_value0 )) = &escalation.cpu { |
120 | 0 | action_info |
121 | 0 | .platform_properties |
122 | 0 | .insert(cpu_property.clone(), cpu_value.to_string()); |
123 | 23 | } |
124 | 23 | awaited_action.set_action_info(Arc::new(action_info)); |
125 | 23 | } |
126 | | |
127 | | /// Base delay for exponential backoff on version conflicts (in ms). |
128 | | const BASE_RETRY_DELAY_MS: u64 = 10; |
129 | | |
130 | | /// Maximum jitter to add to retry delay (in ms). |
131 | | const MAX_RETRY_JITTER_MS: u64 = 20; |
132 | | |
133 | | /// How long to wait before the `retry_count`th attempt at an update that |
134 | | /// lost a version conflict: exponential in the attempt, plus jitter. |
135 | | /// The store errors an operation update retries within its budget: the |
136 | | /// record moved (`Aborted`), or the store did not take the write for a |
137 | | /// reason that passes (unreachable, slow, out of room, or a server reply the |
138 | | /// client could not classify, which is what a Redis `-ERR` arrives as). |
139 | | /// Everything else, `Internal` included, is final at once: an internal |
140 | | /// error is the scheduler's own, not the store's weather. |
141 | 21 | const fn is_transient_store_error(code: Code) -> bool { |
142 | 1 | matches!( |
143 | 21 | code, |
144 | | Code::Aborted |
145 | | | Code::Unavailable |
146 | | | Code::DeadlineExceeded |
147 | | | Code::ResourceExhausted |
148 | | | Code::Unknown |
149 | | ) |
150 | 21 | } |
151 | | |
152 | 20 | fn version_conflict_backoff(retry_count: usize) -> Duration { |
153 | 20 | let base_delay = BASE_RETRY_DELAY_MS * (1 << retry_count.saturating_sub(2).min(4)); |
154 | 20 | let jitter = SystemTime::now() |
155 | 20 | .duration_since(std::time::UNIX_EPOCH) |
156 | 20 | .map_or(0, |d| { |
157 | 20 | u64::try_from(d.as_nanos()).expect("u64 error") % MAX_RETRY_JITTER_MS |
158 | 20 | }); |
159 | 20 | Duration::from_millis(base_delay + jitter) |
160 | 20 | } |
161 | | |
162 | | /// Records the execution metrics for an action whose new state was just |
163 | | /// stored. `is_retry` is set when a failed attempt re-queued the action. |
164 | 21.9k | fn record_execution_metrics(awaited_action: &AwaitedAction, is_retry: bool) { |
165 | 21.9k | let action_state = awaited_action.state(); |
166 | 21.9k | let instance_name = awaited_action |
167 | 21.9k | .action_info() |
168 | 21.9k | .unique_qualifier |
169 | 21.9k | .instance_name() |
170 | 21.9k | .as_str(); |
171 | 21.9k | let worker_id = awaited_action |
172 | 21.9k | .worker_id() |
173 | 21.9k | .map(std::string::ToString::to_string); |
174 | 21.9k | let priority = Some(awaited_action.action_info().priority); |
175 | | |
176 | | // Build base attributes for metrics |
177 | 21.9k | let mut attrs = nativelink_util::metrics::make_execution_attributes( |
178 | 21.9k | instance_name, |
179 | 21.9k | worker_id.as_deref(), |
180 | 21.9k | priority, |
181 | | ); |
182 | | |
183 | | // Add stage attribute |
184 | 21.9k | let execution_stage: ExecutionStage = (&action_state.stage).into(); |
185 | 21.9k | attrs.push(KeyValue::new(EXECUTION_STAGE, execution_stage)); |
186 | | |
187 | | // Record stage transition |
188 | 21.9k | EXECUTION_METRICS.execution_stage_transitions.add(1, &attrs); |
189 | | |
190 | | // For completed actions, record the completion count with result |
191 | 21.9k | match &action_state.stage { |
192 | 10.1k | ActionStage::Completed(action_result) => { |
193 | 10.1k | let result = if action_result.exit_code == 0 { |
194 | 10.0k | ExecutionResult::Success |
195 | | } else { |
196 | 87 | ExecutionResult::Failure |
197 | | }; |
198 | 10.1k | attrs.push(KeyValue::new(EXECUTION_RESULT, result)); |
199 | 10.1k | EXECUTION_METRICS.execution_completed_count.add(1, &attrs); |
200 | 10.1k | nativelink_util::metrics::record_completed_execution_metrics( |
201 | 10.1k | action_result, |
202 | 10.1k | instance_name, |
203 | 10.1k | worker_id.as_deref(), |
204 | 10.1k | priority, |
205 | | ); |
206 | | } |
207 | 0 | ActionStage::CompletedFromCache(_) => { |
208 | 0 | attrs.push(KeyValue::new(EXECUTION_RESULT, ExecutionResult::CacheHit)); |
209 | 0 | EXECUTION_METRICS.execution_completed_count.add(1, &attrs); |
210 | 0 | } |
211 | 11.7k | _ => {} |
212 | | } |
213 | | |
214 | | // A failed attempt that re-queued the action counts as a retry. |
215 | 21.9k | if is_retry { |
216 | 734 | let retry_attrs = nativelink_util::metrics::make_execution_attributes( |
217 | 734 | instance_name, |
218 | 734 | worker_id.as_deref(), |
219 | 734 | priority, |
220 | 734 | ); |
221 | 734 | EXECUTION_METRICS.execution_retry_count.add(1, &retry_attrs); |
222 | 21.1k | } |
223 | 21.9k | } |
224 | | |
225 | | /// Whether an error reading a listed queue entry means the entry itself is |
226 | | /// lost: its record is gone from the store or cannot be decoded. Such an |
227 | | /// entry is skipped and counted. Any other error (the store unreachable, a |
228 | | /// listing page that failed to load) is about the store, not the entry, and |
229 | | /// is propagated so the caller retries instead of reporting a clean pass. |
230 | 14 | pub(crate) const fn is_lost_record(err: &Error) -> bool { |
231 | 14 | matches!6 (err.code, Code::NotFound | Code::InvalidArgument) |
232 | 14 | } |
233 | | |
234 | | /// Simple struct that implements the `ActionStateResult` trait and always returns an error. |
235 | | struct ErrorActionStateResult(Error); |
236 | | |
237 | | #[async_trait] |
238 | | impl ActionStateResult for ErrorActionStateResult { |
239 | 1 | async fn as_state(&self) -> Result<(Arc<ActionState>, Option<OriginMetadata>), Error> { |
240 | | Err(self.0.clone()) |
241 | 1 | } |
242 | | |
243 | 0 | async fn changed(&mut self) -> Result<(Arc<ActionState>, Option<OriginMetadata>), Error> { |
244 | | Err(self.0.clone()) |
245 | 0 | } |
246 | | |
247 | 1 | async fn as_action_info(&self) -> Result<(Arc<ActionInfo>, Option<OriginMetadata>), Error> { |
248 | | Err(self.0.clone()) |
249 | 1 | } |
250 | | } |
251 | | |
252 | | struct ClientActionStateResult<U, T, I, NowFn> |
253 | | where |
254 | | U: AwaitedActionSubscriber, |
255 | | T: AwaitedActionDb, |
256 | | I: InstantWrapper, |
257 | | NowFn: Fn() -> I + Clone + Send + Unpin + Sync + 'static, |
258 | | { |
259 | | inner: MatchingEngineActionStateResult<U, T, I, NowFn>, |
260 | | } |
261 | | |
262 | | impl<U, T, I, NowFn> ClientActionStateResult<U, T, I, NowFn> |
263 | | where |
264 | | U: AwaitedActionSubscriber, |
265 | | T: AwaitedActionDb, |
266 | | I: InstantWrapper, |
267 | | NowFn: Fn() -> I + Clone + Send + Unpin + Sync + 'static, |
268 | | { |
269 | 17.2k | const fn new( |
270 | 17.2k | sub: U, |
271 | 17.2k | simple_scheduler_state_manager: Weak<SimpleSchedulerStateManager<T, I, NowFn>>, |
272 | 17.2k | no_event_action_timeout: Duration, |
273 | 17.2k | now_fn: NowFn, |
274 | 17.2k | ) -> Self { |
275 | 17.2k | Self { |
276 | 17.2k | inner: MatchingEngineActionStateResult::new( |
277 | 17.2k | sub, |
278 | 17.2k | simple_scheduler_state_manager, |
279 | 17.2k | no_event_action_timeout, |
280 | 17.2k | now_fn, |
281 | 17.2k | ), |
282 | 17.2k | } |
283 | 17.2k | } |
284 | | } |
285 | | |
286 | | #[async_trait] |
287 | | impl<U, T, I, NowFn> ActionStateResult for ClientActionStateResult<U, T, I, NowFn> |
288 | | where |
289 | | U: AwaitedActionSubscriber, |
290 | | T: AwaitedActionDb, |
291 | | I: InstantWrapper, |
292 | | NowFn: Fn() -> I + Clone + Send + Unpin + Sync + 'static, |
293 | | { |
294 | 16.3k | async fn as_state(&self) -> Result<(Arc<ActionState>, Option<OriginMetadata>), Error> { |
295 | | self.inner.as_state().await |
296 | 16.3k | } |
297 | | |
298 | 47.5k | async fn changed(&mut self) -> Result<(Arc<ActionState>, Option<OriginMetadata>), Error> { |
299 | | self.inner.changed().await |
300 | 47.5k | } |
301 | | |
302 | 7 | async fn as_action_info(&self) -> Result<(Arc<ActionInfo>, Option<OriginMetadata>), Error> { |
303 | | self.inner.as_action_info().await |
304 | 7 | } |
305 | | |
306 | 4 | async fn client_last_seen(&self) -> Result<Option<SystemTime>, Error> { |
307 | | self.inner.client_last_seen().await |
308 | 4 | } |
309 | | } |
310 | | |
311 | | struct MatchingEngineActionStateResult<U, T, I, NowFn> |
312 | | where |
313 | | U: AwaitedActionSubscriber, |
314 | | T: AwaitedActionDb, |
315 | | I: InstantWrapper, |
316 | | NowFn: Fn() -> I + Clone + Send + Unpin + Sync + 'static, |
317 | | { |
318 | | awaited_action_sub: U, |
319 | | simple_scheduler_state_manager: Weak<SimpleSchedulerStateManager<T, I, NowFn>>, |
320 | | no_event_action_timeout: Duration, |
321 | | now_fn: NowFn, |
322 | | } |
323 | | impl<U, T, I, NowFn> MatchingEngineActionStateResult<U, T, I, NowFn> |
324 | | where |
325 | | U: AwaitedActionSubscriber, |
326 | | T: AwaitedActionDb, |
327 | | I: InstantWrapper, |
328 | | NowFn: Fn() -> I + Clone + Send + Unpin + Sync + 'static, |
329 | | { |
330 | 6.68M | const fn new( |
331 | 6.68M | awaited_action_sub: U, |
332 | 6.68M | simple_scheduler_state_manager: Weak<SimpleSchedulerStateManager<T, I, NowFn>>, |
333 | 6.68M | no_event_action_timeout: Duration, |
334 | 6.68M | now_fn: NowFn, |
335 | 6.68M | ) -> Self { |
336 | 6.68M | Self { |
337 | 6.68M | awaited_action_sub, |
338 | 6.68M | simple_scheduler_state_manager, |
339 | 6.68M | no_event_action_timeout, |
340 | 6.68M | now_fn, |
341 | 6.68M | } |
342 | 6.68M | } |
343 | | } |
344 | | |
345 | | #[async_trait] |
346 | | impl<U, T, I, NowFn> ActionStateResult for MatchingEngineActionStateResult<U, T, I, NowFn> |
347 | | where |
348 | | U: AwaitedActionSubscriber, |
349 | | T: AwaitedActionDb, |
350 | | I: InstantWrapper, |
351 | | NowFn: Fn() -> I + Clone + Send + Unpin + Sync + 'static, |
352 | | { |
353 | 27.3k | async fn as_state(&self) -> Result<(Arc<ActionState>, Option<OriginMetadata>), Error> { |
354 | | let awaited_action = self |
355 | | .awaited_action_sub |
356 | | .borrow() |
357 | | .await |
358 | | .err_tip(|| "In MatchingEngineActionStateResult::as_state")?; |
359 | | Ok(( |
360 | | awaited_action.state().clone(), |
361 | | awaited_action.maybe_origin_metadata().cloned(), |
362 | | )) |
363 | 27.3k | } |
364 | | |
365 | 4 | async fn client_last_seen(&self) -> Result<Option<SystemTime>, Error> { |
366 | | let awaited_action = self |
367 | | .awaited_action_sub |
368 | | .borrow() |
369 | | .await |
370 | | .err_tip(|| "In MatchingEngineActionStateResult::client_last_seen")?; |
371 | | Ok(Some(awaited_action.last_client_keepalive_timestamp())) |
372 | 4 | } |
373 | | |
374 | 6.66M | async fn dispatch_facts(&self) -> Result<DispatchFacts, Error> { |
375 | | let awaited_action = self |
376 | | .awaited_action_sub |
377 | | .borrow() |
378 | | .await |
379 | | .err_tip(|| "In MatchingEngineActionStateResult::dispatch_facts")?; |
380 | | Ok(DispatchFacts { |
381 | | avoid_worker_ids: awaited_action.avoid_worker_ids().to_vec(), |
382 | | avoid_node_names: awaited_action.avoid_node_names().to_vec(), |
383 | | queued_since: Some(awaited_action.state().last_transition_timestamp), |
384 | | }) |
385 | 6.66M | } |
386 | | |
387 | 47.5k | async fn changed(&mut self) -> Result<(Arc<ActionState>, Option<OriginMetadata>), Error> { |
388 | | let mut timeout_attempts = 0; |
389 | | loop { |
390 | | tokio::select! { |
391 | | awaited_action_result = self.awaited_action_sub.changed() => { |
392 | | return awaited_action_result |
393 | | .err_tip(|| "In MatchingEngineActionStateResult::changed") |
394 | 41.1k | .map(|v| (v.state().clone(), v.maybe_origin_metadata().cloned())); |
395 | | } |
396 | | () = (self.now_fn)().sleep(self.no_event_action_timeout) => { |
397 | | // Timeout happened, do additional checks below. |
398 | | } |
399 | | } |
400 | | |
401 | | let awaited_action = self |
402 | | .awaited_action_sub |
403 | | .borrow() |
404 | | .await |
405 | | .err_tip(|| "In MatchingEngineActionStateResult::changed")?; |
406 | | |
407 | | if matches!(awaited_action.state().stage, ActionStage::Queued) { |
408 | | // Actions in queued state do not get periodically updated, |
409 | | // so we don't need to timeout them. |
410 | | continue; |
411 | | } |
412 | | |
413 | | let simple_scheduler_state_manager = self |
414 | | .simple_scheduler_state_manager |
415 | | .upgrade() |
416 | 0 | .err_tip(|| format!("Failed to upgrade weak reference to SimpleSchedulerStateManager in MatchingEngineActionStateResult::changed at attempt: {timeout_attempts}"))?; |
417 | | |
418 | | // Check if worker is alive via registry before timing out. |
419 | | let Some(cause) = simple_scheduler_state_manager |
420 | | .timeout_cause(&awaited_action) |
421 | | .await |
422 | | else { |
423 | | // Worker is alive, continue waiting for updates |
424 | | trace!( |
425 | | operation_id = %awaited_action.operation_id(), |
426 | | "Operation timeout check passed, worker is alive" |
427 | | ); |
428 | | continue; |
429 | | }; |
430 | | |
431 | | warn!( |
432 | | ?awaited_action, |
433 | | "OperationId {} / {} {cause}, issuing a retry", |
434 | | awaited_action.operation_id(), |
435 | | awaited_action.state().client_operation_id, |
436 | | ); |
437 | | |
438 | | simple_scheduler_state_manager |
439 | | .timeout_operation_id(awaited_action.operation_id()) |
440 | | .await |
441 | | .err_tip(|| "In MatchingEngineActionStateResult::changed")?; |
442 | | |
443 | | if timeout_attempts >= MAX_UPDATE_RETRIES { |
444 | | return Err(make_err!( |
445 | | Code::Internal, |
446 | | "Failed to update action after {} retries with no error set in MatchingEngineActionStateResult::changed - {} {:?}", |
447 | | MAX_UPDATE_RETRIES, |
448 | | awaited_action.operation_id(), |
449 | | awaited_action.state().stage, |
450 | | )); |
451 | | } |
452 | | timeout_attempts += 1; |
453 | | } |
454 | 47.5k | } |
455 | | |
456 | 6.66M | async fn as_action_info(&self) -> Result<(Arc<ActionInfo>, Option<OriginMetadata>), Error> { |
457 | | let awaited_action = self |
458 | | .awaited_action_sub |
459 | | .borrow() |
460 | | .await |
461 | | .err_tip(|| "In MatchingEngineActionStateResult::as_action_info")?; |
462 | | Ok(( |
463 | | awaited_action.action_info().clone(), |
464 | | awaited_action.maybe_origin_metadata().cloned(), |
465 | | )) |
466 | 6.66M | } |
467 | | } |
468 | | |
469 | | /// How often an action is queued again, by cause, before the scheduler |
470 | | /// completes it with `FailedPrecondition`. Each cause has its own budget: |
471 | | /// an action's own failures, the workers it lost through no fault of its |
472 | | /// own, and the memory escalations it was granted. |
473 | | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
474 | | pub struct RetryLimits { |
475 | | /// Internal errors and timeouts the action produced. |
476 | | pub max_job_retries: usize, |
477 | | /// Workers lost while the action ran. |
478 | | pub max_worker_loss_retries: usize, |
479 | | /// Memory escalations; 0 leaves only the fleet's ceiling to bound them. |
480 | | pub max_escalations: usize, |
481 | | /// The worker loss from which a lost worker escalates the action's |
482 | | /// memory (RFC-0018); 0 never does. |
483 | | pub escalate_after_worker_losses: usize, |
484 | | /// Command timeouts queued again for another worker; 0 keeps the |
485 | | /// first timeout final. |
486 | | pub max_timeout_retries: usize, |
487 | | } |
488 | | |
489 | | impl RetryLimits { |
490 | | /// Lost workers allowed by default; the action did nothing wrong. |
491 | | pub const DEFAULT_WORKER_LOSS_RETRIES: usize = 10; |
492 | | /// Command timeouts tried again elsewhere by default: one second |
493 | | /// opinion, since a worker gone slow times out whatever it is given. |
494 | | pub const DEFAULT_TIMEOUT_RETRIES: usize = 1; |
495 | | } |
496 | | |
497 | | /// Only the job budget stated: the other two take their defaults, which is |
498 | | /// what older callers meant. |
499 | | impl From<usize> for RetryLimits { |
500 | 0 | fn from(max_job_retries: usize) -> Self { |
501 | 0 | Self { |
502 | 0 | max_job_retries, |
503 | 0 | max_worker_loss_retries: Self::DEFAULT_WORKER_LOSS_RETRIES, |
504 | 0 | max_escalations: 0, |
505 | 0 | escalate_after_worker_losses: 0, |
506 | 0 | max_timeout_retries: Self::DEFAULT_TIMEOUT_RETRIES, |
507 | 0 | } |
508 | 0 | } |
509 | | } |
510 | | |
511 | | /// `SimpleSchedulerStateManager` is responsible for maintaining the state of the scheduler. |
512 | | /// Scheduler state includes the actions that are queued, active, and recently completed. |
513 | | /// It also includes the workers that are available to execute actions based on allocation |
514 | | /// strategy. |
515 | | |
516 | | #[derive(MetricsComponent, Debug)] |
517 | | pub struct SimpleSchedulerStateManager<T, I, NowFn> |
518 | | where |
519 | | T: AwaitedActionDb, |
520 | | I: InstantWrapper, |
521 | | NowFn: Fn() -> I + Clone + Send + Unpin + Sync + 'static, |
522 | | { |
523 | | /// Database for storing the state of all actions. |
524 | | #[metric(group = "action_db")] |
525 | | action_db: T, |
526 | | |
527 | | /// Maximum number of times a job can be retried. |
528 | | // TODO(palfrey) This should be a scheduler decorator instead |
529 | | // of always having it on every SimpleScheduler. |
530 | | #[metric(help = "Maximum number of times a job can be retried")] |
531 | | max_job_retries: usize, |
532 | | |
533 | | #[metric(help = "Maximum number of times a job is queued again after losing its worker")] |
534 | | max_worker_loss_retries: usize, |
535 | | |
536 | | #[metric( |
537 | | help = "Maximum number of memory escalations for a job; 0 leaves the ceiling to bound them" |
538 | | )] |
539 | | max_escalations: usize, |
540 | | |
541 | | #[metric( |
542 | | help = "Worker losses before a requeue also raises the memory reservation; 0 never does" |
543 | | )] |
544 | | escalate_after_worker_losses: usize, |
545 | | |
546 | | #[metric( |
547 | | help = "Maximum number of times a job is queued again for another worker after its command timed out" |
548 | | )] |
549 | | max_timeout_retries: usize, |
550 | | |
551 | | /// Duration after which an action is considered to be timed out if |
552 | | /// no event is received. |
553 | | #[metric( |
554 | | help = "Duration after which an action is considered to be timed out if no event is received" |
555 | | )] |
556 | | no_event_action_timeout: Duration, |
557 | | |
558 | | /// Mark operation as timed out if the worker has not updated in this duration. |
559 | | /// This is used to prevent operations from being stuck in the queue forever |
560 | | /// if it is not being processed by any worker. |
561 | | client_action_timeout: Duration, |
562 | | |
563 | | /// Maximum time an action can stay in Executing state without any worker |
564 | | /// update, regardless of worker keepalive status. `Duration::ZERO` disables. |
565 | | max_executing_timeout: Duration, |
566 | | |
567 | | // A lock to ensure only one timeout operation is running at a time |
568 | | // on this service. |
569 | | timeout_operation_mux: Mutex<()>, |
570 | | |
571 | | /// Weak reference to self. |
572 | | // We use a weak reference to reduce the risk of a memory leak from |
573 | | // future changes. If this becomes some kind of performance issue, |
574 | | // we can consider using a strong reference. |
575 | | weak_self: Weak<Self>, |
576 | | |
577 | | /// Function to get the current time. |
578 | | now_fn: NowFn, |
579 | | |
580 | | /// Worker registry for checking worker liveness. |
581 | | worker_registry: Option<SharedWorkerRegistry>, |
582 | | } |
583 | | |
584 | | impl<T, I, NowFn> SimpleSchedulerStateManager<T, I, NowFn> |
585 | | where |
586 | | T: AwaitedActionDb, |
587 | | I: InstantWrapper, |
588 | | NowFn: Fn() -> I + Clone + Send + Unpin + Sync + 'static, |
589 | | { |
590 | 379 | pub fn new( |
591 | 379 | retry_limits: impl Into<RetryLimits>, |
592 | 379 | no_event_action_timeout: Duration, |
593 | 379 | client_action_timeout: Duration, |
594 | 379 | max_executing_timeout: Duration, |
595 | 379 | action_db: T, |
596 | 379 | now_fn: NowFn, |
597 | 379 | worker_registry: Option<SharedWorkerRegistry>, |
598 | 379 | ) -> Arc<Self> { |
599 | | let RetryLimits { |
600 | 379 | max_job_retries, |
601 | 379 | max_worker_loss_retries, |
602 | 379 | max_escalations, |
603 | 379 | escalate_after_worker_losses, |
604 | 379 | max_timeout_retries, |
605 | 379 | } = retry_limits.into(); |
606 | 379 | Arc::new_cyclic(|weak_self| Self { |
607 | 379 | action_db, |
608 | 379 | max_job_retries, |
609 | 379 | max_worker_loss_retries, |
610 | 379 | max_escalations, |
611 | 379 | escalate_after_worker_losses, |
612 | 379 | max_timeout_retries, |
613 | 379 | no_event_action_timeout, |
614 | 379 | client_action_timeout, |
615 | 379 | max_executing_timeout, |
616 | 379 | timeout_operation_mux: Mutex::new(()), |
617 | 379 | weak_self: weak_self.clone(), |
618 | 379 | now_fn, |
619 | 379 | worker_registry, |
620 | 379 | }) |
621 | 379 | } |
622 | | |
623 | | /// Retires queued actions whose client has stopped listening. |
624 | | /// |
625 | | /// A queued action has no worker, so neither of the other two timeout |
626 | | /// paths can reach it. `MatchingEngineActionStateResult::changed` skips |
627 | | /// the `Queued` stage outright, and `should_timeout_operation` only |
628 | | /// considers `Executing`. That leaves the matching pass as the only |
629 | | /// thing that retires them, and it only does so for actions it happens |
630 | | /// to visit. |
631 | | /// |
632 | | /// When it does not, they accumulate with no expiry and are handed to |
633 | | /// the matcher again on every pass. Every pass reads the whole queued |
634 | | /// set, so enough of them make each read slow and each pass cost more |
635 | | /// than it places: workers sit idle, nothing executes or completes, and |
636 | | /// clients eventually report a remote execution failure. |
637 | | /// |
638 | | /// Returns how many were retired. |
639 | | /// Retires executing actions that nothing else will reach. |
640 | | /// |
641 | | /// The timeout check in `MatchingEngineActionStateResult::changed` runs |
642 | | /// only while a client is waiting on the action, once per |
643 | | /// `no_event_action_timeout`. An action whose client has gone, or whose |
644 | | /// result the worker sent but the store did not take, stays `Executing` |
645 | | /// with its slot's worth of fleet capacity accounted to it until |
646 | | /// something lists it. This does, on the scheduler's own clock: |
647 | | /// |
648 | | /// - an action `timeout_cause` says is overdue is retired through |
649 | | /// `timeout_operation_id`, which re-checks under its lock; |
650 | | /// - an action whose worker `running_on_worker` knows and reports as no |
651 | | /// longer running it has lost its result. It goes back to the queue |
652 | | /// untried once that has been so for `lost_grace`, which covers the |
653 | | /// moment between the store write of an assignment and the dispatch |
654 | | /// that follows it. `lost_since` carries the first sighting between |
655 | | /// sweeps and is pruned of actions no longer in that state. |
656 | | /// |
657 | | /// `running_on_worker` answers `None` for a worker this scheduler is not |
658 | | /// connected to: a peer's worker, or one already gone, which the |
659 | | /// timeout path handles. A record the listing names but cannot read is |
660 | | /// skipped, so one bad entry does not stop the sweep. |
661 | 8 | pub async fn sweep_executing_actions( |
662 | 8 | &self, |
663 | 8 | running_on_worker: &( |
664 | 8 | dyn Fn(WorkerId, OperationId) -> BoxFuture<'static, Option<bool>> + Send + Sync |
665 | 8 | ), |
666 | 8 | lost_since: &mut HashMap<OperationId, SystemTime>, |
667 | 8 | lost_grace: Duration, |
668 | 8 | ) -> Result<ExecutingSweepReport, Error> { |
669 | 8 | let now = (self.now_fn)().now(); |
670 | 8 | let stream = self |
671 | 8 | .action_db |
672 | 8 | .get_range_of_actions( |
673 | 8 | SortedAwaitedActionState::Executing, |
674 | 8 | Bound::Unbounded, |
675 | 8 | Bound::Unbounded, |
676 | 8 | true, |
677 | 8 | ) |
678 | 8 | .await |
679 | 8 | .err_tip(|| "In sweep_executing_actions")?0 ; |
680 | 8 | tokio::pin!(stream); |
681 | | |
682 | 8 | let mut report = ExecutingSweepReport::default(); |
683 | 8 | let mut still_lost = HashMap::new(); |
684 | 16 | while let Some(subscriber8 ) = stream.next().await { |
685 | 8 | let awaited_action = match subscriber { |
686 | 8 | Ok(subscriber) => subscriber.borrow().await, |
687 | 0 | Err(err) => Err(err), |
688 | | }; |
689 | 8 | let awaited_action = match awaited_action { |
690 | 8 | Ok(awaited_action) => awaited_action, |
691 | 0 | Err(err) => { |
692 | 0 | if !is_lost_record(&err) { |
693 | 0 | return Err(err).err_tip(|| "In sweep_executing_actions"); |
694 | 0 | } |
695 | 0 | warn!( |
696 | | ?err, |
697 | | "Executing operation listed but its record cannot be read; skipping" |
698 | | ); |
699 | 0 | record_awaited_action_orphan("executing_sweep"); |
700 | 0 | continue; |
701 | | } |
702 | | }; |
703 | 8 | if !matches!0 (awaited_action.state().stage, ActionStage::Executing) { |
704 | 0 | continue; |
705 | 8 | } |
706 | 8 | report |
707 | 8 | .ages |
708 | 8 | .observe_since(awaited_action.state().last_transition_timestamp, now); |
709 | 8 | let operation_id = awaited_action.operation_id().clone(); |
710 | | |
711 | 8 | if let Some(cause1 ) = self.timeout_cause(&awaited_action).await { |
712 | 1 | match self.timeout_operation_id(&operation_id).await { |
713 | 0 | Ok(false) => {} |
714 | | Ok(true) => { |
715 | 1 | report.retired += 1; |
716 | 1 | if let Some(worker_id) = awaited_action.worker_id() { |
717 | 1 | report.ended.push(SweptAttempt { |
718 | 1 | operation_id: operation_id.clone(), |
719 | 1 | worker_id: worker_id.clone(), |
720 | 1 | reason: AttemptEndReason::Timeout, |
721 | 1 | detail: format!("Operation {cause}"), |
722 | 1 | }); |
723 | 1 | }0 |
724 | | } |
725 | | // A conflicting write means someone else moved it, which |
726 | | // is what we wanted; anything else is logged and the |
727 | | // sweep goes on to the rest. |
728 | 0 | Err(err) if err.code == Code::Aborted => {} |
729 | 0 | Err(err) => { |
730 | 0 | warn!(%operation_id, ?err, "Could not time out an executing operation"); |
731 | 0 | report.failed += 1; |
732 | | } |
733 | | } |
734 | 1 | continue; |
735 | 7 | } |
736 | | |
737 | 7 | let Some(worker_id) = awaited_action.worker_id() else { |
738 | 0 | continue; |
739 | | }; |
740 | | // Freshly assigned: the dispatch that puts it on the worker may |
741 | | // not have happened yet. |
742 | 7 | if now |
743 | 7 | .duration_since(awaited_action.state().last_transition_timestamp) |
744 | 7 | .is_ok_and(|since| since < lost_grace) |
745 | | { |
746 | 1 | continue; |
747 | 6 | } |
748 | 6 | if running_on_worker(worker_id.clone(), operation_id.clone()).await == Some(false) { |
749 | 4 | let first_seen = *lost_since.get(&operation_id).unwrap_or(&now); |
750 | 4 | if now |
751 | 4 | .duration_since(first_seen) |
752 | 4 | .is_ok_and(|lost| lost >= lost_grace) |
753 | | { |
754 | 2 | warn!( |
755 | | %operation_id, |
756 | | ?worker_id, |
757 | 2 | lost_for_s = lost_grace.as_secs(), |
758 | | "Executing operation is not running on its worker; its result was lost, requeuing" |
759 | | ); |
760 | | // Through the error path, so the loss spends one of the |
761 | | // action's `max_job_retries` and a result the store keeps |
762 | | // refusing ends in a failure the client sees, not a loop. |
763 | | // This is a lost execution result, not a worker-reported |
764 | | // Unavailable store read that uses the store-loss budget. |
765 | 2 | match self |
766 | 2 | .inner_update_operation( |
767 | 2 | &operation_id, |
768 | 2 | Some(worker_id), |
769 | 2 | None, |
770 | 2 | UpdateOperationType::UpdateWithError(make_err!( |
771 | 2 | Code::Internal, |
772 | 2 | "{LOST_RESULT_ERROR_TIP}" |
773 | 2 | )), |
774 | | ) |
775 | 2 | .await |
776 | | { |
777 | 2 | Ok(()) => { |
778 | 2 | record_dispatch_requeue("lost_result"); |
779 | 2 | report.requeued += 1; |
780 | 2 | report.ended.push(SweptAttempt { |
781 | 2 | operation_id: operation_id.clone(), |
782 | 2 | worker_id: worker_id.clone(), |
783 | 2 | reason: AttemptEndReason::LostResult, |
784 | 2 | detail: String::new(), |
785 | 2 | }); |
786 | 2 | } |
787 | 0 | Err(err) if err.code == Code::Aborted => {} |
788 | 0 | Err(err) => { |
789 | 0 | warn!(%operation_id, ?err, "Could not requeue an operation whose result was lost"); |
790 | 0 | report.failed += 1; |
791 | 0 | still_lost.insert(operation_id.clone(), first_seen); |
792 | | } |
793 | | } |
794 | 2 | } else { |
795 | 2 | still_lost.insert(operation_id.clone(), first_seen); |
796 | 2 | } |
797 | 2 | } |
798 | | } |
799 | 8 | *lost_since = still_lost; |
800 | 8 | Ok(report) |
801 | 8 | } |
802 | | |
803 | 118 | pub async fn sweep_abandoned_queued_actions(&self) -> Result<u64, Error> { |
804 | 118 | let now = (self.now_fn)().now(); |
805 | 118 | let stream = self |
806 | 118 | .action_db |
807 | 118 | .get_range_of_actions( |
808 | 118 | SortedAwaitedActionState::Queued, |
809 | 118 | Bound::Unbounded, |
810 | 118 | Bound::Unbounded, |
811 | 118 | true, |
812 | 118 | ) |
813 | 118 | .await |
814 | 118 | .err_tip(|| "In sweep_abandoned_queued_actions")?0 ; |
815 | 118 | tokio::pin!(stream); |
816 | | |
817 | 118 | let mut retired = 0u64; |
818 | 4.22k | while let Some(subscriber4.11k ) = stream.next().await { |
819 | | // The queue listed it but the record is gone (evicted) or cannot |
820 | | // be decoded. One such entry must not stop the sweep from |
821 | | // retiring everything behind it, which is how a pile of |
822 | | // abandoned actions becomes permanent. Anything else, such as |
823 | | // the listing's next page failing to load, is a failed sweep: |
824 | | // the entries it did not reach are still there. |
825 | 4.11k | let awaited_action4.11k = match subscriber { |
826 | 4.11k | Ok(subscriber) => subscriber.borrow().await, |
827 | 2 | Err(err) => Err(err), |
828 | | }; |
829 | 4.10k | let awaited_action = match awaited_action3 { |
830 | 4.10k | Ok(awaited_action) => awaited_action, |
831 | 3 | Err(err2 ) if is_lost_record(&err)2 => { |
832 | 2 | warn!( |
833 | | ?err, |
834 | | "Queued operation listed but its record cannot be read; skipping" |
835 | | ); |
836 | 2 | record_awaited_action_orphan("sweep"); |
837 | 2 | continue; |
838 | | } |
839 | 1 | Err(err) => return Err(err).err_tip(|| "In sweep_abandoned_queued_actions"), |
840 | | }; |
841 | | |
842 | | // Only queued actions belong to this sweep, and only once the |
843 | | // client has been gone longer than it is allowed to be. |
844 | 4.10k | if !matches!0 (awaited_action.state().stage, ActionStage::Queued) { |
845 | 0 | continue; |
846 | 4.10k | } |
847 | 4.10k | if awaited_action.last_client_keepalive_timestamp() + self.client_action_timeout >= now |
848 | | { |
849 | 4.09k | continue; |
850 | 11 | } |
851 | | |
852 | 11 | let mut state = awaited_action.state().as_ref().clone(); |
853 | 11 | state.stage = ActionStage::Completed(ActionResult { |
854 | 11 | error: Some(make_err!( |
855 | 11 | Code::DeadlineExceeded, |
856 | 11 | "Operation timed out {} seconds of having no more clients listening", |
857 | 11 | self.client_action_timeout.as_secs_f32(), |
858 | 11 | )), |
859 | 11 | ..ActionResult::default() |
860 | 11 | }); |
861 | 11 | state.last_transition_timestamp = now; |
862 | | |
863 | 11 | let mut new_awaited_action = awaited_action; |
864 | 11 | new_awaited_action.worker_set_state(Arc::new(state), now); |
865 | | // A conflict means something else is already changing this |
866 | | // action, which is the outcome this sweep wants anyway. Leave it |
867 | | // for the next pass rather than fighting for it. |
868 | 11 | match self |
869 | 11 | .action_db |
870 | 11 | .update_awaited_action(new_awaited_action) |
871 | 11 | .await |
872 | | { |
873 | 11 | Ok(()) => retired += 1, |
874 | 0 | Err(err) if err.code == Code::Aborted => {} |
875 | 0 | Err(err) => { |
876 | 0 | return Err(err).err_tip(|| "In sweep_abandoned_queued_actions"); |
877 | | } |
878 | | } |
879 | | } |
880 | | |
881 | 116 | if retired > 0 { |
882 | 5 | warn!( |
883 | | retired, |
884 | 3 | timeout_secs = self.client_action_timeout.as_secs_f32(), |
885 | | "Retired queued operations that had no clients listening" |
886 | | ); |
887 | 111 | } |
888 | 116 | record_queue_retired(retired); |
889 | 116 | Ok(retired) |
890 | 117 | } |
891 | | |
892 | | /// See `AwaitedActionDb::exchange_fleet_capabilities`. |
893 | 1.30k | pub async fn exchange_fleet_capabilities( |
894 | 1.30k | &self, |
895 | 1.30k | scheduler_id: &str, |
896 | 1.30k | local: Vec<PlatformProperties>, |
897 | 1.30k | ttl: Duration, |
898 | 1.30k | ) -> Result<Vec<PeerScheduler>, Error> { |
899 | 1.30k | self.action_db |
900 | 1.30k | .exchange_fleet_capabilities(scheduler_id, local, ttl) |
901 | 1.30k | .await |
902 | 1.30k | } |
903 | | |
904 | 34 | async fn inner_fail_queued_operation( |
905 | 34 | &self, |
906 | 34 | operation_id: &OperationId, |
907 | 34 | err: Error, |
908 | 34 | ) -> Result<bool, Error> { |
909 | 34 | let Some(subscriber33 ) = self |
910 | 34 | .action_db |
911 | 34 | .get_by_operation_id(operation_id) |
912 | 34 | .await |
913 | 34 | .err_tip(|| "In SimpleSchedulerStateManager::fail_queued_operation")?0 |
914 | | else { |
915 | 1 | return Ok(false); |
916 | | }; |
917 | | // The action may be racing an assignment or a client update, so on |
918 | | // a version conflict reload it and, if it is still queued, try again. |
919 | 33 | let mut last_err = None; |
920 | 33 | for attempt in 1..=MAX_UPDATE_RETRIES { |
921 | 33 | if attempt > 1 { |
922 | 0 | tokio::time::sleep(version_conflict_backoff(attempt)).await; |
923 | 33 | } |
924 | 33 | let awaited_action = match subscriber.borrow().await { |
925 | 33 | Ok(awaited_action) => awaited_action, |
926 | | // Store-backed dbs hand out a subscriber for any id and only |
927 | | // discover the operation is gone on read. |
928 | 0 | Err(err) if err.code == Code::NotFound => return Ok(false), |
929 | 0 | Err(err) => { |
930 | 0 | return Err(err) |
931 | 0 | .err_tip(|| "In SimpleSchedulerStateManager::fail_queued_operation"); |
932 | | } |
933 | | }; |
934 | 33 | if !matches!2 (awaited_action.state().stage, ActionStage::Queued) { |
935 | 2 | return Ok(false); |
936 | 31 | } |
937 | | |
938 | 31 | let now = (self.now_fn)().now(); |
939 | 31 | let mut state = awaited_action.state().as_ref().clone(); |
940 | 31 | state.stage = ActionStage::Completed(ActionResult { |
941 | 31 | error: Some(err.clone()), |
942 | 31 | ..ActionResult::default() |
943 | 31 | }); |
944 | 31 | state.last_transition_timestamp = now; |
945 | | |
946 | 31 | let mut new_awaited_action = awaited_action; |
947 | 31 | new_awaited_action.worker_set_state(Arc::new(state), now); |
948 | 31 | match self |
949 | 31 | .action_db |
950 | 31 | .update_awaited_action(new_awaited_action.clone()) |
951 | 31 | .await |
952 | | { |
953 | | Ok(()) => { |
954 | 31 | info!(%operation_id, ?err, "Failed a queued action no worker can run"); |
955 | 31 | record_execution_metrics(&new_awaited_action, false); |
956 | 31 | return Ok(true); |
957 | | } |
958 | 0 | Err(err) if err.code == Code::Aborted => last_err = Some(err), |
959 | 0 | Err(err) => { |
960 | 0 | return Err(err) |
961 | 0 | .err_tip(|| "In SimpleSchedulerStateManager::fail_queued_operation"); |
962 | | } |
963 | | } |
964 | | } |
965 | 0 | warn!( |
966 | | %operation_id, |
967 | | ?last_err, |
968 | | "Could not fail an unsatisfiable action after repeated version conflicts, will try again on the next matching pass" |
969 | | ); |
970 | 0 | Ok(false) |
971 | 34 | } |
972 | | |
973 | 13 | pub async fn should_timeout_operation(&self, awaited_action: &AwaitedAction) -> bool { |
974 | 13 | self.timeout_cause(awaited_action).await.is_some() |
975 | 13 | } |
976 | | |
977 | | /// Why the scheduler should time out an executing action, or `None` if |
978 | | /// it should not. |
979 | 2.73k | pub async fn timeout_cause(&self, awaited_action: &AwaitedAction) -> Option<TimeoutCause> { |
980 | 2.73k | if !matches!76 (awaited_action.state().stage, ActionStage::Executing) { |
981 | 76 | return None; |
982 | 2.65k | } |
983 | | |
984 | 2.65k | let now = (self.now_fn)().now(); |
985 | | |
986 | | // Honor the per-action `Action.timeout` from the RBE protocol as a |
987 | | // backend wall-clock deadline, in every liveness case below. The |
988 | | // worker enforces the same timeout, but its clock starts when the |
989 | | // command starts, after the inputs are fetched, which the scheduler |
990 | | // cannot observe. Enforcing it at `Action.timeout` from assignment |
991 | | // would pre-empt every live worker's own DEADLINE_EXCEEDED result, so |
992 | | // allow `no_event_action_timeout` more, measured from the later of the |
993 | | // assignment and the worker's last update: a live worker then reports |
994 | | // first, and a wedged one (hung input fetch, unkillable child, broken |
995 | | // external timeout, orphan) is still ended here. |
996 | 2.65k | let action_timeout = awaited_action.action_info().timeout; |
997 | 2.65k | if action_timeout > Duration::ZERO { |
998 | 2.63k | let anchor = awaited_action |
999 | 2.63k | .state() |
1000 | 2.63k | .last_transition_timestamp |
1001 | 2.63k | .max(awaited_action.last_worker_updated_timestamp()); |
1002 | 2.63k | if let Ok(elapsed) = now.duration_since(anchor) |
1003 | 2.63k | && elapsed > action_timeout.saturating_add(self.no_event_action_timeout) |
1004 | | { |
1005 | 5 | return Some(TimeoutCause::ActionTimeout { |
1006 | 5 | timeout: action_timeout, |
1007 | 5 | grace: self.no_event_action_timeout, |
1008 | 5 | }); |
1009 | 2.63k | } |
1010 | 16 | } |
1011 | | |
1012 | 2.65k | let liveness = match (&self.worker_registry, awaited_action.worker_id()) { |
1013 | 2.64k | (Some(worker_registry), Some(worker_id)) => { |
1014 | 2.64k | worker_registry |
1015 | 2.64k | .check_liveness(worker_id, self.no_event_action_timeout, now) |
1016 | 2.64k | .await |
1017 | | } |
1018 | | // No registry, or not assigned yet: fall back to the |
1019 | | // timestamp-only check. |
1020 | 2 | _ => WorkerLiveness::Stale, |
1021 | | }; |
1022 | | |
1023 | 2.65k | let last_update = awaited_action.last_worker_updated_timestamp(); |
1024 | 2.65k | let silent_for = |ceiling: Duration| {249 |
1025 | 249 | now.duration_since(last_update) |
1026 | 249 | .is_ok_and(|elapsed| elapsed > ceiling) |
1027 | 249 | .then_some(TimeoutCause::NoWorkerUpdate(ceiling)) |
1028 | 249 | }; |
1029 | | |
1030 | 2.65k | match liveness { |
1031 | | // Ours and heartbeating: besides Action.timeout above, only the |
1032 | | // stuck-but-alive ceiling applies, and disabling that leaves an |
1033 | | // action with no Action.timeout no ceiling on a live worker. |
1034 | | WorkerLiveness::Alive => { |
1035 | 2.41k | if self.max_executing_timeout > Duration::ZERO { |
1036 | 10 | silent_for(self.max_executing_timeout) |
1037 | | } else { |
1038 | 2.40k | None |
1039 | | } |
1040 | | } |
1041 | | |
1042 | | // Usually a peer's healthy worker, so worker_timeout_s must not |
1043 | | // apply. It can also be an orphan no instance will ever reap, and |
1044 | | // max_action_executing_timeout_s defaults to disabled, so fall |
1045 | | // back to a ceiling rather than never timing out. |
1046 | | WorkerLiveness::Unknown => { |
1047 | 216 | if self.max_executing_timeout > Duration::ZERO { |
1048 | 3 | silent_for(self.max_executing_timeout) |
1049 | | } else { |
1050 | 213 | silent_for(ORPHANED_ACTION_TIMEOUT) |
1051 | | } |
1052 | | } |
1053 | | |
1054 | | // Registered here and gone quiet: ours, and it looks dead. |
1055 | 23 | WorkerLiveness::Stale => silent_for(self.no_event_action_timeout), |
1056 | | } |
1057 | 2.73k | } |
1058 | | |
1059 | 6.67M | async fn apply_filter_predicate( |
1060 | 6.67M | &self, |
1061 | 6.67M | awaited_action: &AwaitedAction, |
1062 | 6.67M | subscriber: &T::Subscriber, |
1063 | 6.67M | filter: &OperationFilter, |
1064 | 6.67M | ) -> bool { |
1065 | | // Note: The caller must filter `client_operation_id`. |
1066 | | |
1067 | 6.67M | let mut maybe_reloaded_awaited_action: Option<AwaitedAction> = None; |
1068 | 6.67M | let now = (self.now_fn)().now(); |
1069 | | |
1070 | | // Check if client has timed out |
1071 | 6.67M | if awaited_action.last_client_keepalive_timestamp() + self.client_action_timeout < now { |
1072 | | // This may change if the version is out of date. |
1073 | 2.49k | let mut timed_out = true; |
1074 | 2.49k | if !awaited_action.state().stage.is_finished() { |
1075 | 2.49k | let mut state = awaited_action.state().as_ref().clone(); |
1076 | 2.49k | warn!(operation_id = ?awaited_action7 .operation_id7 (), timeout_secs = self.client_action_timeout7 .as_secs_f327 (), "Operation timed out having no more clients listening"); |
1077 | 2.49k | state.stage = ActionStage::Completed(ActionResult { |
1078 | 2.49k | error: Some(make_err!( |
1079 | 2.49k | Code::DeadlineExceeded, |
1080 | 2.49k | "Operation timed out {} seconds of having no more clients listening", |
1081 | 2.49k | self.client_action_timeout.as_secs_f32(), |
1082 | 2.49k | )), |
1083 | 2.49k | ..ActionResult::default() |
1084 | 2.49k | }); |
1085 | 2.49k | state.last_transition_timestamp = now; |
1086 | 2.49k | let state = Arc::new(state); |
1087 | | // We may be competing with an client timestamp update, so try |
1088 | | // this a few times. |
1089 | 2.49k | for attempt in 1..=MAX_UPDATE_RETRIES2.49k { |
1090 | 2.49k | let mut new_awaited_action = match &maybe_reloaded_awaited_action { |
1091 | 2.49k | None => awaited_action.clone(), |
1092 | 4 | Some(reloaded_awaited_action) => reloaded_awaited_action.clone(), |
1093 | | }; |
1094 | 2.49k | new_awaited_action.worker_set_state(state.clone(), (self.now_fn)().now()); |
1095 | 2.49k | let err5 = match self |
1096 | 2.49k | .action_db |
1097 | 2.49k | .update_awaited_action(new_awaited_action) |
1098 | 2.49k | .await |
1099 | | { |
1100 | 2.49k | Ok(()) => break, |
1101 | 5 | Err(err) => err, |
1102 | | }; |
1103 | | // Reload from the database if the action was outdated. |
1104 | 5 | let maybe_awaited_action = |
1105 | 5 | if attempt == MAX_UPDATE_RETRIES || err.code != Code::Aborted4 { |
1106 | 1 | None |
1107 | | } else { |
1108 | 4 | subscriber.borrow().await.ok() |
1109 | | }; |
1110 | 5 | if let Some(reloaded_awaited_action4 ) = maybe_awaited_action { |
1111 | 4 | maybe_reloaded_awaited_action = Some(reloaded_awaited_action); |
1112 | 4 | } else { |
1113 | 1 | warn!( |
1114 | | "Failed to update action to timed out state after client keepalive timeout. This is ok if multiple schedulers tried to set the state at the same time: {err}", |
1115 | | ); |
1116 | 1 | break; |
1117 | | } |
1118 | | // Re-check the predicate after reload. |
1119 | 4 | if maybe_reloaded_awaited_action |
1120 | 4 | .as_ref() |
1121 | 4 | .is_some_and(|awaited_action| { |
1122 | 4 | awaited_action.last_client_keepalive_timestamp() |
1123 | 4 | + self.client_action_timeout |
1124 | 4 | >= (self.now_fn)().now() |
1125 | 4 | }) |
1126 | | { |
1127 | 0 | timed_out = false; |
1128 | 0 | break; |
1129 | 4 | } else if maybe_reloaded_awaited_action |
1130 | 4 | .as_ref() |
1131 | 4 | .is_some_and(|awaited_action| awaited_action.state().stage.is_finished()) |
1132 | | { |
1133 | 0 | break; |
1134 | 4 | } |
1135 | | } |
1136 | 0 | } |
1137 | 2.49k | if timed_out { |
1138 | 2.49k | return false; |
1139 | 0 | } |
1140 | 6.66M | } |
1141 | | // If the action was reloaded, then use that for the rest of the checks |
1142 | | // instead of the input parameter. |
1143 | 6.66M | let awaited_action = maybe_reloaded_awaited_action |
1144 | 6.66M | .as_ref() |
1145 | 6.66M | .unwrap_or(awaited_action); |
1146 | | |
1147 | 6.66M | if let Some(operation_id0 ) = &filter.operation_id |
1148 | 0 | && operation_id != awaited_action.operation_id() |
1149 | | { |
1150 | 0 | return false; |
1151 | 6.66M | } |
1152 | | |
1153 | 6.66M | if filter.worker_id.is_some() && filter.worker_id.as_ref()0 != awaited_action.worker_id() { |
1154 | 0 | return false; |
1155 | 6.66M | } |
1156 | | |
1157 | | { |
1158 | 6.66M | if let Some(filter_unique_key0 ) = &filter.unique_key { |
1159 | 0 | match &awaited_action.action_info().unique_qualifier { |
1160 | 0 | ActionUniqueQualifier::Cacheable(unique_key) => { |
1161 | 0 | if filter_unique_key != unique_key { |
1162 | 0 | return false; |
1163 | 0 | } |
1164 | | } |
1165 | | ActionUniqueQualifier::Uncacheable(_) => { |
1166 | 0 | return false; |
1167 | | } |
1168 | | } |
1169 | 6.66M | } |
1170 | 6.66M | if let Some(action_digest0 ) = filter.action_digest |
1171 | 0 | && action_digest != awaited_action.action_info().digest() |
1172 | | { |
1173 | 0 | return false; |
1174 | 6.66M | } |
1175 | | } |
1176 | | |
1177 | | { |
1178 | 6.66M | let last_worker_update_timestamp = awaited_action.last_worker_updated_timestamp(); |
1179 | 6.66M | if let Some(worker_update_before0 ) = filter.worker_update_before |
1180 | 0 | && worker_update_before < last_worker_update_timestamp |
1181 | | { |
1182 | 0 | return false; |
1183 | 6.66M | } |
1184 | 6.66M | if let Some(completed_before0 ) = filter.completed_before |
1185 | 0 | && awaited_action.state().stage.is_finished() |
1186 | 0 | && completed_before < last_worker_update_timestamp |
1187 | | { |
1188 | 0 | return false; |
1189 | 6.66M | } |
1190 | 6.66M | if filter.stages != OperationStageFlags::Any { |
1191 | 6.66M | let stage_flag = match awaited_action.state().stage { |
1192 | 0 | ActionStage::Unknown => OperationStageFlags::Any, |
1193 | 0 | ActionStage::CacheCheck => OperationStageFlags::CacheCheck, |
1194 | 6.66M | ActionStage::Queued => OperationStageFlags::Queued, |
1195 | 8 | ActionStage::Executing => OperationStageFlags::Executing, |
1196 | | ActionStage::Completed(_) | ActionStage::CompletedFromCache(_) => { |
1197 | 0 | OperationStageFlags::Completed |
1198 | | } |
1199 | | }; |
1200 | 6.66M | if !filter.stages.intersects(stage_flag) { |
1201 | 0 | return false; |
1202 | 6.66M | } |
1203 | 211 | } |
1204 | | } |
1205 | | |
1206 | 6.66M | true |
1207 | 6.67M | } |
1208 | | |
1209 | | /// Let the scheduler know that an operation has timed out from |
1210 | | /// the client side (ie: worker has not updated in a while). |
1211 | | /// Returns whether the operation was timed out; false when the check |
1212 | | /// under the lock finds it no longer needs to be. |
1213 | 12 | async fn timeout_operation_id(&self, operation_id: &OperationId) -> Result<bool, Error> { |
1214 | | // Ensure that only one timeout operation is running at a time. |
1215 | | // Failing to do this could result in the same operation being |
1216 | | // timed out multiple times at the same time. |
1217 | | // Note: We could implement this on a per-operation_id basis, but it is quite |
1218 | | // complex to manage the locks. |
1219 | 12 | let _lock = self.timeout_operation_mux.lock().await; |
1220 | | |
1221 | 12 | let awaited_action_subscriber = self |
1222 | 12 | .action_db |
1223 | 12 | .get_by_operation_id(operation_id) |
1224 | 12 | .await |
1225 | 12 | .err_tip(|| "In SimpleSchedulerStateManager::timeout_operation_id")?0 |
1226 | 12 | .err_tip(|| {0 |
1227 | 0 | format!("Operation id {operation_id} does not exist in SimpleSchedulerStateManager::timeout_operation_id") |
1228 | 0 | })?; |
1229 | | |
1230 | 12 | let awaited_action = awaited_action_subscriber |
1231 | 12 | .borrow() |
1232 | 12 | .await |
1233 | 12 | .err_tip(|| "In SimpleSchedulerStateManager::timeout_operation_id")?0 ; |
1234 | | |
1235 | | // Re-check under the lock against freshly loaded state, and delegate |
1236 | | // rather than re-deriving the rule: the two copies had drifted. |
1237 | 12 | let Some(cause8 ) = self.timeout_cause(&awaited_action).await else { |
1238 | 4 | trace!( |
1239 | | %operation_id, |
1240 | 0 | worker_id = ?awaited_action.worker_id(), |
1241 | | "Operation no longer needs timing out, skipping" |
1242 | | ); |
1243 | 4 | return Ok(false); |
1244 | | }; |
1245 | | |
1246 | 8 | warn!( |
1247 | | %operation_id, |
1248 | 2 | worker_id = ?awaited_action.worker_id(), |
1249 | | %cause, |
1250 | | "Timing out operation" |
1251 | | ); |
1252 | | |
1253 | 8 | self.assign_operation( |
1254 | 8 | operation_id, |
1255 | 8 | Err(make_err!(Code::DeadlineExceeded, "Operation {cause}")), |
1256 | 8 | ) |
1257 | 8 | .await |
1258 | 8 | .map(|()| true) |
1259 | 12 | } |
1260 | | |
1261 | 21.9k | async fn inner_update_operation( |
1262 | 21.9k | &self, |
1263 | 21.9k | operation_id: &OperationId, |
1264 | 21.9k | maybe_worker_id: Option<&WorkerId>, |
1265 | 21.9k | worker_node: Option<&str>, |
1266 | 21.9k | update: UpdateOperationType, |
1267 | 21.9k | ) -> Result<(), Error> { |
1268 | 21.9k | let update_type_str = match &update { |
1269 | 0 | UpdateOperationType::KeepAlive => "KeepAlive", |
1270 | 21.1k | UpdateOperationType::UpdateWithActionStage(stage) => match stage { |
1271 | 0 | ActionStage::Queued => "Stage:Queued", |
1272 | 11.0k | ActionStage::Executing => "Stage:Executing", |
1273 | 10.1k | ActionStage::Completed(_) => "Stage:Completed", |
1274 | 0 | ActionStage::CompletedFromCache(_) => "Stage:CompletedFromCache", |
1275 | 0 | ActionStage::CacheCheck => "Stage:CacheCheck", |
1276 | 0 | ActionStage::Unknown => "Stage:Unknown", |
1277 | | }, |
1278 | 283 | UpdateOperationType::UpdateWithError(_) => "Error", |
1279 | 445 | UpdateOperationType::UpdateWithDisconnect(_) => "Disconnect", |
1280 | 1 | UpdateOperationType::ExecutionComplete => "ExecutionComplete", |
1281 | 12 | UpdateOperationType::UpdateWithDecline(_) => "Decline", |
1282 | 15 | UpdateOperationType::UpdateWithEscalation(_) => "Escalation", |
1283 | | }; |
1284 | | |
1285 | 21.9k | debug!( |
1286 | | %operation_id, |
1287 | | ?maybe_worker_id, |
1288 | | update_type = %update_type_str, |
1289 | | "inner_update_operation START" |
1290 | | ); |
1291 | | |
1292 | 21.9k | let mut last_err = None; |
1293 | 21.9k | let mut retry_count = 0; |
1294 | 21.9k | for _ in 0..MAX_UPDATE_RETRIES { |
1295 | 21.9k | retry_count += 1; |
1296 | 21.9k | if retry_count > 1 { |
1297 | 20 | let delay = version_conflict_backoff(retry_count); |
1298 | | |
1299 | 20 | warn!( |
1300 | | %operation_id, |
1301 | | ?maybe_worker_id, |
1302 | | retry_count, |
1303 | 6 | delay_ms = delay.as_millis(), |
1304 | | update_type = %update_type_str, |
1305 | | "Retrying operation update after a version conflict or a transient store error (with backoff)" |
1306 | | ); |
1307 | | |
1308 | 20 | tokio::time::sleep(delay).await; |
1309 | 21.9k | } |
1310 | 21.9k | let maybe_awaited_action_subscriber = self |
1311 | 21.9k | .action_db |
1312 | 21.9k | .get_by_operation_id(operation_id) |
1313 | 21.9k | .await |
1314 | 21.9k | .err_tip(|| "In SimpleSchedulerStateManager::update_operation")?0 ; |
1315 | 21.9k | let Some(awaited_action_subscriber21.9k ) = maybe_awaited_action_subscriber else { |
1316 | | // No action found. It is ok if the action was not found. It |
1317 | | // probably means that the action was dropped, but worker was |
1318 | | // still processing it. |
1319 | 2 | warn!( |
1320 | | %operation_id, |
1321 | | "Unable to update action due to it being missing, probably dropped" |
1322 | | ); |
1323 | 2 | return Ok(()); |
1324 | | }; |
1325 | | |
1326 | 21.9k | let mut awaited_action21.9k = awaited_action_subscriber |
1327 | 21.9k | .borrow() |
1328 | 21.9k | .await |
1329 | 21.9k | .err_tip(|| "In SimpleSchedulerStateManager::update_operation")?7 ; |
1330 | | |
1331 | | // Make sure the worker id matches the awaited action worker id. |
1332 | | // This might happen if the worker sending the update is not the |
1333 | | // worker that was assigned. |
1334 | 21.9k | if awaited_action.worker_id().is_some() |
1335 | 10.8k | && maybe_worker_id.is_some() |
1336 | 10.8k | && maybe_worker_id != awaited_action.worker_id() |
1337 | | { |
1338 | | // If another worker is already assigned to the action, another |
1339 | | // worker probably picked up the action. We should not update the |
1340 | | // action in this case and abort this operation. |
1341 | 2 | let err = make_err!( |
1342 | 2 | Code::Aborted, |
1343 | | "Worker ids do not match - {:?} != {:?} for {:?}", |
1344 | | maybe_worker_id, |
1345 | 2 | awaited_action.worker_id(), |
1346 | | awaited_action, |
1347 | | ); |
1348 | 2 | info!( |
1349 | | "Worker ids do not match - {:?} != {:?} for {:?}. This is probably due to another worker picking up the action.", |
1350 | | maybe_worker_id, |
1351 | 2 | awaited_action.worker_id(), |
1352 | | awaited_action, |
1353 | | ); |
1354 | 2 | return Err(err); |
1355 | 21.9k | } |
1356 | | |
1357 | | // Make sure we don't update an action that is already completed. |
1358 | 21.9k | if awaited_action.state().stage.is_finished() { |
1359 | 6 | match &update { |
1360 | | UpdateOperationType::UpdateWithDisconnect(_) |
1361 | | | UpdateOperationType::KeepAlive => { |
1362 | | // No need to error a keep-alive when it's completed, it's just |
1363 | | // unnecessary log noise. |
1364 | 0 | return Ok(()); |
1365 | | } |
1366 | | _ => { |
1367 | 6 | return Err(make_err!( |
1368 | 6 | Code::Internal, |
1369 | 6 | "Action {operation_id} is already completed with state {:?} - maybe_worker_id: {:?}", |
1370 | 6 | awaited_action.state().stage, |
1371 | 6 | maybe_worker_id, |
1372 | 6 | )); |
1373 | | } |
1374 | | } |
1375 | 21.9k | } |
1376 | | |
1377 | | // A disconnect for an operation no worker holds any more (the |
1378 | | // executing sweep requeued it) must not spend a worker loss. |
1379 | 21.9k | if matches!21.4k (update, UpdateOperationType::UpdateWithDisconnect(_)) |
1380 | 440 | && awaited_action.worker_id().is_none() |
1381 | | { |
1382 | 0 | return Ok(()); |
1383 | 21.9k | } |
1384 | | |
1385 | 21.9k | let mut is_retry = false; |
1386 | 21.9k | let stage21.9k = match &update { |
1387 | | UpdateOperationType::KeepAlive => { |
1388 | 0 | awaited_action.worker_keep_alive((self.now_fn)().now()); |
1389 | 0 | match self |
1390 | 0 | .action_db |
1391 | 0 | .update_awaited_action(awaited_action) |
1392 | 0 | .await |
1393 | 0 | .err_tip(|| "Failed to send KeepAlive in SimpleSchedulerStateManager::update_operation") { |
1394 | | // Try again if there was a version mismatch. |
1395 | 0 | Err(err) if err.code == Code::Aborted => { |
1396 | 0 | last_err = Some(err); |
1397 | 0 | continue; |
1398 | | } |
1399 | 0 | result => return result, |
1400 | | } |
1401 | | } |
1402 | 21.1k | UpdateOperationType::UpdateWithActionStage(stage) => { |
1403 | 21.1k | if stage == &ActionStage::Executing |
1404 | 11.0k | && awaited_action.state().stage == ActionStage::Executing |
1405 | | { |
1406 | 7 | warn!(state = ?awaited_action.state(), "Action already assigned"); |
1407 | 7 | return Err(make_err!(Code::Aborted, "Action already assigned")); |
1408 | 21.1k | } |
1409 | | // A command that ran past its timeout on a worker comes |
1410 | | // back as a result. Once, it is queued again for another |
1411 | | // worker: a worker whose host has gone slow times out |
1412 | | // everything it is given, and only a second worker tells |
1413 | | // that from an action that is slow by itself. A timeout |
1414 | | // there, or with no budget, is final as before. |
1415 | 10.1k | if let (ActionStage::Completed(result), Some(worker_id)) = |
1416 | 21.1k | (stage, maybe_worker_id) |
1417 | 10.1k | && result |
1418 | 10.1k | .error |
1419 | 10.1k | .as_ref() |
1420 | 10.1k | .is_some_and(|err| err.code31 == Code::DeadlineExceeded31 ) |
1421 | 28 | && awaited_action.timeouts < self.max_timeout_retries |
1422 | | { |
1423 | 5 | awaited_action.timeouts += 1; |
1424 | 5 | awaited_action.avoid_worker(worker_id.clone()); |
1425 | 5 | if let Some(node2 ) = worker_node { |
1426 | 2 | awaited_action.avoid_node(node); |
1427 | 3 | } |
1428 | 5 | warn!( |
1429 | | %operation_id, |
1430 | | %worker_id, |
1431 | | timeouts = awaited_action.timeouts, |
1432 | | max_timeout_retries = self.max_timeout_retries, |
1433 | | "Command timed out on the worker; queued again for another worker" |
1434 | | ); |
1435 | 5 | is_retry = true; |
1436 | 5 | ActionStage::Queued |
1437 | | } else { |
1438 | 21.1k | stage.clone() |
1439 | | } |
1440 | | } |
1441 | 282 | UpdateOperationType::UpdateWithError(err) => { |
1442 | | // Don't count a backpressure failure as an attempt for an action. |
1443 | 282 | let due_to_backpressure = err.code == Code::ResourceExhausted; |
1444 | | // Nor a store the worker could not reach once its retries |
1445 | | // ran out: a CAS pod being replaced is not the action's |
1446 | | // doing. The worker marks those; a bare Unavailable is |
1447 | | // also what this scheduler raises for a lost result, which |
1448 | | // must keep spending max_job_retries. A store that never |
1449 | | // comes back is bounded like a lost worker. |
1450 | 282 | let store_unreachable = |
1451 | 282 | err.message_string().contains(STORE_UNREACHABLE_ERROR_TIP); |
1452 | | // A result that never reached the scheduler is not the |
1453 | | // worker's doing either: it ran the action. The retry |
1454 | | // spends max_job_retries, as before, but may go straight |
1455 | | // back to that worker; avoiding it would make a lone |
1456 | | // worker wait out the fallback for its own lost result. |
1457 | 282 | let lost_result = err.message_string().contains(LOST_RESULT_ERROR_TIP); |
1458 | 282 | if store_unreachable { |
1459 | 3 | awaited_action.store_losses += 1; |
1460 | 279 | } else if !due_to_backpressure { |
1461 | 278 | awaited_action.attempts += 1; |
1462 | 278 | }1 |
1463 | | |
1464 | 282 | if store_unreachable |
1465 | 3 | && awaited_action.store_losses > self.max_worker_loss_retries |
1466 | | { |
1467 | 1 | ActionStage::Completed(ActionResult { |
1468 | 1 | execution_metadata: ExecutionMetadata { |
1469 | 1 | worker: maybe_worker_id.map_or_else(String::default, ToString::to_string), |
1470 | 1 | ..ExecutionMetadata::default() |
1471 | 1 | }, |
1472 | 1 | error: Some(make_err!( |
1473 | 1 | Code::FailedPrecondition, |
1474 | 1 | "The worker running this action could not reach its store {} times, more than max_worker_loss_retries ({}); the store this action's inputs live on is down, not the action. {}", |
1475 | 1 | awaited_action.store_losses, |
1476 | 1 | self.max_worker_loss_retries, |
1477 | 1 | format!("for operation_id: {operation_id}, maybe_worker_id: {maybe_worker_id:?}"), |
1478 | 1 | ).merge(err.clone())), |
1479 | 1 | ..ActionResult::default() |
1480 | 1 | }) |
1481 | 281 | } else if awaited_action.attempts > self.max_job_retries { |
1482 | | // FailedPrecondition, not Internal: Bazel resubmits an |
1483 | | // Internal execute failure as a fresh operation with a |
1484 | | // fresh attempt counter, so the cap never reached the |
1485 | | // client and a crashing action cycled through the pool |
1486 | | // for as long as the build ran. |
1487 | 3 | ActionStage::Completed(ActionResult { |
1488 | 3 | execution_metadata: ExecutionMetadata { |
1489 | 3 | worker: maybe_worker_id.map_or_else(String::default, ToString::to_string), |
1490 | 3 | ..ExecutionMetadata::default() |
1491 | 3 | }, |
1492 | 3 | error: Some(make_err!( |
1493 | 3 | Code::FailedPrecondition, |
1494 | 3 | "Job cancelled because it attempted to execute too many times {} > {} times {}", |
1495 | 3 | awaited_action.attempts, |
1496 | 3 | self.max_job_retries, |
1497 | 3 | format!("for operation_id: {operation_id}, maybe_worker_id: {maybe_worker_id:?}"), |
1498 | 3 | ).merge(err.clone())), |
1499 | 3 | ..ActionResult::default() |
1500 | 3 | }) |
1501 | | } else { |
1502 | 278 | if let Some(worker_id270 ) = maybe_worker_id |
1503 | 270 | && !store_unreachable |
1504 | 268 | && !due_to_backpressure |
1505 | 267 | && !lost_result |
1506 | | { |
1507 | | // The action failed on this worker; a retry |
1508 | | // prefers another while one can take it. |
1509 | 265 | awaited_action.avoid_worker(worker_id.clone()); |
1510 | 265 | if let Some(node0 ) = worker_node { |
1511 | 0 | awaited_action.avoid_node(node); |
1512 | 265 | } |
1513 | 13 | } |
1514 | 278 | is_retry = true; |
1515 | 278 | ActionStage::Queued |
1516 | | } |
1517 | | } |
1518 | 440 | UpdateOperationType::UpdateWithDisconnect(loss) => { |
1519 | | // A lost worker (OOM-killed as a whole, pod evicted, |
1520 | | // network drop, timed out) is usually not this action's |
1521 | | // doing, so it has its own budget rather than spending |
1522 | | // the action's `max_job_retries`. The budget is what |
1523 | | // stops an action that takes a worker down every time |
1524 | | // it runs from doing so forever. |
1525 | 440 | awaited_action.worker_losses += 1; |
1526 | | |
1527 | 440 | if awaited_action.worker_losses > self.max_worker_loss_retries { |
1528 | 3 | ActionStage::Completed(ActionResult { |
1529 | 3 | execution_metadata: ExecutionMetadata { |
1530 | 3 | worker: maybe_worker_id |
1531 | 3 | .map_or_else(String::default, ToString::to_string), |
1532 | 3 | ..ExecutionMetadata::default() |
1533 | 3 | }, |
1534 | 3 | error: Some(make_err!( |
1535 | 3 | Code::FailedPrecondition, |
1536 | 3 | "The worker running this action was lost {} times, more than max_worker_loss_retries ({}); the action is likely what takes the worker down (OOM-killed pod, eviction). Give it a memory reservation or raise the pool's memory limit before retrying. {}", |
1537 | 3 | awaited_action.worker_losses, |
1538 | 3 | self.max_worker_loss_retries, |
1539 | 3 | format!( |
1540 | 3 | "for operation_id: {operation_id}, maybe_worker_id: {maybe_worker_id:?}" |
1541 | 3 | ), |
1542 | 3 | )), |
1543 | 3 | ..ActionResult::default() |
1544 | 3 | }) |
1545 | | } else { |
1546 | | // A loss that may have been this action's memory |
1547 | | // (RFC-0018): escalate when the worker saw it over |
1548 | | // its reservation, or when this is not the first |
1549 | | // worker it took down, while the escalation budget |
1550 | | // lasts. Past it, the loss budget ends the action. |
1551 | 437 | if let Some(loss21 ) = loss |
1552 | 21 | && (loss.sampled_over |
1553 | 13 | || (self.escalate_after_worker_losses > 0 |
1554 | 9 | && awaited_action.worker_losses |
1555 | 9 | >= self.escalate_after_worker_losses)) |
1556 | 12 | && (self.max_escalations == 0 |
1557 | 6 | || awaited_action.escalations < self.max_escalations) |
1558 | | { |
1559 | 9 | awaited_action.escalations += 1; |
1560 | 9 | warn!( |
1561 | | %operation_id, |
1562 | | ?maybe_worker_id, |
1563 | | worker_losses = awaited_action.worker_losses, |
1564 | | sampled_over = loss.sampled_over, |
1565 | | property = %loss.escalation.property, |
1566 | | escalated = loss.escalation.value, |
1567 | | "Worker lost under this action; requeuing with a larger reservation" |
1568 | | ); |
1569 | 9 | raise_reservation(&mut awaited_action, &loss.escalation); |
1570 | 9 | record_dispatch_requeue("loss_escalation"); |
1571 | 428 | } |
1572 | 437 | is_retry = true; |
1573 | 437 | ActionStage::Queued |
1574 | | } |
1575 | | } |
1576 | 15 | UpdateOperationType::UpdateWithEscalation(escalation) => { |
1577 | | // The scheduler's own sizing decision, not a failure of |
1578 | | // the action: it has its own budget and does not spend |
1579 | | // `max_job_retries`. The fleet's ceiling bounds it |
1580 | | // anyway; the budget catches a fleet whose ceiling is |
1581 | | // far above where the action started. |
1582 | 15 | awaited_action.escalations += 1; |
1583 | 15 | if self.max_escalations > 0 && awaited_action.escalations > self.max_escalations5 |
1584 | | { |
1585 | 1 | ActionStage::Completed(ActionResult { |
1586 | 1 | execution_metadata: ExecutionMetadata { |
1587 | 1 | worker: maybe_worker_id |
1588 | 1 | .map_or_else(String::default, ToString::to_string), |
1589 | 1 | ..ExecutionMetadata::default() |
1590 | 1 | }, |
1591 | 1 | error: Some( |
1592 | 1 | make_err!( |
1593 | 1 | Code::FailedPrecondition, |
1594 | 1 | "Job cancelled after being killed for memory at every reservation tried ({} escalations, more than max_steps {}); the last reservation was {} {} {}", |
1595 | 1 | awaited_action.escalations, |
1596 | 1 | self.max_escalations, |
1597 | 1 | escalation.value, |
1598 | 1 | escalation.property, |
1599 | 1 | format!("for operation_id: {operation_id}, maybe_worker_id: {maybe_worker_id:?}"), |
1600 | 1 | ) |
1601 | 1 | .merge(escalation.reason.clone()), |
1602 | 1 | ), |
1603 | 1 | ..ActionResult::default() |
1604 | 1 | }) |
1605 | | } else { |
1606 | | // The same action, asking for more: the next |
1607 | | // dispatch carries the raised reservation and the |
1608 | | // worker holds it to that. The last step also asks |
1609 | | // for the worker's whole CPU, so it runs alone. |
1610 | 14 | raise_reservation(&mut awaited_action, escalation); |
1611 | 14 | is_retry = true; |
1612 | 14 | ActionStage::Queued |
1613 | | } |
1614 | | } |
1615 | | // We shouldn't get here, but we just ignore it if we do. |
1616 | 12 | UpdateOperationType::UpdateWithDecline(decline) => { |
1617 | | // Nothing ran, so nothing is charged against the retry |
1618 | | // cap and the retry metric does not move; the action |
1619 | | // goes back where it was. |
1620 | 12 | debug!(reason = %decline.reason, "Dispatch declined, requeuing untried"); |
1621 | 12 | ActionStage::Queued |
1622 | | } |
1623 | | UpdateOperationType::ExecutionComplete => { |
1624 | 0 | warn!("inner_update_operation got an ExecutionComplete, that's unexpected."); |
1625 | 0 | return Ok(()); |
1626 | | } |
1627 | | }; |
1628 | 21.9k | let now = (self.now_fn)().now(); |
1629 | 21.9k | if matches!21.1k (stage, ActionStage::Queued) { |
1630 | 746 | // If the action is queued, we need to unset the worker id regardless of |
1631 | 746 | // which worker sent the update. |
1632 | 746 | awaited_action.set_worker_id(None, now); |
1633 | 21.1k | } else { |
1634 | 21.1k | awaited_action.set_worker_id(maybe_worker_id.cloned(), now); |
1635 | 21.1k | } |
1636 | 21.9k | awaited_action.worker_set_state( |
1637 | 21.9k | Arc::new(ActionState { |
1638 | 21.9k | stage, |
1639 | 21.9k | // Client id is not known here, it is the responsibility of |
1640 | 21.9k | // the the subscriber impl to replace this with the |
1641 | 21.9k | // correct client id. |
1642 | 21.9k | client_operation_id: operation_id.clone(), |
1643 | 21.9k | action_digest: awaited_action.action_info().digest(), |
1644 | 21.9k | last_transition_timestamp: now, |
1645 | 21.9k | }), |
1646 | 21.9k | now, |
1647 | | ); |
1648 | | |
1649 | 21.9k | let update_action_result = self |
1650 | 21.9k | .action_db |
1651 | 21.9k | .update_awaited_action(awaited_action.clone()) |
1652 | 21.9k | .await |
1653 | 21.9k | .err_tip(|| "In SimpleSchedulerStateManager::update_operation"); |
1654 | 21.9k | if let Err(err21 ) = update_action_result { |
1655 | | // Aborted says the record moved under us: re-read and apply |
1656 | | // again. A store that is unreachable, slow, full or answering |
1657 | | // with a server error is transient in the same way, and the |
1658 | | // cost of not retrying is not symmetric: the worker has |
1659 | | // already reported this result and will not report it again, |
1660 | | // so a dropped write parks the action until the executing |
1661 | | // ceiling (found by the simulation harness, 2026-11-04). The |
1662 | | // same bounded budget and backoff cover both; a store that |
1663 | | // stays down fails the update as before. |
1664 | 21 | if is_transient_store_error(err.code) { |
1665 | 20 | debug!( |
1666 | | %operation_id, |
1667 | | retry_count, |
1668 | | update_type = %update_type_str, |
1669 | | code = ?err.code, |
1670 | | "Store write did not land (transient), will retry" |
1671 | | ); |
1672 | 20 | last_err = Some(err); |
1673 | 20 | continue; |
1674 | 1 | } |
1675 | 1 | warn!( |
1676 | | %operation_id, |
1677 | | update_type = %update_type_str, |
1678 | | ?err, |
1679 | | "inner_update_operation FAILED (non-retryable)" |
1680 | | ); |
1681 | 1 | return Err(err); |
1682 | 21.8k | } |
1683 | | |
1684 | 21.8k | record_execution_metrics(&awaited_action, is_retry); |
1685 | | |
1686 | 21.8k | debug!( |
1687 | | %operation_id, |
1688 | | retry_count, |
1689 | | update_type = %update_type_str, |
1690 | | "inner_update_operation SUCCESS" |
1691 | | ); |
1692 | 21.8k | return Ok(()); |
1693 | | } |
1694 | | |
1695 | 0 | warn!( |
1696 | | %operation_id, |
1697 | | update_type = %update_type_str, |
1698 | | retry_count = MAX_UPDATE_RETRIES, |
1699 | | "inner_update_operation EXHAUSTED all retries" |
1700 | | ); |
1701 | 0 | Err(last_err.unwrap_or_else(|| { |
1702 | 0 | make_err!( |
1703 | 0 | Code::Internal, |
1704 | | "Failed to update action after {} retries with no error set", |
1705 | | MAX_UPDATE_RETRIES, |
1706 | | ) |
1707 | 0 | })) |
1708 | 21.9k | } |
1709 | | |
1710 | 16.5k | async fn inner_add_operation( |
1711 | 16.5k | &self, |
1712 | 16.5k | new_client_operation_id: OperationId, |
1713 | 16.5k | action_info: Arc<ActionInfo>, |
1714 | 16.5k | ) -> Result<T::Subscriber, Error> { |
1715 | 16.5k | self.action_db |
1716 | 16.5k | .add_action( |
1717 | 16.5k | new_client_operation_id, |
1718 | 16.5k | action_info, |
1719 | 16.5k | self.no_event_action_timeout, |
1720 | 16.5k | ) |
1721 | 16.5k | .await |
1722 | 16.5k | .err_tip(|| "In SimpleSchedulerStateManager::add_operation") |
1723 | 16.5k | } |
1724 | | |
1725 | 37.4k | async fn inner_filter_operations<'a, F>( |
1726 | 37.4k | &'a self, |
1727 | 37.4k | filter: OperationFilter, |
1728 | 37.4k | to_action_state_result: F, |
1729 | 37.4k | ) -> Result<ActionStateResultStream<'a>, Error> |
1730 | 37.4k | where |
1731 | 37.4k | F: Fn(T::Subscriber) -> Box<dyn ActionStateResult> + Send + Sync + 'a, |
1732 | 37.4k | { |
1733 | 37.0k | const fn sorted_awaited_action_state_for_flags( |
1734 | 37.0k | stage: OperationStageFlags, |
1735 | 37.0k | ) -> Option<SortedAwaitedActionState> { |
1736 | 37.0k | match stage { |
1737 | 0 | OperationStageFlags::CacheCheck => Some(SortedAwaitedActionState::CacheCheck), |
1738 | 36.8k | OperationStageFlags::Queued => Some(SortedAwaitedActionState::Queued), |
1739 | 184 | OperationStageFlags::Executing => Some(SortedAwaitedActionState::Executing), |
1740 | 0 | OperationStageFlags::Completed => Some(SortedAwaitedActionState::Completed), |
1741 | 8 | _ => None, |
1742 | | } |
1743 | 37.0k | } |
1744 | | |
1745 | 37.4k | if let Some(operation_id0 ) = &filter.operation_id { |
1746 | 0 | let maybe_subscriber = self |
1747 | 0 | .action_db |
1748 | 0 | .get_by_operation_id(operation_id) |
1749 | 0 | .await |
1750 | 0 | .err_tip(|| "In SimpleSchedulerStateManager::filter_operations")?; |
1751 | 0 | let Some(subscriber) = maybe_subscriber else { |
1752 | 0 | return Ok(Box::pin(stream::empty())); |
1753 | | }; |
1754 | 0 | let awaited_action = subscriber |
1755 | 0 | .borrow() |
1756 | 0 | .await |
1757 | 0 | .err_tip(|| "In SimpleSchedulerStateManager::filter_operations")?; |
1758 | 0 | if !self |
1759 | 0 | .apply_filter_predicate(&awaited_action, &subscriber, &filter) |
1760 | 0 | .await |
1761 | | { |
1762 | 0 | return Ok(Box::pin(stream::empty())); |
1763 | 0 | } |
1764 | 0 | return Ok(Box::pin(stream::once(async move { |
1765 | 0 | to_action_state_result(subscriber) |
1766 | 0 | }))); |
1767 | 37.4k | } |
1768 | 37.4k | if let Some(client_operation_id413 ) = &filter.client_operation_id { |
1769 | 413 | let maybe_subscriber = self |
1770 | 413 | .action_db |
1771 | 413 | .get_awaited_action_by_id(client_operation_id) |
1772 | 413 | .await |
1773 | 413 | .err_tip(|| "In SimpleSchedulerStateManager::filter_operations")?0 ; |
1774 | 413 | let Some(subscriber207 ) = maybe_subscriber else { |
1775 | 206 | return Ok(Box::pin(stream::empty())); |
1776 | | }; |
1777 | 207 | let awaited_action = subscriber |
1778 | 207 | .borrow() |
1779 | 207 | .await |
1780 | 207 | .err_tip(|| "In SimpleSchedulerStateManager::filter_operations")?0 ; |
1781 | 207 | if !self |
1782 | 207 | .apply_filter_predicate(&awaited_action, &subscriber, &filter) |
1783 | 207 | .await |
1784 | | { |
1785 | 0 | return Ok(Box::pin(stream::empty())); |
1786 | 207 | } |
1787 | 207 | return Ok(Box::pin(stream::once(async move { |
1788 | 207 | to_action_state_result(subscriber) |
1789 | 207 | }))); |
1790 | 37.0k | } |
1791 | | |
1792 | 37.0k | let Some(sorted_awaited_action_state) = |
1793 | 37.0k | sorted_awaited_action_state_for_flags(filter.stages) |
1794 | | else { |
1795 | 8 | let mut all_items7 : Vec<_>7 = self |
1796 | 8 | .action_db |
1797 | 8 | .get_all_awaited_actions() |
1798 | 8 | .await |
1799 | 8 | .err_tip(|| "In SimpleSchedulerStateManager::filter_operations")?0 |
1800 | 24 | .filter_map8 (|listed| async move {12 listed_or_lost12 (listed12 ) }) |
1801 | 10 | .try_filter_map8 (|awaited_action_subscriber| async move { |
1802 | 10 | borrow_listed(awaited_action_subscriber).await |
1803 | 20 | }) |
1804 | 10 | .try_filter_map8 (|(subscriber, awaited_action)| { |
1805 | 10 | let filter = filter.clone(); |
1806 | 10 | async move { |
1807 | 10 | Ok(self |
1808 | 10 | .apply_filter_predicate(&awaited_action, &subscriber, &filter) |
1809 | 10 | .await |
1810 | 10 | .then_some((subscriber, awaited_action.sort_key()))) |
1811 | 10 | } |
1812 | 10 | }) |
1813 | 8 | .try_collect() |
1814 | 8 | .await |
1815 | 8 | .err_tip(|| "In SimpleSchedulerStateManager::filter_operations")?1 ; |
1816 | | |
1817 | | #[allow(clippy::unnecessary_sort_by)] |
1818 | 0 | match filter.order_by_priority_direction { |
1819 | 0 | Some(OrderDirection::Asc) => all_items.sort_unstable_by(|(_, a), (_, b)| a.cmp(b)), |
1820 | 0 | Some(OrderDirection::Desc) => all_items.sort_unstable_by(|(_, a), (_, b)| b.cmp(a)), |
1821 | 7 | None => {} |
1822 | | } |
1823 | 7 | return Ok(Box::pin(stream::iter( |
1824 | 7 | all_items |
1825 | 7 | .into_iter() |
1826 | 7 | .map(move |(subscriber, _)| to_action_state_result3 (subscriber3 )), |
1827 | | ))); |
1828 | | }; |
1829 | | |
1830 | 37.0k | let desc = matches!517 ( |
1831 | 36.5k | filter.order_by_priority_direction, |
1832 | | Some(OrderDirection::Desc) |
1833 | | ); |
1834 | 37.0k | let stream37.0k = self |
1835 | 37.0k | .action_db |
1836 | 37.0k | .get_range_of_actions( |
1837 | 37.0k | sorted_awaited_action_state, |
1838 | 37.0k | Bound::Unbounded, |
1839 | 37.0k | Bound::Unbounded, |
1840 | 37.0k | desc, |
1841 | 37.0k | ) |
1842 | 37.0k | .await |
1843 | 37.0k | .err_tip(|| "In SimpleSchedulerStateManager::filter_operations")?4 |
1844 | 13.3M | .filter_map37.0k (|listed| async move {6.67M listed_or_lost6.67M (listed6.67M ) }) |
1845 | 6.67M | .try_filter_map37.0k (|awaited_action_subscriber| async move { |
1846 | 6.67M | borrow_listed(awaited_action_subscriber).await |
1847 | 13.3M | }) |
1848 | 6.67M | .try_filter_map37.0k (move |(subscriber, awaited_action)| { |
1849 | 6.67M | let filter = filter.clone(); |
1850 | 6.67M | async move { |
1851 | 6.67M | Ok(self |
1852 | 6.67M | .apply_filter_predicate(&awaited_action, &subscriber, &filter) |
1853 | 6.67M | .await |
1854 | 6.67M | .then_some(subscriber)) |
1855 | 6.67M | } |
1856 | 6.67M | }) |
1857 | 6.66M | .map37.0k (move |result| -> Box<dyn ActionStateResult> { |
1858 | 6.66M | result.map_or_else( |
1859 | 2 | |e| -> Box<dyn ActionStateResult> { Box::new(ErrorActionStateResult(e)) }, |
1860 | 6.66M | |v| -> Box<dyn ActionStateResult> { to_action_state_result(v) }, |
1861 | | ) |
1862 | 6.66M | }); |
1863 | 37.0k | Ok(Box::pin(stream)) |
1864 | 37.4k | } |
1865 | | } |
1866 | | |
1867 | | /// A listed entry the store could not even hand over: the row was gone |
1868 | | /// (`NotFound`) or would not decode (`InvalidArgument`), which the Redis |
1869 | | /// listing reports as an error item before any subscriber exists. Skipped |
1870 | | /// and counted like a record that fails to read below. Anything else is the |
1871 | | /// store failing and stays an error for the reader. |
1872 | 6.67M | fn listed_or_lost<S>(listed: Result<S, Error>) -> Option<Result<S, Error>> { |
1873 | 5 | match listed { |
1874 | 5 | Err(err3 ) if is_lost_record(&err)3 => { |
1875 | 3 | warn!( |
1876 | | ?err, |
1877 | | "Operation listed but its row cannot be read; skipping it in the listing" |
1878 | | ); |
1879 | 3 | record_awaited_action_orphan("listing"); |
1880 | 3 | None |
1881 | | } |
1882 | 6.67M | listed => Some(listed), |
1883 | | } |
1884 | 6.67M | } |
1885 | | |
1886 | | /// Reads the record behind a listed subscriber. A record the index listed |
1887 | | /// but the store no longer has (it expired between the search and the |
1888 | | /// read, or eviction took it) is skipped and counted, not returned as an |
1889 | | /// error: one such row must not end a listing that the provisioner and the |
1890 | | /// admin API read every few seconds. Any other error is the store failing |
1891 | | /// and is passed on. The sweep and the matcher skip the same way. |
1892 | 6.67M | async fn borrow_listed<S: AwaitedActionSubscriber>( |
1893 | 6.67M | subscriber: S, |
1894 | 6.67M | ) -> Result<Option<(S, AwaitedAction)>, Error> { |
1895 | 6.67M | match subscriber.borrow().await { |
1896 | 6.67M | Ok(awaited_action) => Ok(Some((subscriber, awaited_action))), |
1897 | 2 | Err(err1 ) if is_lost_record(&err)1 => { |
1898 | 1 | warn!( |
1899 | | ?err, |
1900 | | "Operation listed but its record cannot be read; skipping it in the listing" |
1901 | | ); |
1902 | 1 | record_awaited_action_orphan("listing"); |
1903 | 1 | Ok(None) |
1904 | | } |
1905 | | // The store itself failed; that is the listing's error to report. |
1906 | 1 | Err(err) => Err(err).err_tip(|| "In SimpleSchedulerStateManager::filter_operations"), |
1907 | | } |
1908 | 6.67M | } |
1909 | | |
1910 | | #[async_trait] |
1911 | | impl<T, I, NowFn> ClientStateManager for SimpleSchedulerStateManager<T, I, NowFn> |
1912 | | where |
1913 | | T: AwaitedActionDb, |
1914 | | I: InstantWrapper, |
1915 | | NowFn: Fn() -> I + Clone + Send + Unpin + Sync + 'static, |
1916 | | { |
1917 | | async fn add_action( |
1918 | | &self, |
1919 | | client_operation_id: OperationId, |
1920 | | action_info: Arc<ActionInfo>, |
1921 | 16.5k | ) -> Result<Box<dyn ActionStateResult>, Error> { |
1922 | | let sub = self |
1923 | | .inner_add_operation(client_operation_id, action_info.clone()) |
1924 | | .await?; |
1925 | | |
1926 | | Ok(Box::new(ClientActionStateResult::new( |
1927 | | sub, |
1928 | | self.weak_self.clone(), |
1929 | | self.no_event_action_timeout, |
1930 | | self.now_fn.clone(), |
1931 | | ))) |
1932 | 16.5k | } |
1933 | | |
1934 | | async fn filter_operations<'a>( |
1935 | | &'a self, |
1936 | | filter: OperationFilter, |
1937 | 1.10k | ) -> Result<ActionStateResultStream<'a>, Error> { |
1938 | 718 | self.inner_filter_operations(filter, move |rx| { |
1939 | 718 | Box::new(ClientActionStateResult::new( |
1940 | 718 | rx, |
1941 | 718 | self.weak_self.clone(), |
1942 | 718 | self.no_event_action_timeout, |
1943 | 718 | self.now_fn.clone(), |
1944 | 718 | )) |
1945 | 718 | }) |
1946 | | .await |
1947 | 1.10k | } |
1948 | | } |
1949 | | |
1950 | | #[async_trait] |
1951 | | impl<T, I, NowFn> WorkerStateManager for SimpleSchedulerStateManager<T, I, NowFn> |
1952 | | where |
1953 | | T: AwaitedActionDb, |
1954 | | I: InstantWrapper, |
1955 | | NowFn: Fn() -> I + Clone + Send + Unpin + Sync + 'static, |
1956 | | { |
1957 | | async fn update_operation( |
1958 | | &self, |
1959 | | operation_id: &OperationId, |
1960 | | worker_id: &WorkerId, |
1961 | | update: UpdateOperationType, |
1962 | 447 | ) -> Result<(), Error> { |
1963 | | self.inner_update_operation(operation_id, Some(worker_id), None, update) |
1964 | | .await |
1965 | 447 | } |
1966 | | |
1967 | | async fn update_operation_on_node( |
1968 | | &self, |
1969 | | operation_id: &OperationId, |
1970 | | worker_id: &WorkerId, |
1971 | | worker_node: Option<&str>, |
1972 | | update: UpdateOperationType, |
1973 | 10.4k | ) -> Result<(), Error> { |
1974 | | self.inner_update_operation(operation_id, Some(worker_id), worker_node, update) |
1975 | | .await |
1976 | 10.4k | } |
1977 | | |
1978 | | async fn is_executing_on_worker( |
1979 | | &self, |
1980 | | operation_id: &OperationId, |
1981 | | worker_id: &WorkerId, |
1982 | 8.89k | ) -> Result<bool, Error> { |
1983 | | let Some(subscriber) = self |
1984 | | .action_db |
1985 | | .get_by_operation_id(operation_id) |
1986 | | .await |
1987 | | .err_tip(|| "In SimpleSchedulerStateManager::is_executing_on_worker")? |
1988 | | else { |
1989 | | return Ok(false); |
1990 | | }; |
1991 | | let awaited_action = match subscriber.borrow().await { |
1992 | | Ok(awaited_action) => awaited_action, |
1993 | | // Store-backed dbs hand out a subscriber for any id and only |
1994 | | // discover the operation is gone on read. |
1995 | | Err(err) if err.code == Code::NotFound => return Ok(false), |
1996 | | Err(err) => { |
1997 | | return Err(err) |
1998 | | .err_tip(|| "In SimpleSchedulerStateManager::is_executing_on_worker"); |
1999 | | } |
2000 | | }; |
2001 | | Ok( |
2002 | | matches!(awaited_action.state().stage, ActionStage::Executing) |
2003 | | && awaited_action.worker_id() == Some(worker_id), |
2004 | | ) |
2005 | 8.89k | } |
2006 | | } |
2007 | | |
2008 | | #[async_trait] |
2009 | | impl<T, I, NowFn> MatchingEngineStateManager for SimpleSchedulerStateManager<T, I, NowFn> |
2010 | | where |
2011 | | T: AwaitedActionDb, |
2012 | | I: InstantWrapper, |
2013 | | NowFn: Fn() -> I + Clone + Send + Unpin + Sync + 'static, |
2014 | | { |
2015 | | async fn filter_operations<'a>( |
2016 | | &'a self, |
2017 | | filter: OperationFilter, |
2018 | 36.3k | ) -> Result<ActionStateResultStream<'a>, Error> { |
2019 | 6.66M | self.inner_filter_operations(filter, |rx| { |
2020 | 6.66M | Box::new(MatchingEngineActionStateResult::new( |
2021 | 6.66M | rx, |
2022 | 6.66M | self.weak_self.clone(), |
2023 | 6.66M | self.no_event_action_timeout, |
2024 | 6.66M | self.now_fn.clone(), |
2025 | 6.66M | )) |
2026 | 6.66M | }) |
2027 | | .await |
2028 | 36.3k | } |
2029 | | |
2030 | | async fn assign_operation( |
2031 | | &self, |
2032 | | operation_id: &OperationId, |
2033 | | worker_id_or_reason_for_unassign: Result<&WorkerId, Error>, |
2034 | 11.0k | ) -> Result<(), Error> { |
2035 | | let (maybe_worker_id, update) = match worker_id_or_reason_for_unassign { |
2036 | | Ok(worker_id) => ( |
2037 | | Some(worker_id), |
2038 | | UpdateOperationType::UpdateWithActionStage(ActionStage::Executing), |
2039 | | ), |
2040 | | Err(err) => (None, UpdateOperationType::UpdateWithError(err)), |
2041 | | }; |
2042 | | self.inner_update_operation(operation_id, maybe_worker_id, None, update) |
2043 | | .await |
2044 | 11.0k | } |
2045 | | |
2046 | | async fn fail_queued_operation( |
2047 | | &self, |
2048 | | operation_id: &OperationId, |
2049 | | err: Error, |
2050 | 34 | ) -> Result<bool, Error> { |
2051 | | self.inner_fail_queued_operation(operation_id, err).await |
2052 | 34 | } |
2053 | | } |