Skip to main content

dynamo_runtime/routing_policy/
occupancy.rs

1// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{
5    collections::HashSet,
6    sync::{
7        Arc,
8        atomic::{AtomicU64, Ordering},
9    },
10};
11
12use dashmap::{DashMap, mapref::entry::Entry};
13
14use super::{CandidateView, RouteContext, RouteDecision, RoutePicker, RoutePolicy};
15use crate::{component::Endpoint, traits::DistributedRuntimeProvider};
16
17/// The result of an atomic load-aware selection and reservation.
18pub struct OccupancySelection {
19    worker_id: u64,
20    candidate_count: usize,
21    load: u64,
22    reservation: OccupancyReservation,
23}
24
25impl OccupancySelection {
26    pub fn worker_id(&self) -> u64 {
27        self.worker_id
28    }
29
30    pub fn candidate_count(&self) -> usize {
31        self.candidate_count
32    }
33
34    /// Selected worker load after this request was admitted.
35    pub fn load(&self) -> u64 {
36        self.load
37    }
38
39    pub fn into_reservation(self) -> OccupancyReservation {
40        self.reservation
41    }
42}
43
44/// Shared O(1) per-worker request occupancy.
45///
46/// Discovery controls eligibility separately from accounting. Removing a worker
47/// marks it absent immediately, but a counter with live reservations remains until
48/// its final reservation releases. Re-adding the same worker ID therefore sees the
49/// retained load instead of starting from zero.
50#[derive(Debug, Default)]
51pub struct RoutingOccupancyState {
52    counts: DashMap<u64, Arc<AtomicU64>>,
53    discovered: parking_lot::RwLock<HashSet<u64>>,
54    admission_lock: parking_lot::Mutex<()>,
55}
56
57impl RoutingOccupancyState {
58    fn increment_locked(&self, worker_id: u64) -> Arc<AtomicU64> {
59        let count = self
60            .counts
61            .entry(worker_id)
62            .or_insert_with(|| Arc::new(AtomicU64::new(0)))
63            .clone();
64        count.fetch_add(1, Ordering::Relaxed);
65        count
66    }
67
68    pub(crate) fn increment(&self, worker_id: u64) -> Arc<AtomicU64> {
69        let _admission = self.admission_lock.lock();
70        self.increment_locked(worker_id)
71    }
72
73    pub(crate) async fn select_exact_min_and_increment(&self, worker_ids: &[u64]) -> Option<u64> {
74        let picker = RoutePicker::new(RoutePolicy::LeastLoaded);
75        self.select_and_admit(
76            &picker,
77            CandidateView::Workers(worker_ids),
78            RouteContext::default(),
79        )
80        .map(|(decision, _)| decision.target.worker_id)
81    }
82
83    pub(crate) fn peek(
84        &self,
85        picker: &RoutePicker,
86        candidates: CandidateView<'_>,
87        context: RouteContext,
88    ) -> Option<RouteDecision> {
89        picker.peek(candidates, context, |id| self.load(id))
90    }
91
92    pub(crate) fn select_and_admit(
93        &self,
94        picker: &RoutePicker,
95        candidates: CandidateView<'_>,
96        context: RouteContext,
97    ) -> Option<(RouteDecision, Option<Arc<AtomicU64>>)> {
98        let _admission = self.admission_lock.lock();
99        let decision = picker.select(candidates, context, |id| self.load(id))?;
100        let counter = match decision.admission {
101            super::AdmissionKind::None => None,
102            super::AdmissionKind::Occupancy => {
103                Some(self.increment_locked(decision.target.worker_id))
104            }
105        };
106        Some((decision, counter))
107    }
108
109    /// Atomically run host-owned selection against the live load view and reserve its result.
110    pub fn select_and_reserve_with<E>(
111        self: &Arc<Self>,
112        candidates: &[u64],
113        select: impl FnOnce(&dyn Fn(u64) -> u64) -> Result<u64, E>,
114    ) -> Result<OccupancySelection, E> {
115        let _admission = self.admission_lock.lock();
116        let worker_id = select(&|worker_id| self.load(worker_id))?;
117        debug_assert!(candidates.contains(&worker_id));
118        let counter = self.increment_locked(worker_id);
119        let load = counter.load(Ordering::Relaxed);
120        Ok(OccupancySelection {
121            worker_id,
122            candidate_count: candidates.len(),
123            load,
124            reservation: OccupancyReservation::from_counter(Arc::clone(self), worker_id, counter),
125        })
126    }
127
128    /// Reserve an explicitly selected worker.
129    pub fn reserve(self: &Arc<Self>, worker_id: u64) -> OccupancyReservation {
130        let counter = self.increment(worker_id);
131        OccupancyReservation::from_counter(Arc::clone(self), worker_id, counter)
132    }
133
134    fn decrement_locked(&self, worker_id: u64, counter: &Arc<AtomicU64>) {
135        let _ = counter.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
136            Some(current.saturating_sub(1))
137        });
138
139        if counter.load(Ordering::Relaxed) != 0 || self.discovered.read().contains(&worker_id) {
140            return;
141        }
142
143        if let Entry::Occupied(entry) = self.counts.entry(worker_id)
144            && Arc::ptr_eq(entry.get(), counter)
145            && entry.get().load(Ordering::Relaxed) == 0
146        {
147            entry.remove();
148        }
149    }
150
151    pub(crate) fn release(&self, worker_id: u64, counter: &Arc<AtomicU64>) {
152        let _admission = self.admission_lock.lock();
153        self.decrement_locked(worker_id, counter);
154    }
155
156    pub(crate) fn decrement(&self, worker_id: u64) {
157        let _admission = self.admission_lock.lock();
158        let counter = self.counts.get(&worker_id).map(|entry| entry.clone());
159        if let Some(counter) = counter {
160            self.decrement_locked(worker_id, &counter);
161        }
162    }
163
164    pub fn load(&self, worker_id: u64) -> u64 {
165        self.counts
166            .get(&worker_id)
167            .map(|count| count.load(Ordering::Relaxed))
168            .unwrap_or(0)
169    }
170
171    /// Reconcile discovery eligibility without discarding guard-owned accounting.
172    pub(crate) fn retain(&self, worker_ids: &[u64]) {
173        let _admission = self.admission_lock.lock();
174        let mut live = self.discovered.write();
175        live.clear();
176        live.extend(worker_ids.iter().copied());
177        self.counts.retain(|worker_id, count| {
178            live.contains(worker_id) || count.load(Ordering::Relaxed) != 0
179        });
180    }
181
182    #[cfg(test)]
183    pub(crate) fn contains_slot(&self, worker_id: u64) -> bool {
184        self.counts.contains_key(&worker_id)
185    }
186}
187
188/// One guard-owned occupancy booking.
189pub struct OccupancyReservation {
190    state: Arc<RoutingOccupancyState>,
191    worker_id: u64,
192    counter: Arc<AtomicU64>,
193}
194
195impl OccupancyReservation {
196    pub(crate) fn from_counter(
197        state: Arc<RoutingOccupancyState>,
198        worker_id: u64,
199        counter: Arc<AtomicU64>,
200    ) -> Self {
201        Self {
202            state,
203            worker_id,
204            counter,
205        }
206    }
207
208    pub fn worker_id(&self) -> u64 {
209        self.worker_id
210    }
211
212    pub fn load(&self) -> u64 {
213        self.counter.load(Ordering::Relaxed)
214    }
215
216    /// Move this booking to the worker selected by transport fallback.
217    pub fn retarget(&mut self, worker_id: u64) -> u64 {
218        if self.worker_id == worker_id {
219            return self.load();
220        }
221
222        let _admission = self.state.admission_lock.lock();
223        let next = self.state.increment_locked(worker_id);
224        self.state.decrement_locked(self.worker_id, &self.counter);
225        self.worker_id = worker_id;
226        self.counter = next;
227        self.load()
228    }
229}
230
231impl Drop for OccupancyReservation {
232    fn drop(&mut self) {
233        self.state.release(self.worker_id, &self.counter);
234    }
235}
236
237/// Get or create the shared routing occupancy state for an endpoint.
238pub(crate) async fn get_or_create_routing_occupancy_state(
239    endpoint: &Endpoint,
240) -> Arc<RoutingOccupancyState> {
241    let drt = endpoint.drt();
242    let registry = drt.routing_occupancy_states();
243    let mut registry = registry.lock().await;
244
245    if let Some(weak) = registry.get(endpoint) {
246        if let Some(state) = weak.upgrade() {
247            return state;
248        }
249        registry.remove(endpoint);
250    }
251
252    let state = Arc::new(RoutingOccupancyState::default());
253    registry.insert(endpoint.clone(), Arc::downgrade(&state));
254    state
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    #[test]
262    fn absent_worker_keeps_live_reservation_across_same_id_readd() {
263        let state = Arc::new(RoutingOccupancyState::default());
264        state.retain(&[7]);
265        let old = state.reserve(7);
266
267        state.retain(&[]);
268        assert_eq!(state.load(7), 1);
269        assert!(state.contains_slot(7));
270
271        state.retain(&[7]);
272        let new = state.reserve(7);
273        assert_eq!(state.load(7), 2);
274
275        drop(old);
276        assert_eq!(state.load(7), 1);
277        drop(new);
278        assert_eq!(state.load(7), 0);
279    }
280
281    #[test]
282    fn absent_worker_slot_is_removed_after_final_release() {
283        let state = Arc::new(RoutingOccupancyState::default());
284        state.retain(&[7]);
285        let reservation = state.reserve(7);
286
287        state.retain(&[]);
288        assert!(state.contains_slot(7));
289        drop(reservation);
290
291        assert_eq!(state.load(7), 0);
292        assert!(!state.contains_slot(7));
293    }
294
295    #[test]
296    fn retarget_moves_exactly_one_booking() {
297        let state = Arc::new(RoutingOccupancyState::default());
298        state.retain(&[1, 2]);
299        let mut reservation = state.reserve(1);
300
301        assert_eq!(reservation.retarget(2), 1);
302        assert_eq!(state.load(1), 0);
303        assert_eq!(state.load(2), 1);
304
305        drop(reservation);
306        assert_eq!(state.load(2), 0);
307    }
308
309    #[test]
310    fn concurrent_selection_and_reservation_stays_balanced() {
311        let state = Arc::new(RoutingOccupancyState::default());
312        state.retain(&[1, 2, 3]);
313        let threads = (0..90)
314            .map(|_| {
315                let state = Arc::clone(&state);
316                std::thread::spawn(move || {
317                    state
318                        .select_and_reserve_with(&[1, 2, 3], |load| {
319                            Ok::<_, std::convert::Infallible>(
320                                [1, 2, 3]
321                                    .into_iter()
322                                    .min_by_key(|worker_id| load(*worker_id))
323                                    .unwrap(),
324                            )
325                        })
326                        .unwrap()
327                        .into_reservation()
328                })
329            })
330            .collect::<Vec<_>>();
331        let reservations = threads
332            .into_iter()
333            .map(|thread| thread.join().unwrap())
334            .collect::<Vec<_>>();
335
336        assert_eq!([state.load(1), state.load(2), state.load(3)], [30, 30, 30]);
337        drop(reservations);
338        assert_eq!([state.load(1), state.load(2), state.load(3)], [0, 0, 0]);
339    }
340}