Coverage Report

Created: 2026-11-13 23:29

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-service/src/worker_api_server.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::convert::Into;
16
use core::pin::Pin;
17
use core::time::Duration;
18
use std::collections::HashMap;
19
use std::sync::Arc;
20
use std::time::{SystemTime, UNIX_EPOCH};
21
22
use futures::stream::unfold;
23
use futures::{Stream, StreamExt};
24
use nativelink_config::cas_server::WorkerApiConfig;
25
use nativelink_error::{make_err, Code, Error, ResultExt};
26
use nativelink_proto::com::github::trace_machina::nativelink::remote_execution::update_for_scheduler::Update;
27
use nativelink_proto::com::github::trace_machina::nativelink::remote_execution::worker_api_server::{
28
    WorkerApi, WorkerApiServer as Server,
29
};
30
use nativelink_proto::com::github::trace_machina::nativelink::remote_execution::{
31
    execute_declined, execute_result, ExecuteAccepted, ExecuteComplete, ExecuteDeclined, ExecuteResult, GoingAwayRequest, KeepAliveRequest, UpdateForScheduler, UpdateForWorker
32
};
33
use nativelink_scheduler::worker::Worker;
34
use nativelink_scheduler::worker_scheduler::WorkerScheduler;
35
use nativelink_util::background_spawn;
36
use nativelink_util::action_messages::{OperationId, WorkerId};
37
use nativelink_util::operation_state_manager::UpdateOperationType;
38
use nativelink_util::platform_properties::PlatformProperties;
39
use rand::RngCore;
40
use tokio::sync::mpsc;
41
use tokio::time::interval;
42
use tonic::{Response, Status};
43
use tracing::{debug, error, warn, instrument, Level};
44
use uuid::Uuid;
45
46
pub type ConnectWorkerStream =
47
    Pin<Box<dyn Stream<Item = Result<UpdateForWorker, Status>> + Send + Sync + 'static>>;
48
49
pub type NowFn = Box<dyn Fn() -> Result<Duration, Error> + Send + Sync>;
50
51
/// Makes the unique part of a new worker's id.
52
pub type WorkerIdFn = Box<dyn Fn() -> String + Send + Sync>;
53
54
/// How often workers are told to kill operations the scheduler no longer
55
/// has executing on them, unless overridden by
56
/// `kill_revoked_operations_interval_s` in the worker API config.
57
const DEFAULT_KILL_REVOKED_OPERATIONS_INTERVAL_S: u64 = 5;
58
59
pub struct WorkerApiServer {
60
    scheduler: Arc<dyn WorkerScheduler>,
61
    now_fn: Arc<NowFn>,
62
    node_id: [u8; 6],
63
    worker_id_fn: Option<WorkerIdFn>,
64
}
65
66
impl core::fmt::Debug for WorkerApiServer {
67
0
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
68
0
        f.debug_struct("WorkerApiServer")
69
0
            .field("node_id", &self.node_id)
70
0
            .finish_non_exhaustive()
71
0
    }
