Coverage Report

Created: 2026-08-22 00:12

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-scheduler/src/simple_scheduler.rs
Line
Count
Source
1
// Copyright 2024 The NativeLink Authors. All rights reserved.
2
//
3
// Licensed under the Functional Source License, Version 1.1, Apache 2.0 Future License (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
//    See LICENSE file for details
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14
15
use std::collections::{BTreeSet, HashMap};
16
use std::sync::Arc;
17
use std::time::{Instant, SystemTime};
18
19
use async_trait::async_trait;
20
use futures::{Future, StreamExt, future};
21
use nativelink_config::schedulers::SimpleSpec;
22
use nativelink_error::{Code, Error, ResultExt};
23
use nativelink_metric::{MetricsComponent, RootMetricsComponent};
24
use nativelink_proto::com::github::trace_machina::nativelink::events::{
25
    Event, OriginEvent, RequestEvent, event, request_event,
26
};
27
use nativelink_proto::com::github::trace_machina::nativelink::remote_execution::StartExecute;
28
use nativelink_util::action_messages::{ActionInfo, ActionState, OperationId, WorkerId};
29
use nativelink_util::instant_wrapper::InstantWrapper;
30
use nativelink_util::metrics::record_matching_pass;
31
use nativelink_util::operation_state_manager::{
32
    ActionStateResult, ActionStateResultStream, ClientStateManager, MatchingEngineStateManager,
33
    OperationFilter, OperationStageFlags, OrderDirection, UpdateOperationType,
34
};
35
use nativelink_util::origin_event::{OriginMetadata, get_node_id};
36
use nativelink_util::shutdown_guard::ShutdownGuard;
37
use nativelink_util::spawn;
38
use nativelink_util::task::JoinHandleDropGuard;
39
use opentelemetry::KeyValue;
40
use opentelemetry::baggage::BaggageExt;
41
use opentelemetry::context::{Context, FutureExt as OtelFutureExt};
42
use opentelemetry_semantic_conventions::attribute::ENDUSER_ID;
43
use tokio::sync::{Notify, mpsc};
44
use tokio::time::Duration;
45
use tracing::{debug, error, info, info_span, warn};
46
use uuid::Uuid;
47
48
use crate::api_worker_scheduler::ApiWorkerScheduler;
49
use crate::awaited_action_db::{AwaitedActionDb, CLIENT_KEEPALIVE_DURATION};
50
use crate::known_platform_property_provider::KnownPlatformPropertyProvider;
51
use crate::platform_property_manager::PlatformPropertyManager;
52
use crate::simple_scheduler_state_manager::SimpleSchedulerStateManager;
53
use crate::worker::{ActionInfoWithProps, Worker, WorkerTimestamp};
54
use crate::worker_registry::WorkerRegistry;
55
use crate::worker_scheduler::WorkerScheduler;
56
57
/// Default timeout for workers in seconds.
58
/// If this changes, remember to change the documentation in the config.
59
const DEFAULT_WORKER_TIMEOUT_S: u64 = 5;
60
61
/// Default timeout for a sent kill to be acknowledged in seconds.
62
/// If this changes, remember to change the documentation in the config.
63
const DEFAULT_UNACKNOWLEDGED_KILL_TIMEOUT_S: u64 = 60;
64
65
/// Mark operations as completed with error if no client has updated them
66
/// within this duration.
67
/// If this changes, remember to change the documentation in the config.
68
const DEFAULT_CLIENT_ACTION_TIMEOUT_S: u64 = 60;
69
70
/// Default times a job can retry before failing.
71
/// If this changes, remember to change the documentation in the config.
72
const DEFAULT_MAX_JOB_RETRIES: usize = 3;
73
74
struct SimpleSchedulerActionStateResult {
75
    client_operation_id: OperationId,
76
    action_state_result: Box<dyn ActionStateResult>,
77
}
78
79
impl SimpleSchedulerActionStateResult {
80
41
    fn new(
81
41
        client_operation_id: OperationId,
82
41
        action_state_result: Box<dyn ActionStateResult>,
83
41
    ) -> Self {
84
41
        Self {
85
41
            client_operation_id,
86
41
            action_state_result,
87
41
        }
88
41
    }
89
}
90
91
#[async_trait]
92
impl ActionStateResult for SimpleSchedulerActionStateResult {
93
5
    async fn as_state(&self) -> Result<(Arc<ActionState>, Option<OriginMetadata>), Error> {
94
        let (mut action_state, origin_metadata) = self
95
            .action_state_result
96
            .as_state()
97
            .await
98
            .err_tip(|| "In SimpleSchedulerActionStateResult")?;
99
        // We need to ensure the client is not aware of the downstream
100
        // operation id, so override it before it goes out.
101
        Arc::make_mut(&mut action_state).client_operation_id = self.client_operation_id.clone();
102
        Ok((action_state, origin_metadata))
103
5
    }
104
105
52
    async fn changed(&mut self) -> Result<(Arc<ActionState>, Option<OriginMetadata>), Error> {
106
        let (mut action_state, origin_metadata) = self
107
            .action_state_result
108
            .changed()
109
            .await
110
            .err_tip(|| "In SimpleSchedulerActionStateResult")?;
111
        // We need to ensure the client is not aware of the downstream
112
        // operation id, so override it before it goes out.
113
        Arc::make_mut(&mut action_state).client_operation_id = self.client_operation_id.clone();
114
        Ok((action_state, origin_metadata))
115
52
    }
116
117
0
    async fn as_action_info(&self) -> Result<(Arc<ActionInfo>, Option<OriginMetadata>), Error> {
118
        self.action_state_result
119
            .as_action_info()
120
            .await
121
            .err_tip(|| "In SimpleSchedulerActionStateResult")
122
0
    }
123
}
124
125
/// Engine used to manage the queued/running tasks and relationship with
126
/// the worker nodes. All state on how the workers and actions are interacting
127
/// should be held in this struct.
128
#[derive(MetricsComponent)]
129
pub struct SimpleScheduler {
130
    /// Manager for matching engine side of the state manager.
131
    #[metric(group = "matching_engine_state_manager")]
132
    matching_engine_state_manager: Arc<dyn MatchingEngineStateManager>,
133
134
    /// Manager for client state of this scheduler.
135
    #[metric(group = "client_state_manager")]
136
    client_state_manager: Arc<dyn ClientStateManager>,
137
138
    /// Manager for platform of this scheduler.
139
    #[metric(group = "platform_properties")]
140
    platform_property_manager: Arc<PlatformPropertyManager>,
141
142
    /// A `Workers` pool that contains all workers that are available to execute actions in a priority
143
    /// order based on the allocation strategy.
144
    #[metric(group = "worker_scheduler")]
145
    worker_scheduler: Arc<ApiWorkerScheduler>,
146
147
    /// The sender to send origin events to the origin events.
148
    maybe_origin_event_tx: Option<mpsc::Sender<OriginEvent>>,
149
150
    /// Background task that tries to match actions to workers. If this struct
151
    /// is dropped the spawn will be cancelled as well.
152
    task_worker_matching_spawn: JoinHandleDropGuard<()>,
153
154
    /// Every duration, do logging of worker matching
155
    /// e.g. "worker busy", "can't find any worker"
156
    /// Set to None to disable. This is quite noisy, so we limit it
157
    worker_match_logging_interval: Option<Duration>,
158
}
159
160
impl core::fmt::Debug for SimpleScheduler {
161
0
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
162
0
        f.debug_struct("SimpleScheduler")
163
0
            .field("platform_property_manager", &self.platform_property_manager)
164
0
            .field("worker_scheduler", &self.worker_scheduler)
165
0
            .field("maybe_origin_event_tx", &self.maybe_origin_event_tx)
166
0
            .field(
167
0
                "task_worker_matching_spawn",
168
0
                &self.task_worker_matching_spawn,
169
0
            )
170
0
            .finish_non_exhaustive()
171
0
    }
