veilid_tools/async_locks/async_weighted_semaphore.rs
1//! AsyncWeightedSemaphore
2//!
3//! A semaphore whose capacity (the "limit") can be changed at any time, and
4//! whose permits carry a weight rather than counting as one. Acquisition waits
5//! until the in-flight weight plus the requested weight fits under the current
6//! limit. Unlike a fixed-permit semaphore, the limit can be lowered while
7//! permits are held: new acquisitions wait until enough weight drains.
8//!
9//! Used to bound total in-flight bytes across all concurrent DHT operations
10//! against an adaptively-estimated uplink budget.
11use super::*;
12
13use core::sync::atomic::{AtomicU64, Ordering};
14use event_listener::Event;
15
16/// A semaphore admitting weighted permits under a runtime-adjustable capacity limit.
17///
18/// Acquisition waits until the in-flight weight plus the requested weight fits under
19/// the current limit. The limit can be raised or lowered while permits are held; a
20/// single permit whose weight exceeds the limit is admitted when nothing else is in
21/// flight, so it cannot deadlock.
22#[derive(Debug)]
23pub struct AsyncWeightedSemaphore {
24 /// Total weight currently held by live permits
25 in_flight: AtomicU64,
26 /// Current capacity; acquisitions wait until in_flight + weight <= limit
27 limit: AtomicU64,
28 /// Wakes waiters when weight is released or the limit is raised
29 event: Event,
30}
31
32impl AsyncWeightedSemaphore {
33 /// Create a semaphore with the given initial capacity limit.
34 #[must_use]
35 pub fn new(limit: u64) -> Arc<Self> {
36 Arc::new(Self {
37 in_flight: AtomicU64::new(0),
38 limit: AtomicU64::new(limit),
39 event: Event::new(),
40 })
41 }
42
43 /// Current capacity limit
44 pub fn limit(&self) -> u64 {
45 self.limit.load(Ordering::Acquire)
46 }
47
48 /// Total weight currently held by live permits
49 pub fn in_flight(&self) -> u64 {
50 self.in_flight.load(Ordering::Acquire)
51 }
52
53 /// Change the capacity limit. Raising it wakes waiters to re-check; lowering
54 /// it applies backpressure, waiting acquisitions until in-flight weight drains.
55 pub fn set_limit(&self, new_limit: u64) {
56 let old = self.limit.swap(new_limit, Ordering::AcqRel);
57 if new_limit > old {
58 self.event.notify(usize::MAX);
59 }
60 }
61
62 /// Raise the limit to at least `candidate`, never lowering it; wakes waiters if raised.
63 /// Returns the previous limit so callers can detect/log an actual raise.
64 ///
65 /// Monotonic: a `candidate` at or below the current limit is a no-op.
66 pub fn raise_limit(&self, candidate: u64) -> u64 {
67 let prev = self.limit.fetch_max(candidate, Ordering::AcqRel);
68 if candidate > prev {
69 self.event.notify(usize::MAX);
70 }
71 prev
72 }
73
74 /// Try to acquire `weight` units without waiting.
75 ///
76 /// Never blocks. On success the guard holds the weight until dropped. A zero-weight
77 /// request always succeeds and accounts nothing.
78 #[must_use]
79 pub fn try_acquire(self: &Arc<Self>, weight: u64) -> Option<AsyncWeightedSemaphoreGuard> {
80 // Zero-weight permits always succeed and account nothing
81 if weight == 0 {
82 return Some(AsyncWeightedSemaphoreGuard {
83 sem: self.clone(),
84 weight: 0,
85 });
86 }
87 loop {
88 let cur = self.in_flight.load(Ordering::Acquire);
89 let lim = self.limit.load(Ordering::Acquire);
90 // Progress guarantee: admit one operation when nothing is in flight even
91 // if its weight exceeds the limit, so a single large op can't deadlock.
92 if cur != 0 && cur.saturating_add(weight) > lim {
93 return None;
94 }
95 if self
96 .in_flight
97 .compare_exchange_weak(
98 cur,
99 cur.saturating_add(weight),
100 Ordering::AcqRel,
101 Ordering::Acquire,
102 )
103 .is_ok()
104 {
105 return Some(AsyncWeightedSemaphoreGuard {
106 sem: self.clone(),
107 weight,
108 });
109 }
110 // CAS lost a race; retry
111 }
112 }
113
114 /// Acquire `weight` units, waiting until they fit under the current limit.
115 ///
116 /// Blocks until enough weight drains (or the limit is raised). The returned guard holds
117 /// the weight until dropped. No deadlock timeout: an over-limit request is admitted only
118 /// when nothing else is in flight.
119 #[must_use]
120 pub async fn acquire(self: &Arc<Self>, weight: u64) -> AsyncWeightedSemaphoreGuard {
121 loop {
122 if let Some(g) = self.try_acquire(weight) {
123 return g;
124 }
125 // Register before re-checking so a release/raise between the failed try
126 // and the await is not missed.
127 let listener = self.event.listen();
128 if let Some(g) = self.try_acquire(weight) {
129 return g;
130 }
131 listener.await;
132 }
133 }
134
135 fn release(&self, weight: u64) {
136 if weight == 0 {
137 return;
138 }
139 self.in_flight.fetch_sub(weight, Ordering::AcqRel);
140 self.event.notify(usize::MAX);
141 }
142}
143
144/// A guard holding `weight` units; releases them back to the semaphore on drop.
145#[clippy::has_significant_drop]
146#[derive(Debug)]
147pub struct AsyncWeightedSemaphoreGuard {
148 sem: Arc<AsyncWeightedSemaphore>,
149 weight: u64,
150}
151
152impl Drop for AsyncWeightedSemaphoreGuard {
153 fn drop(&mut self) {
154 self.sem.release(self.weight);
155 }
156}