72
}
73
74
impl WorkerApiServer {
75
0
    pub fn new(
76
0
        config: &WorkerApiConfig,
77
0
        schedulers: &HashMap<String, Arc<dyn WorkerScheduler>>,
78
0
    ) -> Result<Self, Error> {
79
0
        let node_id = {
80
0
            let mut out = [0; 6];
81
0
            rand::rng().fill_bytes(&mut out);
82
0
            out
83
        };
84
0
        let now_fn: Arc<NowFn> = Arc::new(Box::new(move || {
85
0
            SystemTime::now().duration_since(UNIX_EPOCH).map_err(|err| {
86
0
                Error::from_std_err(Code::Internal, &err)
87
0
                    .append("System time is now behind unix epoch")
88
0
            })
89
0
        }));
90
0
        Self::spawn_timers(config, schedulers, &now_fn);
91
0
        Self::with_shared_now_fn(config, schedulers, now_fn, node_id)
92
0
    }
93
94
    /// Starts the worker timeout and revoked-operation timers for every
95
    /// scheduler, reading the time from `now_fn`. `new()` calls it; a server
96
    /// built with `new_with_now_fn()` has no timers until this is called.
97
188
    pub fn spawn_timers(
98
188
        config: &WorkerApiConfig,
99
188
        schedulers: &HashMap<String, Arc<dyn WorkerScheduler>>,
100
188
        now_fn: &Arc<NowFn>,
101
188
    ) {
102
188
        let kill_revoked_enabled = !config.disable_kill_revoked_operations;
103
188
        let kill_revoked_interval_s = if config.kill_revoked_operations_interval_s == 0 {
104
188
            DEFAULT_KILL_REVOKED_OPERATIONS_INTERVAL_S
105
        } else {
106
0
            config.kill_revoked_operations_interval_s
107
        };
108
188
        for scheduler in schedulers.values() {
109
            // This will protect us from holding a reference to the scheduler forever in the
110
            // event our ExecutionServer dies. Our scheduler is a weak ref, so the spawn will
111
            // eventually see the Arc went away and return.
112
188
            let weak_scheduler = Arc::downgrade(scheduler);
113
188
            let now_fn = now_fn.clone();
114
188
            background_spawn!("worker_api_server", async move {
115
188
                let mut timeout_ticker = interval(Duration::from_secs(1));
116
188
                let mut kill_ticker = interval(Duration::from_secs(kill_revoked_interval_s));
117
                loop {
118
12.6k
                    tokio::select! {
119
12.6k
                        _ = timeout_ticker.tick() => {
120
10.2k
                            let timestamp = match now_fn() {
121
10.2k
                                Ok(timestamp) => timestamp,
122
0
                                Err(err) => {
123
0
                                    error!(?err, "Failed to read the time for remove_timedout_workers");
124
0
                                    continue;
125
                                }
126
                            };
127
10.2k
                            match weak_scheduler.upgrade() {
128
10.2k
                                Some(scheduler) => {
129
0
                                    if let Err(err) =
130
10.2k
                                        scheduler.remove_timedout_workers(timestamp.as_secs()).await
131
                                    {
132
0
                                        error!(?err, "Failed to remove_timedout_workers",);
133
10.2k
                                    }
134
                                }
135
                                // If we fail to upgrade, our service is probably destroyed, so return.
136
5
                                None => return,
137
                            }
138
                        }
139
12.6k
                        _ = kill_ticker.tick(), if kill_revoked_enabled => {
140
2.15k
                            match weak_scheduler.upgrade() {
141
2.15k
                                Some(scheduler) => {
142
2.15k
                                    if let Err(
err0
) = scheduler.kill_revoked_operations().await {
143
0
                                        error!(?err, "Failed to kill_revoked_operations");
144
2.15k
                                    }
145
                                }
146
0
                                None => return,
147
                            }
148
                        }
149
                    }
150
                }
151
5
            });
152
        }
153
188
    }
154
155
    /// Same as `new()`, but you can pass a custom `now_fn`, that returns a Duration since `UNIX_EPOCH`
156
    /// representing the current time. Used mostly in  unit tests.
157
206
    pub fn new_with_now_fn(
158
206
        config: &WorkerApiConfig,
159
206
        schedulers: &HashMap<String, Arc<dyn WorkerScheduler>>,
160
206
        now_fn: NowFn,
161
206
        node_id: [u8; 6],
162
206
    ) -> Result<Self, Error> {
163
206
        Self::with_shared_now_fn(config, schedulers, Arc::new(now_fn), node_id)
164
206
    }
165
166
206
    fn with_shared_now_fn(
167
206
        config: &WorkerApiConfig,
168
206
        schedulers: &HashMap<String, Arc<dyn WorkerScheduler>>,
169
206
        now_fn: Arc<NowFn>,
170
206
        node_id: [u8; 6],
171
206
    ) -> Result<Self, Error> {
172
206
        let scheduler = schedulers
173
206
            .get(&config.scheduler)
174
206
            .err_tip(|| 
{0
175
0
                format!(
176
                    "Scheduler needs config for '{}' because it exists in worker_api",
177
                    config.scheduler
178
                )
179
0
            })?
180
206
            .clone();
181
206
        Ok(Self {
182
206
            scheduler,
183
206
            now_fn,
184
206
            node_id,
185
206
            worker_id_fn: None,
186
206
        })
187
206
    }
188
189
    /// Takes the unique part of each worker id from `worker_id_fn` instead
190
    /// of a time-based UUID, for tests that need the same ids every run.
191
    #[must_use]
192
188
    pub fn with_worker_id_fn(mut self, worker_id_fn: WorkerIdFn) -> Self {
193
188
        self.worker_id_fn = Some(worker_id_fn);
194
188
        self
195
188
    }
196
197
0
    pub fn into_service(self) -> Server<Self> {
198
0
        Server::new(self)
199
0
    }
200
201
2.00k
    async fn inner_connect_worker(
202
2.00k
        &self,
203
2.00k
        mut update_stream: impl Stream<Item = Result<UpdateForScheduler, Status>>
204
2.00k
        + Unpin
205
2.00k
        + Send
206
2.00k
        + 'static,
207
2.00k
    ) -> Result<Response<ConnectWorkerStream>, Error> {
208
2.00k
        let first_message = update_stream
209
2.00k
            .next()
210
2.00k
            .await
211
2.00k
            .err_tip(|| "Missing first message for connect_worker")
?0
212
2.00k
            .err_tip(|| "Error reading first message for connect_worker")
?0
;
213
2.00k
        let Some(Update::ConnectWorkerRequest(connect_worker_request)) = first_message.update
214
        else {
215
0
            return Err(make_err!(
216
0
                Code::Internal,
217
0
                "First message was not a ConnectWorkerRequest"
218
0
            ));
219
        };
220
221
2.00k
        let (tx, rx) = mpsc::channel(nativelink_scheduler::worker::channel_capacity(
222
2.00k
            connect_worker_request.max_inflight_tasks,
223
2.00k
        ));
224
225
        // First convert our proto platform properties into one our scheduler understands.
226
2.00k
        let platform_properties = {
227
2.00k
            let mut platform_properties = PlatformProperties::default();
228
2.00k
            for 
property7
in connect_worker_request.properties {
229
7
                let platform_property_value = self
230
7
                    .scheduler
231
7
                    .get_platform_property_manager()
232
7
                    .make_prop_value(&property.name, &property.value)
233
7
                    .err_tip(|| "Bad Property during connect_worker()")
?0
;
234
7
                platform_properties
235
7
                    .properties
236
7
                    .insert(property.name.clone(), platform_property_value);
237
            }
238
2.00k
            platform_properties
239
        };
240
241
        // Now register the worker with the scheduler.
242
2.00k
        let worker_id = {
243
2.00k
            let unique = self.worker_id_fn.as_ref().map_or_else(
244
18
                || Uuid::now_v6(&self.node_id).hyphenated().to_string(),
245
1.99k
                |worker_id_fn| worker_id_fn(),
246
            );
247
2.00k
            let worker_id = WorkerId(format!(
248
2.00k
                "{}{unique}",
249
2.00k
                connect_worker_request.worker_id_prefix,
250
2.00k
            ));
251
2.00k
            let mut worker = Worker::new(
252
2.00k
                worker_id.clone(),
253
2.00k
                platform_properties,
254
2.00k
                tx,
255
2.00k
                (self.now_fn)()
?0
.as_secs(),
256
2.00k
                connect_worker_request.max_inflight_tasks,
257
            );
258
2.00k
            worker.admits_when_idle = connect_worker_request.admits_when_idle;
259
2.00k
            self.scheduler
260
2.00k
                .add_worker(worker)
261
2.00k
                .await
262
2.00k
                .err_tip(|| "Failed to add worker in inner_connect_worker()")
?0
;
263
2.00k
            worker_id
264
        };
265
266
2.00k
        WorkerConnection::start(
267
2.00k
            self.scheduler.clone(),
268
2.00k
            self.now_fn.clone(),
269
2.00k
            worker_id.clone(),
270
2.00k
            update_stream,
271
        );
272
273
2.00k
        Ok(Response::new(Box::pin(unfold(
274
2.00k
            (rx, worker_id),
275
15.0k
            move |state| async move {
276
15.0k
                let (mut rx, worker_id) = state;
277
15.0k
                if let Some(
update_for_worker13.0k
) = rx.recv().await {
278
13.0k
                    return Some((Ok(update_for_worker), (rx, worker_id)));
279
327
                }
280
327
                warn!(
281
                    ?worker_id,
282
                    "UpdateForWorker channel was closed, thus closing connection to worker node",
283
                );
284
285
327
                None
286
28.4k
            },
287
        ))))
288
2.00k
    }
289
290
2.00k
    pub async fn inner_connect_worker_for_testing(
291
2.00k
        &self,
292
2.00k
        update_stream: impl Stream<Item = Result<UpdateForScheduler, Status>> + Unpin + Send + 'static,
293
2.00k
    ) -> Result<Response<ConnectWorkerStream>, Error> {
294
2.00k
        self.inner_connect_worker(update_stream).await
295
2.00k
    }
296
}
297
298
#[tonic::async_trait]
299
impl WorkerApi for WorkerApiServer {
300
    type ConnectWorkerStream = ConnectWorkerStream;
301
302
    #[instrument(
303
        err,
304
        level = Level::ERROR,
305
        skip_all,
306
        fields(request = ?grpc_request.get_ref())
307
    )]