172
}
173
174
impl SimpleScheduler {
175
1
    fn origin_event_id(event: &Event) -> String {
176
1
        Uuid::now_v6(&get_node_id(Some(event)))
177
1
            .hyphenated()
178
1
            .to_string()
179
1
    }
180
181
1
    fn scheduler_start_execute_event(
182
1
        worker_id: &WorkerId,
183
1
        operation_id: &OperationId,
184
1
        action_info: &ActionInfoWithProps,
185
1
    ) -> Event {
186
1
        let start_execute = StartExecute {
187
1
            execute_request: Some(action_info.inner.as_ref().into()),
188
1
            operation_id: operation_id.to_string(),
189
1
            queued_timestamp: Some(action_info.inner.insert_timestamp.into()),
190
1
            platform: Some((&action_info.platform_properties).into()),
191
1
            worker_id: worker_id.to_string(),
192
1
        };
193
1
        Event {
194
1
            event: Some(event::Event::Request(RequestEvent {
195
1
                event: Some(request_event::Event::SchedulerStartExecute(start_execute)),
196
1
            })),
197
1
        }
198
1
    }
199
200
1
    async fn publish_scheduler_start_execute(
201
1
        maybe_origin_event_tx: Option<&mpsc::Sender<OriginEvent>>,
202
1
        origin_metadata: &OriginMetadata,
203
1
        event_id: String,
204
1
        event: Event,
205
1
    ) {
206
1
        let Some(origin_event_tx) = maybe_origin_event_tx else {
207
0
            return;
208
        };
209
210
1
        let origin_event = OriginEvent {
211
1
            version: 0,
212
1
            event_id,
213
1
            parent_event_id: String::new(),
214
1
            bazel_request_metadata: origin_metadata.bazel_metadata.clone(),
215
1
            identity: origin_metadata.identity.clone(),
216
1
            event: Some(event),
217
1
        };
218
219
        // Awaited send (not try_send): backpressure rather than drop, so the
220
        // start-execute event that later resource-usage events reference as
221
        // their parent isn't silently lost when the queue is full.
222
1
        if let Err(
err0
) = origin_event_tx.send(origin_event).await {
223
0
            warn!(
224
                ?err,
225
                "Failed to publish scheduler start execute origin event"
226
            );
227
1
        }
228
1
    }
229
230
    /// Attempts to find a worker to execute an action and begins executing it.
231
    /// If an action is already running that is cacheable it may merge this
232
    /// action with the results and state changes of the already running
233
    /// action. If the task cannot be executed immediately it will be queued
234
    /// for execution based on priority and other metrics.
235
    /// All further updates to the action will be provided through the returned
236
    /// value.
237
41
    async fn inner_add_action(
238
41
        &self,
239
41
        client_operation_id: OperationId,
240
41
        action_info: Arc<ActionInfo>,
241
41
    ) -> Result<Box<dyn ActionStateResult>, Error> {
242
41
        let action_state_result = self
243
41
            .client_state_manager
244
41
            .add_action(client_operation_id.clone(), action_info)
245
41
            .await
246
41
            .err_tip(|| "In SimpleScheduler::add_action")
?0
;
247
41
        Ok(Box::new(SimpleSchedulerActionStateResult::new(
248
41
            client_operation_id.clone(),
249
41
            action_state_result,
250
41
        )))
251
41
    }
252
253
508
    async fn inner_filter_operations(
254
508
        &self,
255
508
        filter: OperationFilter,
256
508
    ) -> Result<ActionStateResultStream<'_>, Error> {
257
508
        self.client_state_manager
258
508
            .filter_operations(filter)
259
508
            .await
260
508
            .err_tip(|| "In SimpleScheduler::find_by_client_operation_id getting filter result")
261
508
    }
