dynamo_runtime/routing_policy/
occupancy.rs1use 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
17pub 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 pub fn load(&self) -> u64 {
36 self.load
37 }
38
39 pub fn into_reservation(self) -> OccupancyReservation {
40 self.reservation
41 }
42}
43
44#[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 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 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 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
188pub 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 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
237pub(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}