Coverage Report

Created: 2026-09-18 20:40

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-scheduler/src/worker_registry.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 core::time::Duration;
16
use std::collections::HashMap;
17
use std::sync::Arc;
18
use std::time::SystemTime;
19
20
use async_lock::RwLock;
21
use nativelink_util::action_messages::WorkerId;
22
use tracing::{debug, trace};
23
24
/// Ceiling for an action whose worker no instance here recognises. Long on
25
/// purpose: heartbeats never reach the shared state, so a healthy hour-long
26
/// action and an abandoned one look identical from here, and acting early
27
/// costs live work.
28
pub const ORPHANED_ACTION_TIMEOUT: Duration = Duration::from_hours(1);
29
30
/// What this scheduler instance knows about a worker's liveness.
31
///
32
/// A worker only appears in the registry of the instance holding its
33
/// `connect_worker` stream, so `Unknown` means "not mine to judge", not
34
/// "dead".
35
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36
pub enum WorkerLiveness {
37
    /// Registered here and has heartbeat within the timeout.
38
    Alive,
39
    /// Registered here, but has not been heard from within the timeout.
40
    Stale,
41
    /// Not registered here at all. Either connected to a different scheduler
42
    /// instance, or already evicted (in which case its actions were requeued
43
    /// at eviction time and no longer reference it).
44
    Unknown,
45
}
46
47
/// In-memory worker registry that tracks worker liveness.
48
///
49
/// Per-process: fed by the `connect_worker` streams this instance owns, not a
50
/// view of every worker in the deployment.
51
#[derive(Debug)]
52
pub struct WorkerRegistry {
53
    workers: RwLock<HashMap<WorkerId, SystemTime>>,
54
}
55
56
impl Default for WorkerRegistry {
57
0
    fn default() -> Self {
58
0
        Self::new()
59
0
    }
60
}
61
62
impl WorkerRegistry {
63
    /// Creates a new worker registry.
64
57
    pub fn new() -> Self {
65
57
        Self {
66
57
            workers: RwLock::new(HashMap::new()),
67
57
        }
68
57
    }
69
70
    /// Updates the heartbeat timestamp for a worker.
71
10
    pub async fn update_worker_heartbeat(&self, worker_id: &WorkerId, now: SystemTime) {
72
10
        let mut workers = self.workers.write().await;
73
10
        workers.insert(worker_id.clone(), now);
74
10
        trace!(?worker_id, now = %humantime::format_rfc3339(now), "FLOW: Worker heartbeat updated in registry");
75
10
    }
76
77
51
    pub async fn register_worker(&self, worker_id: &WorkerId, now: SystemTime) {
78
51
        let mut workers = self.workers.write().await;
79
51
        workers.insert(worker_id.clone(), now);
80
51
        debug!(?worker_id, "FLOW: Worker registered in registry");
81
51
    }
82
83
8
    pub async fn remove_worker(&self, worker_id: &WorkerId) {
84
8
        let mut workers = self.workers.write().await;
85
8
        workers.remove(worker_id);
86
8
        debug!(?worker_id, "FLOW: Worker removed from registry");
87
8
    }
88
89
    /// Liveness as far as THIS instance can tell. Callers acting on "dead"
90
    /// must handle `Unknown` separately.
91
26
    pub async fn check_liveness(
92
26
        &self,
93
26
        worker_id: &WorkerId,
94
26
        timeout: Duration,
95
26
        now: SystemTime,
96
26
    ) -> WorkerLiveness {
97
26
        let workers = self.workers.read().await;
98
99
26
        let Some(
last_seen16
) = workers.get(worker_id) else {
100
10
            trace!(?worker_id, "FLOW: Worker not registered on this instance");
101
10
            return WorkerLiveness::Unknown;
102
        };
103
104
        // An overflowing deadline can only be clock skew; treat as alive.
105
16
        let liveness = match last_seen.checked_add(timeout) {
106
16
            Some(
deadline8
) if deadline > no
w8
=>
WorkerLiveness::Alive8
,
107
8
            Some(_) => WorkerLiveness::Stale,
108
0
            None => WorkerLiveness::Alive,
109
        };
110
16
        trace!(
111
            ?worker_id,
112
16
            last_seen = %humantime::format_rfc3339(*last_seen),
113
            ?timeout,
114
            ?liveness,
115
            "FLOW: Worker liveness check"
116
        );
117
16
        liveness
118
26
    }
119
120
9
    pub async fn is_worker_alive(
121
9
        &self,
122
9
        worker_id: &WorkerId,
123
9
        timeout: Duration,
124
9
        now: SystemTime,
125
9
    ) -> bool {
126
9
        self.check_liveness(worker_id, timeout, now).await == WorkerLiveness::Alive
127
9
    }
128
129
0
    pub async fn get_worker_last_seen(&self, worker_id: &WorkerId) -> Option<SystemTime> {
130
0
        let workers = self.workers.read().await;
131
0
        workers.get(worker_id).copied()
132
0
    }
133
}
134
135
pub type SharedWorkerRegistry = Arc<WorkerRegistry>;
136
137
#[cfg(test)]
138
mod tests {
139
    use nativelink_macro::nativelink_test;
140
141
    use super::*;
142
143
    #[nativelink_test]
144
    async fn test_worker_heartbeat() {
145
        let registry = WorkerRegistry::new();
146
        let worker_id = WorkerId::from(String::from("test"));
147
        let now = SystemTime::now();
148
149
        // Worker not registered yet
150
        assert!(
151
            !registry
152
                .is_worker_alive(&worker_id, Duration::from_secs(5), now)
153
                .await
154
        );
155
156
        // Register worker
157
        registry.register_worker(&worker_id, now).await;
158
        assert!(
159
            registry
160
                .is_worker_alive(&worker_id, Duration::from_secs(5), now)
161
                .await
162
        );
163
164
        // Check with expired timeout
165
        let future = now.checked_add(Duration::from_secs(10)).unwrap();
166
        assert!(
167
            !registry
168
                .is_worker_alive(&worker_id, Duration::from_secs(5), future)
169
                .await
170
        );
171
172
        // Update heartbeat
173
        registry.update_worker_heartbeat(&worker_id, future).await;
174
        assert!(
175
            registry
176
                .is_worker_alive(&worker_id, Duration::from_secs(5), future)
177
                .await
178
        );
179
    }
180
181
    #[nativelink_test]
182
    async fn test_check_liveness_distinguishes_stale_from_unknown() {
183
        let registry = WorkerRegistry::new();
184
        let mine = WorkerId::from(String::from("mine"));
185
        let theirs = WorkerId::from(String::from("belongs-to-another-instance"));
186
        let now = SystemTime::now();
187
        let timeout = Duration::from_secs(5);
188
189
        // Never registered here. This is the case that must NOT read as dead:
190
        // with several schedulers on shared state it is a peer's worker.
191
        assert_eq!(
192
            registry.check_liveness(&theirs, timeout, now).await,
193
            WorkerLiveness::Unknown
194
        );
195
196
        registry.register_worker(&mine, now).await;
197
        assert_eq!(
198
            registry.check_liveness(&mine, timeout, now).await,
199
            WorkerLiveness::Alive
200
        );
201
202
        // Registered here but gone quiet: genuinely ours and genuinely dead.
203
        let later = now.checked_add(Duration::from_secs(10)).unwrap();
204
        assert_eq!(
205
            registry.check_liveness(&mine, timeout, later).await,
206
            WorkerLiveness::Stale
207
        );
208
209
        // Eviction takes it back to Unknown, not Stale.
210
        registry.remove_worker(&mine).await;
211
        assert_eq!(
212
            registry.check_liveness(&mine, timeout, later).await,
213
            WorkerLiveness::Unknown
214
        );
215
    }
216
217
    #[nativelink_test]
218
    async fn test_remove_worker() {
219
        let registry = WorkerRegistry::new();
220
        let worker_id = WorkerId::from(String::from("test-worker"));
221
        let now = SystemTime::now();
222
223
        registry.register_worker(&worker_id, now).await;
224
        assert!(
225
            registry
226
                .is_worker_alive(&worker_id, Duration::from_secs(5), now)
227
                .await
228
        );
229
230
        registry.remove_worker(&worker_id).await;
231
        assert!(
232
            !registry
233
                .is_worker_alive(&worker_id, Duration::from_secs(5), now)
234
                .await
235
        );
236
    }
237
}