262
263
150
    async fn get_queued_operations(&self) -> Result<ActionStateResultStream<'_>, Error> {
264
150
        let filter = OperationFilter {
265
150
            stages: OperationStageFlags::Queued,
266
150
            order_by_priority_direction: Some(OrderDirection::Desc),
267
150
            ..Default::default()
268
150
        };
269
150
        self.matching_engine_state_manager
270
150
            .filter_operations(filter)
271
150
            .await
272
150
            .err_tip(|| "In SimpleScheduler::get_queued_operations getting filter result")
273
150
    }
274
275
13
    
pub async fn do_try_match_for_test(&self) -> Result<(), Error>0
{
276
13
        self.do_try_match(true).await
277
13
    }
278
279
    // TODO(palfrey) This is an O(n*m) (aka n^2) algorithm. In theory we
280
    // can create a map of capabilities of each worker and then try and match
281
    // the actions to the worker using the map lookup (ie. map reduce).
282
150
    async fn do_try_match(&self, full_worker_logging: bool) -> Result<(), Error> {
283
150
        let match_started = Instant::now();
284
150
        let result = self.do_try_match_inner(full_worker_logging).await;
285
150
        record_matching_pass(match_started.elapsed().as_secs_f64(), result.is_ok());
286
150
        result
287
150
    }