308
    async fn connect_worker(
309
        &self,
310
        grpc_request: tonic::Request<tonic::Streaming<UpdateForScheduler>>,
311
    ) -> Result<Response<Self::ConnectWorkerStream>, Status> {
312
        let resp = self
313
            .inner_connect_worker(grpc_request.into_inner())
314
            .await
315
            .map_err(Into::into);
316
        if resp.is_ok() {
317
            debug!(return = "Ok(<stream>)");
318
        }
319
        resp
320
    }
321
}
322
323
struct WorkerConnection {
324
    scheduler: Arc<dyn WorkerScheduler>,
325
    now_fn: Arc<NowFn>,
326
    worker_id: WorkerId,
327
}
328
329
impl WorkerConnection {
330
2.00k
    fn start(
331
2.00k
        scheduler: Arc<dyn WorkerScheduler>,
332
2.00k
        now_fn: Arc<NowFn>,
333
2.00k
        worker_id: WorkerId,
334
2.00k
        mut connection: impl Stream<Item = Result<UpdateForScheduler, Status>> + Unpin + Send + 'static,
335
2.00k
    ) {
336
2.00k
        let instance = Self {
337
2.00k
            scheduler,
338
2.00k
            now_fn,
339
2.00k
            worker_id,
340
2.00k
        };
341
342
2.00k
        background_spawn!("worker_api", async move 
{2.00k
343
            // The stream is read by a task of its own that only hands each
344
            // message on; they are processed here, in order. Processing one
345
            // inline in the reader (a result's store write, say) stopped the
346
            // reads for its duration, and a worker that filled the stream's
347
            // flow-control window in the meantime had nothing it was allowed
348
            // to put on the wire: its keepalives waited behind the stall
349
            // until this scheduler evicted it. The queue is unbounded on
350
            // purpose: a worker's messages are bounded by its in-flight
351
            // actions and its keepalive interval, and backpressure here is
352
            // exactly the stall.
353
2.00k
            let (tx, mut rx) = mpsc::unbounded_channel();
354
2.00k
            let reader_worker_id = instance.worker_id.clone();
355
2.00k
            background_spawn!("worker_api_reader", async move {
356
101k
                while let Some(
maybe_update99.8k
) = connection.next().await {
357
99.8k
                    let update = match maybe_update.map(|u| u.update) {
358
99.8k
                        Ok(Some(update)) => update,
359
                        Ok(None) => {
360
0
                            tracing::warn!(worker_id=?reader_worker_id, "Empty update");
361
0
                            continue;
362
                        }
363
0
                        Err(err) => {
364
0
                            tracing::warn!(worker_id=?reader_worker_id, ?err, "Error from worker");
365
0
                            break;
366
                        }
367
                    };
368
99.8k
                    if tx.send(update).is_err() {
369
0
                        break;
370
99.8k
                    }
371
                }
372
873
            });
373
2.00k
            let mut had_going_away = false;
374
99.8k
            while let Some(
update97.9k
) = rx.recv().await {
375
97.9k
                let 
result97.8k
= match update {
376
0
                    Update::ConnectWorkerRequest(_connect_worker_request) => Err(make_err!(
377
0
                        Code::Internal,
378
0
                        "Got ConnectWorkerRequest after initial message for {}",
379
0
                        instance.worker_id
380
0
                    )),
381
66.8k
                    Update::KeepAliveRequest(keep_alive_request) => {
382
66.8k
                        instance.inner_keep_alive(keep_alive_request).await
383
                    }
384
1
                    Update::GoingAwayRequest(going_away_request) => {
385
                        // A drain keeps the worker until its stream closes,
386
                        // so that close still has to remove it.
387
1
                        had_going_away = !going_away_request.drain;
388
1
                        instance.inner_going_away(going_away_request).await
389
                    }
390
10.3k
                    Update::ExecuteResult(execute_result) => {
391
10.3k
                        instance.inner_execution_response(execute_result).await
392
                    }
393
10.0k
                    Update::ExecuteComplete(execute_complete) => {
394
10.0k
                        instance.execution_complete(execute_complete).await
395
                    }
396
10.6k
                    Update::ExecuteAccepted(execute_accepted) => {
397
10.6k
                        instance.dispatch_accepted(execute_accepted).await
398
                    }
399
1
                    Update::ExecuteDeclined(execute_declined) => {
400
1
                        instance.dispatch_declined(execute_declined).await
401
                    }
402
                };
403
97.8k
                if let Err(
err2.54k
) = result {
404
2.54k
                    tracing::warn!(worker_id=?instance.worker_id, ?err, "Error processing worker message");
405
95.2k
                }
406
            }
407
831
            tracing::debug!(worker_id=?instance.worker_id, "Update for scheduler dropped");
408
831
            if !had_going_away {
409
828
                drop(
410
831
                    instance
411
831
                        .scheduler
412
831
                        .worker_disconnected(&instance.worker_id)
413
831
                        .await,
414
                );
415
0
            }
416
828
        });
417
2.00k
    }
418
419
66.8k
    async fn inner_keep_alive(&self, keep_alive_request: KeepAliveRequest) -> Result<(), Error> {
420
66.8k
        self.scheduler
421
66.8k
            .worker_keep_alive_received(
422
66.8k
                &self.worker_id,
423
66.8k
                (self.now_fn)()
?0
.as_secs(),
424
66.8k
                keep_alive_request.load,
425
            )
426
66.8k
            .await
427
66.8k
            .err_tip(|| "Could not process keep_alive from worker in inner_keep_alive()")
?2.53k
;
428
64.2k
        if let Some(
running64.2k
) = keep_alive_request.running_operations {
429
            // The memory first: the running report can release an operation
430
            // the worker left out, and a peak for it is then dropped anyway.
431
64.2k
            let peaks: Vec<(OperationId, u64)> = running
432
64.2k
                .memory
433
64.2k
                .into_iter()
434
64.2k
                .map(|memory| 
{1
435
1
                    (
436
1
                        OperationId::from(memory.operation_id),
437
1
                        memory.peak_memory_kb,
438
1
                    )
439
1
                })
440
64.2k
                .collect();
441
64.2k
            if !peaks.is_empty() {
442
1
                self.scheduler
443
1
                    .worker_operation_memory_reported(&self.worker_id, peaks)
444
1
                    .await
445
1
                    .err_tip(|| "Could not process the memory report in inner_keep_alive()")
?0
;
446
64.2k
            }
447
64.2k
            self.scheduler
448
64.2k
                .worker_running_operations_reported(
449
64.2k
                    &self.worker_id,
450
64.2k
                    running
451
64.2k
                        .operation_ids
452
64.2k
                        .into_iter()
453
64.2k
                        .map(OperationId::from)
454
64.2k
                        .collect(),
455
64.2k
                )
456
64.2k
                .await
457
64.2k
                .err_tip(
458
                    || "Could not process the running operations report in inner_keep_alive()",
459
0
                )?;
460
11
        }
461
64.2k
        Ok(())
462
66.7k
    }
463
464
1
    async fn inner_going_away(&self, going_away_request: GoingAwayRequest) -> Result<(), Error> {
465
1
        if going_away_request.drain {
466
1
            self.scheduler
467
1
                .set_drain_worker(&self.worker_id, true)
468
1
                .await
469
1
                .err_tip(|| "While draining worker in WorkerApiServer::inner_going_away")
?0
;
470
1
            return Ok(());
471
0
        }
472
0
        self.scheduler
473
0
            .remove_worker(&self.worker_id)
474
0
            .await
475
0
            .err_tip(|| "While calling WorkerApiServer::inner_going_away")?;
476
0
        Ok(())
477
1
    }
478
479
    /// Any message from the worker proves it is alive, not only keepalives.
480
    /// Only a keepalive carries a load report and lifts a pause; this
481
    /// refreshes the timestamp alone, so a decline cannot undo the pause
482
    /// it just took.
483
    ///
484
    /// The refresh is best-effort: it is a side effect of the message,
485
    /// never the point of it. A result or completion the worker already
486
    /// produced must reach `update_action` even if the refresh fails, or
487
    /// the operation is stranded in `Executing` with nothing left to
488
    /// finish it.
489
31.0k
    async fn touch_liveness_best_effort(&self) {
490
31.0k
        let result = async {
491
31.0k
            self.scheduler
492
31.0k
                .worker_liveness_refreshed(&self.worker_id, (self.now_fn)()
?0
.as_secs())
493
31.0k
                .await
494
31.0k
        }
495
31.0k
        .await;
496
31.0k
        if let Err(
err2
) = result {
497
2
            warn!(
498
                worker_id = %self.worker_id,
499
                ?err,
500
                "Could not refresh worker liveness; continuing to process the message",
501
            );
502
31.0k
        }
503
31.0k
    }
504
505
10.3k
    async fn inner_execution_response(&self, execute_result: ExecuteResult) -> Result<(), Error> {
506
10.3k
        self.touch_liveness_best_effort().await;
507
10.3k
        let operation_id = OperationId::from(execute_result.operation_id.clone());
508
509
10.3k
        if let Some(
resource_usage0
) = execute_result.resource_usage {
510
0
            self.scheduler
511
0
                .record_action_resource_usage(&self.worker_id, &operation_id, resource_usage)
512
0
                .await
513
0
                .err_tip(|| {
514
0
                    format!("Failed to record resource usage for operation {operation_id}")
515
0
                })?;
516
10.3k
        }
517
518
10.3k
        match execute_result
519
10.3k
            .result
520
10.3k
            .err_tip(|| "Expected result to exist in ExecuteResult")
?0
521
        {
522
10.0k
            execute_result::Result::ExecuteResponse(finished_result) => {
523
10.0k
                let action_stage = finished_result
524
10.0k
                    .try_into()
525
10.0k
                    .err_tip(|| "Failed to convert ExecuteResponse into an ActionStage")
?0
;
526
10.0k
                self.scheduler
527
10.0k
                    .update_action(
528
10.0k
                        &self.worker_id,
529
10.0k
                        &operation_id,
530
10.0k
                        UpdateOperationType::UpdateWithActionStage(action_stage),
531
10.0k
                    )
532
10.0k
                    .await
533
10.0k
                    .err_tip(|| 
format!9
("Failed to operation {operation_id}"))
?9
;
534
            }
535
260
            execute_result::Result::InternalError(e) => {
536
260
                self.scheduler
537
260
                    .update_action(
538
260
                        &self.worker_id,
539
260
                        &operation_id,
540
260
                        UpdateOperationType::UpdateWithError(e.into()),
541
260
                    )
542
260
                    .await
543
260
                    .err_tip(|| 
format!1
("Failed to operation {operation_id}"))
?1
;
544
            }
545
        }
546
10.2k
        Ok(())
547
10.3k
    }
548
549
    /// The answer is recorded before the liveness refresh: the refresh runs
550
    /// the unacknowledged sweep, which would otherwise requeue an
551
    /// acknowledgement that arrives right at the timeout.
552
10.6k
    async fn dispatch_accepted(&self, execute_accepted: ExecuteAccepted) -> Result<(), Error> {
553
10.6k
        let operation_id = OperationId::from(execute_accepted.operation_id);
554
10.6k
        self.scheduler
555
10.6k
            .worker_dispatch_accepted(&self.worker_id, &operation_id)
556
10.6k
            .await
557
10.6k
            .err_tip(|| 
format!0
("Failed to record acceptance of operation {operation_id}"))
?0
;
558
10.6k
        self.touch_liveness_best_effort().await;
559
10.6k
        Ok(())
560
10.6k
    }
561
562
1
    async fn dispatch_declined(&self, execute_declined: ExecuteDeclined) -> Result<(), Error> {
563
1
        let operation_id = OperationId::from(execute_declined.operation_id);
564
1
        let reason = execute_declined::Reason::try_from(execute_declined.reason)
565
1
            .unwrap_or(execute_declined::Reason::Unspecified);
566
1
        let needs_kb = (reason == execute_declined::Reason::Load && execute_declined.needed_kb > 0)
567
1
            .then_some(execute_declined.needed_kb);
568
1
        let mut why = reason.as_str_name().to_ascii_lowercase();
569
1
        if !execute_declined.detail.is_empty() {
570
0
            why.push_str(": ");
571
0
            why.push_str(&execute_declined.detail);
572
1
        }
573
1
        self.scheduler
574
1
            .worker_dispatch_declined(&self.worker_id, &operation_id, why, needs_kb)
575
1
            .await
576
1
            .err_tip(|| 
format!0
("Failed to record decline of operation {operation_id}"))
?0
;
577
1
        self.touch_liveness_best_effort().await;
578
1
        Ok(())
579
1
    }
580
581
10.0k
    async fn execution_complete(&self, execute_complete: ExecuteComplete) -> Result<(), Error> {
582
10.0k
        self.touch_liveness_best_effort().await;
583
10.0k
        let operation_id = OperationId::from(execute_complete.operation_id);
584
10.0k
        self.scheduler
585
10.0k
            .update_action(
586
10.0k
                &self.worker_id,
587
10.0k
                &operation_id,
588
10.0k
                UpdateOperationType::ExecutionComplete,
589
10.0k
            )
590
10.0k
            .await
591
10.0k
            .err_tip(|| 
format!0
("Failed to operation {operation_id}"))
?0
;
592
10.0k
        Ok(())
593
10.0k
    }
594
}