288
289
150
    async fn do_try_match_inner(&self, full_worker_logging: bool) -> Result<(), Error> {
290
70
        async fn match_action_to_worker(
291
70
            action_state_result: &dyn ActionStateResult,
292
70
            workers: &ApiWorkerScheduler,
293
70
            matching_engine_state_manager: &dyn MatchingEngineStateManager,
294
70
            platform_property_manager: &PlatformPropertyManager,
295
70
            maybe_origin_event_tx: Option<&mpsc::Sender<OriginEvent>>,
296
70
            full_worker_logging: bool,
297
70
        ) -> Result<(), Error> {
298
70
            let (action_info, maybe_origin_metadata) =
299
70
                action_state_result
300
70
                    .as_action_info()
301
70
                    .await
302
70
                    .err_tip(|| "Failed to get action_info from as_action_info_result stream")
?0
;
303
304
            // TODO(palfrey) We should not compute this every time and instead store
305
            // it with the ActionInfo when we receive it.
306
70
            let platform_properties = platform_property_manager
307
70
                .make_platform_properties(action_info.platform_properties.clone())
308
70
                .err_tip(
309
                    || "Failed to make platform properties in SimpleScheduler::do_try_match",
310
0
                )?;
311
312
70
            let origin_metadata = maybe_origin_metadata.unwrap_or_default();
313
70
            let action_info = ActionInfoWithProps {
314
70
                inner: action_info,
315
70
                platform_properties,
316
70
                origin_metadata: origin_metadata.clone(),
317
70
                scheduler_start_execute_event_id: None,
318
70
            };
319
320
            // Try to find a worker for the action.
321
47
            let worker_id = {
322
70
                match workers
323
70
                    .find_worker_for_action(&action_info.platform_properties, full_worker_logging)
324
70
                    .await
325
                {
326
47
                    Some(worker_id) => worker_id,
327
                    // If we could not find a worker for the action,
328
                    // we have nothing to do.
329
23
                    None => return Ok(()),
330
                }
331
            };
332
333
47
            let event_origin_metadata = origin_metadata.clone();
334
47
            let attach_operation_fut = async move {
335
                // Extract the operation_id from the action_state.
336
47
                let operation_id = {
337
47
                    let (action_state, _origin_metadata) = action_state_result
338
47
                        .as_state()
339
47
                        .await
340
47
                        .err_tip(|| "Failed to get action_info from as_state_result stream")
?0
;
341
47
                    action_state.client_operation_id.clone()
342
                };
343
344
                // Tell the matching engine that the operation is being assigned to a worker.
345
47
                let assign_result = matching_engine_state_manager
346
47
                    .assign_operation(&operation_id, Ok(&worker_id))
347
47
                    .await
348
47
                    .err_tip(|| "Failed to assign operation in do_try_match");
349
47
                if let Err(
err5
) = assign_result {
350
5
                    if err.code == Code::Aborted {
351
                        // If the operation was aborted, it means that the operation was
352
                        // cancelled due to another operation being assigned to the worker.
353
4
                        return Ok(());
354
1
                    }
355
                    // Any other error is a real error.
356
1
                    return Err(err);
357
42
                }
358
359
42
                let mut action_info = action_info;
360
42
                let scheduler_start_execute_event = maybe_origin_event_tx.map(|_| 
{1
361
1
                    let event = SimpleScheduler::scheduler_start_execute_event(
362
1
                        &worker_id,
363
1
                        &operation_id,
364
1
                        &action_info,
365
                    );
366
1
                    let event_id = SimpleScheduler::origin_event_id(&event);
367
1
                    action_info.scheduler_start_execute_event_id = Some(event_id.clone());
368
1
                    (event_id, event)
369
1
                });
370
371
42
                debug!(%worker_id, %operation_id, ?action_info, "Notifying worker of operation");
372
42
                workers
373
42
                    .worker_notify_run_action(worker_id, operation_id, action_info)
374
42
                    .await
375
42
                    .err_tip(|| 
{1
376
1
                        "Failed to run worker_notify_run_action in SimpleScheduler::do_try_match"
377
1
                    })?;
378
379
41
                if let Some((
event_id1
,
event1
)) = scheduler_start_execute_event {
380
1
                    SimpleScheduler::publish_scheduler_start_execute(
381
1
                        maybe_origin_event_tx,
382
1
                        &event_origin_metadata,
383
1
                        event_id,
384
1
                        event,
385
1
                    )
386
1
                    .await;
387
40
                }
388
389
41
                Ok(())
390
47
            };
391
47
            tokio::pin!(attach_operation_fut);
392
393
47
            let ctx = Context::current_with_baggage(vec![KeyValue::new(
394
                ENDUSER_ID,
395
47
                origin_metadata.identity,
396
            )]);
397
398
47
            info_span!("do_try_match")
399
47
                .in_scope(|| attach_operation_fut)
400
47
                .with_context(ctx)
401
47
                .await
402
70
        }
403
404
150
        let mut result = Ok(());
405
406
150
        let start = Instant::now();
407
408
150
        let mut stream = self
409
150
            .get_queued_operations()
410
150
            .await
411
150
            .err_tip(|| "Failed to get queued operations in do_try_match")
?0
;
412
413
150
        let query_elapsed = start.elapsed();
414
150
        if query_elapsed > Duration::from_secs(1) {
415
0
            warn!(
416
0
                elapsed_ms = query_elapsed.as_millis(),
417
                "Slow get_queued_operations query"
418
            );
419
150
        }
420
421
220
        while let Some(
action_state_result70
) = stream.next().await {
422
70
            result = result.merge(
423
70
                match_action_to_worker(
424
70
                    action_state_result.as_ref(),
425
70
                    self.worker_scheduler.as_ref(),
426
70
                    self.matching_engine_state_manager.as_ref(),
427
70
                    self.platform_property_manager.as_ref(),
428
70
                    self.maybe_origin_event_tx.as_ref(),
429
70
                    full_worker_logging,
430
70
                )
431
70
                .await,
432
            );
433
        }
434
435
150
        let total_elapsed = start.elapsed();
436
150
        if total_elapsed > Duration::from_secs(5) {
437
0
            warn!(
438
0
                total_ms = total_elapsed.as_millis(),
439
0
                query_ms = query_elapsed.as_millis(),
440
                "Slow do_try_match cycle"
441
            );
442
150
        }
443
444
150
        result
445
150
    }
446
}
447
448
impl SimpleScheduler {
449
1
    pub fn new<A: AwaitedActionDb>(
450
1
        spec: &SimpleSpec,
451
1
        awaited_action_db: A,
452
1
        task_change_notify: Arc<Notify>,
453
1
        maybe_origin_event_tx: Option<mpsc::Sender<OriginEvent>>,
454
1
    ) -> (Arc<Self>, Arc<dyn WorkerScheduler>) {
455
1
        Self::new_with_callback(
456
1
            spec,
457
1
            awaited_action_db,
458
0
            || {
459
                // The cost of running `do_try_match()` is very high, but constant
460
                // in relation to the number of changes that have happened. This
461
                // means that grabbing this lock to process `do_try_match()` should
462
                // always yield to any other tasks that might want the lock. The
463
                // easiest and most fair way to do this is to sleep for a small
464
                // amount of time. Using something like tokio::task::yield_now()
465
                // does not yield as aggressively as we'd like if new futures are
466
                // scheduled within a future.
467
0
                tokio::time::sleep(Duration::from_millis(1))
468
0
            },
469
1
            task_change_notify,
470
            SystemTime::now,
471
1
            maybe_origin_event_tx,
472
        )
473
1
    }
474
475
33
    pub fn new_with_callback<
476
33
        Fut: Future<Output = ()> + Send,
477
33
        F: Fn() -> Fut + Send + Sync + 'static,
478
33
        A: AwaitedActionDb,
479
33
        I: InstantWrapper,
480
33
        NowFn: Fn() -> I + Clone + Send + Unpin + Sync + 'static,
481
33
    >(
482
33
        spec: &SimpleSpec,
483
33
        mut awaited_action_db: A,
484
33
        on_matching_engine_run: F,
485
33
        task_change_notify: Arc<Notify>,
486
33
        now_fn: NowFn,
487
33
        maybe_origin_event_tx: Option<mpsc::Sender<OriginEvent>>,
488
33
    ) -> (Arc<Self>, Arc<dyn WorkerScheduler>) {
489
33
        let platform_property_manager = Arc::new(PlatformPropertyManager::new(
490
33
            spec.supported_platform_properties
491
33
                .clone()
492
33
                .unwrap_or_default(),
493
        ));
494
495
33
        let mut worker_timeout_s = spec.worker_timeout_s;
496
33
        if worker_timeout_s == 0 {
497
28
            worker_timeout_s = DEFAULT_WORKER_TIMEOUT_S;
498
28
        
}5
499
500
33
        let mut client_action_timeout_s = spec.client_action_timeout_s;
501
33
        if client_action_timeout_s == 0 {
502
32
            client_action_timeout_s = DEFAULT_CLIENT_ACTION_TIMEOUT_S;
503
32
        
}1
504
        // This matches the value of CLIENT_KEEPALIVE_DURATION which means that
505
        // tasks are going to be dropped all over the place, this isn't a good
506
        // setting.
507
33
        if client_action_timeout_s <= CLIENT_KEEPALIVE_DURATION.as_secs() {
508
0
            error!(
509
                client_action_timeout_s,
510
                "Setting client_action_timeout_s to less than the client keep alive interval is going to cause issues, please set above {}.",
511
0
                CLIENT_KEEPALIVE_DURATION.as_secs()
512
            );
513
33
        }
514
515
33
        let mut max_job_retries = spec.max_job_retries;
516
33
        if max_job_retries == 0 {
517
31
            max_job_retries = DEFAULT_MAX_JOB_RETRIES;
518
31
        
}2
519
520
33
        let mut unacknowledged_kill_timeout_s = spec.unacknowledged_kill_timeout_s;
521
33
        if unacknowledged_kill_timeout_s == 0 {
522
33
            unacknowledged_kill_timeout_s = DEFAULT_UNACKNOWLEDGED_KILL_TIMEOUT_S;
523
33
        
}0
524
525
33
        let worker_change_notify = Arc::new(Notify::new());
526
527
        // Create shared worker registry for single heartbeat per worker.
528
33
        let worker_registry = Arc::new(WorkerRegistry::new());
529
530
        // The db decides on its own whether an executing action was abandoned,
531
        // so it needs the same liveness view the state manager uses.
532
33
        awaited_action_db.set_worker_registry(worker_registry.clone());
533
534
33
        let state_manager = SimpleSchedulerStateManager::new(
535
33
            max_job_retries,
536
33
            Duration::from_secs(worker_timeout_s),
537
33
            Duration::from_secs(client_action_timeout_s),
538
33
            Duration::from_secs(spec.max_action_executing_timeout_s),
539
33
            awaited_action_db,
540
33
            now_fn,
541
33
            Some(worker_registry.clone()),
542
        );
543
544
33
        let worker_scheduler = ApiWorkerScheduler::new(
545
33
            state_manager.clone(),
546
33
            platform_property_manager.clone(),
547
33
            spec.allocation_strategy,
548
33
            worker_change_notify.clone(),
549
33
            worker_timeout_s,
550
33
            unacknowledged_kill_timeout_s,
551
33
            worker_registry,
552
33
            maybe_origin_event_tx.clone(),
553
        );
554
555
33
        let worker_scheduler_clone = worker_scheduler.clone();
556
557
33
        let fallback_match_interval = match spec.fallback_match_interval_s {
558
            // Zero or any negative value means disabled.
559
33
            ..=0 => 
None32
,
560
1
            secs => Some(Duration::from_secs(secs.unsigned_abs())),
561
        };
562
563
33
        let action_scheduler = Arc::new_cyclic(move |weak_self| -> Self {
564
33
            let weak_inner = weak_self.clone();
565
33
            let task_worker_matching_spawn =
566
33
                spawn!("simple_scheduler_task_worker_matching", async move 
{31
567
31
                    let mut last_match_successful = true;
568
31
                    let mut worker_match_logging_last: Option<Instant> = None;
569
                    // Break out of the loop only when the inner is dropped.
570
                    loop {
571
168
                        let task_change_fut = task_change_notify.notified();
572
168
                        let worker_change_fut = worker_change_notify.notified();
573
168
                        tokio::pin!(task_change_fut);
574
168
                        tokio::pin!(worker_change_fut);
575
                        // Wait for either of these futures to be ready.
576
168
                        let state_changed = future::select(task_change_fut, worker_change_fut);
577
168
                        let max_wait = if last_match_successful {
578
                            // Even on success, periodically re-run the match as a
579
                            // fallback for missed notifications and eventually
580
                            // consistent backends (e.g. a re-queued operation that
581
                            // was not yet visible to the search triggered by its
582
                            // own notification). Without this, such an operation
583
                            // can stay queued until an unrelated event triggers
584
                            // another matching pass.
585
167
                            fallback_match_interval
586
                        } else {
587
                            // If the last match failed, then run again after a short sleep.
588
                            // This resolves issues where we tried to re-schedule a job to
589
                            // a disconnected worker.  The sleep ensures we don't enter a
590
                            // hard loop if there's something wrong inside do_try_match.
591
1
                            Some(Duration::from_millis(100))
592
                        };
593
168
                        if let Some(
max_wait6
) = max_wait {
594
6
                            let sleep_fut = tokio::time::sleep(max_wait);
595
6
                            tokio::pin!(sleep_fut);
596
6
                            let _ = future::select(state_changed, sleep_fut).await;
597
                        } else {
598
162
                            let _ = state_changed.await;
599
                        }
600
601
137
                        let result = match weak_inner.upgrade() {
602
137
                            Some(scheduler) => {
603
137
                                let now = Instant::now();
604
137
                                let full_worker_logging = {
605
137
                                    match scheduler.worker_match_logging_interval {
606
132
                                        None => false,
607
5
                                        Some(duration) => match worker_match_logging_last {
608
2
                                            None => true,
609
3
                                            Some(when) => now.duration_since(when) >= duration,
610
                                        },
611
                                    }
612
                                };
613
614
137
                                let res = scheduler.do_try_match(full_worker_logging).await;
615
137
                                if full_worker_logging {
616
2
                                    let operations_stream = scheduler
617
2
                                        .matching_engine_state_manager
618
2
                                        .filter_operations(OperationFilter::default())
619
2
                                        .await
620
2
                                        .err_tip(|| "In action_scheduler getting filter result");
621
622
2
                                    let mut oldest_actions_in_state: HashMap<
623
2
                                        String,
624
2
                                        BTreeSet<Arc<ActionState>>,
625
2
                                    > = HashMap::new();
626
2
                                    let max_items = 5;
627
628
2
                                    match operations_stream {
629
2
                                        Ok(stream) => {
630
2
                                            let actions = stream
631
2
                                                .filter_map(|item| async move 
{0
632
0
                                                    match item.as_ref().as_state().await {
633
0
                                                        Ok((action_state, _origin_metadata)) => {
634
0
                                                            Some(action_state)
635
                                                        }
636
0
                                                        Err(e) => {
637
0
                                                            error!(
638
                                                                ?e,
639
                                                                "Failed to get action state!"
640
                                                            );
641
0
                                                            None
642
                                                        }
643
                                                    }
644
0
                                                })
645
2
                                                .collect::<Vec<_>>()
646
2
                                                .await;
647
2
                                            for 
action_state0
in &actions {
648
0
                                                let name = action_state.stage.name();
649
0
                                                if let Some(values) =
650
0
                                                    oldest_actions_in_state.get_mut(&name)
651
                                                {
652
0
                                                    values.insert(action_state.clone());
653
0
                                                    if values.len() > max_items {
654
0
                                                        values.pop_first();
655
0
                                                    }
656
0
                                                } else {
657
0
                                                    let mut values = BTreeSet::new();
658
0
                                                    values.insert(action_state.clone());
659
0
                                                    oldest_actions_in_state.insert(name, values);
660
0
                                                }
661
                                            }
662
                                        }
663
0
                                        Err(e) => {
664
0
                                            error!(?e, "Failed to get operations list!");
665
                                        }
666
                                    }
667
668
2
                                    for 
value0
in oldest_actions_in_state.values() {
669
0
                                        let mut items = vec![];
670
0
                                        for item in value {
671
0
                                            items.push(item.to_string());
672
0
                                        }
673
0
                                        info!(?items, "Oldest actions in state");
674
                                    }
675
676
2
                                    worker_match_logging_last.replace(now);
677
135
                                }
678
137
                                res
679
                            }
680
                            // If the inner went away it means the scheduler is shutting
681
                            // down, so we need to resolve our future.
682
0
                            None => return,
683
                        };
684
137
                        last_match_successful = result.is_ok();
685
137
                        if let Err(
err1
) = result {
686
1
                            error!(?err, "Error while running do_try_match");
687
136
                        }
688
689
137
                        on_matching_engine_run().await;
690
                    }
691
                    // Unreachable.
692
0
                });
693
694
33
            let worker_match_logging_interval = match spec.worker_match_logging_interval_s {
695
                // -1 or 0 means disabled (0 used to cause expensive logging on every call)
696
30
                -1 | 0 => None,
697
3
                signed_secs => {
698
3
                    if let Ok(
secs2
) = TryInto::<u64>::try_into(signed_secs) {
699
2
                        Some(Duration::from_secs(secs))
700
                    } else {
701
1
                        error!(
702
                            worker_match_logging_interval_s = spec.worker_match_logging_interval_s,
703
                            "Valid values for worker_match_logging_interval_s are -1, 0, or a positive integer, setting to disabled",
704
                        );
705
1
                        None
706
                    }
707
                }
708
            };
709
33
            Self {
710
33
                matching_engine_state_manager: state_manager.clone(),
711
33
                client_state_manager: state_manager.clone(),
712
33
                worker_scheduler,
713
33
                platform_property_manager,
714
33
                maybe_origin_event_tx,
715
33
                task_worker_matching_spawn,
716
33
                worker_match_logging_interval,
717
33
            }
718
33
        });
719
33
        (action_scheduler, worker_scheduler_clone)
720
33
    }
721
}
722
723
#[async_trait]
724
impl ClientStateManager for SimpleScheduler {
725
    async fn add_action(
726
        &self,
727
        client_operation_id: OperationId,
728
        action_info: Arc<ActionInfo>,
729
41
    ) -> Result<Box<dyn ActionStateResult>, Error> {
730
        self.inner_add_action(client_operation_id, action_info)
731
            .await
732
41
    }
733
734
    async fn filter_operations<'a>(
735
        &'a self,
736
        filter: OperationFilter,
737
508
    ) -> Result<ActionStateResultStream<'a>, Error> {
738
        self.inner_filter_operations(filter).await
739
508
    }
740
}
741
742
#[async_trait]
743
impl KnownPlatformPropertyProvider for SimpleScheduler {
744
0
    async fn get_known_properties(&self, _instance_name: &str) -> Result<Vec<String>, Error> {
745
        Ok(self
746
            .worker_scheduler
747
            .get_platform_property_manager()
748
            .get_known_properties()
749
            .keys()
750
            .cloned()
751
            .collect())
752
0
    }
753
}
754
755
#[async_trait]
756
impl WorkerScheduler for SimpleScheduler {
757
0
    fn get_platform_property_manager(&self) -> &PlatformPropertyManager {
758
0
        self.worker_scheduler.get_platform_property_manager()
759
0
    }
760
761
39
    async fn add_worker(&self, worker: Worker) -> Result<(), Error> {
762
        self.worker_scheduler.add_worker(worker).await
763
39
    }
764
765
    async fn update_action(
766
        &self,
767
        worker_id: &WorkerId,
768
        operation_id: &OperationId,
769
        update: UpdateOperationType,
770
13
    ) -> Result<(), Error> {
771
        self.worker_scheduler
772
            .update_action(worker_id, operation_id, update)
773
            .await
774
13
    }
775
776
    async fn worker_keep_alive_received(
777
        &self,
778
        worker_id: &WorkerId,
779
        timestamp: WorkerTimestamp,
780
3
    ) -> Result<(), Error> {
781
        self.worker_scheduler
782
            .worker_keep_alive_received(worker_id, timestamp)
783
            .await
784
3
    }
785
786
5
    async fn remove_worker(&self, worker_id: &WorkerId) -> Result<(), Error> {
787
        self.worker_scheduler.remove_worker(worker_id).await
788
5
    }
789
790
0
    async fn shutdown(&self, shutdown_guard: ShutdownGuard) {
791
        self.worker_scheduler.shutdown(shutdown_guard).await;
792
0
    }
793
794
3
    async fn remove_timedout_workers(&self, now_timestamp: WorkerTimestamp) -> Result<(), Error> {
795
        self.worker_scheduler
796
            .remove_timedout_workers(now_timestamp)
797
            .await
798
3
    }
799
800
2
    async fn set_drain_worker(&self, worker_id: &WorkerId, is_draining: bool) -> Result<(), Error> {
801
        self.worker_scheduler
802
            .set_drain_worker(worker_id, is_draining)
803
            .await
804
2
    }
805
806
7
    async fn kill_revoked_operations(&self) -> Result<(), Error> {
807
        self.worker_scheduler.kill_revoked_operations().await
808
7
    }
809
}
810
811
impl RootMetricsComponent for SimpleScheduler {}