spin_connection_semaphore/
lib.rs1use std::sync::Arc;
2use std::sync::atomic::{AtomicBool, Ordering};
3use std::time::Duration;
4
5use anyhow::anyhow;
6use tokio::sync::{OwnedSemaphorePermit, Semaphore, TryAcquireError};
7use tokio::time;
8
9#[derive(Clone, Debug)]
15pub struct LimitedSemaphore {
16 sem: Arc<Semaphore>,
17 limit: usize,
18}
19
20impl LimitedSemaphore {
21 pub fn new(limit: usize) -> Self {
25 Self {
26 sem: Arc::new(Semaphore::new(limit)),
27 limit,
28 }
29 }
30
31 pub fn limit(&self) -> usize {
33 self.limit
34 }
35
36 #[cfg(test)]
39 pub(crate) fn semaphore(&self) -> Arc<Semaphore> {
40 self.sem.clone()
41 }
42}
43
44#[derive(Clone, Debug)]
46pub struct ConnectionSemaphore {
47 global: Option<LimitedSemaphore>,
51 factor_specific: Option<LimitedSemaphore>,
56 factor: &'static str,
58 wait_timeout: Option<Duration>,
62 app_id: Arc<str>,
67 rejecting: Arc<AtomicBool>,
71}
72
73impl ConnectionSemaphore {
74 pub fn new(
82 global: Option<LimitedSemaphore>,
83 factor_specific: Option<LimitedSemaphore>,
84 factor: &'static str,
85 app_id: Arc<str>,
86 wait_timeout: Option<Duration>,
87 ) -> Self {
88 Self {
89 global,
90 factor_specific,
91 factor,
92 wait_timeout,
93 app_id,
94 rejecting: Arc::new(AtomicBool::new(false)),
95 }
96 }
97
98 fn mark_serving(&self) {
101 self.rejecting.store(false, Ordering::Relaxed);
102 }
103
104 pub async fn acquire(&self) -> anyhow::Result<ConnectionPermit> {
114 if let Ok(permit) = self.try_acquire_permits() {
116 spin_telemetry::counter!(
117 outbound_connection_permits_acquired = 1,
118 kind = self.factor,
119 waited = false
120 );
121 self.mark_serving();
122 self.emit_utilization();
123 return Ok(permit);
124 }
125
126 match self.wait_timeout {
127 Some(timeout) => time::timeout(timeout, self.acquire_inner())
128 .await
129 .map_err(|_| {
130 if !self.rejecting.swap(true, Ordering::Relaxed) {
133 tracing::warn!(
134 kind = self.factor,
135 app_id = %self.app_id,
136 "connection permit rejected: timeout waiting for permit"
137 );
138 }
139 anyhow!("connection semaphore timed out after {timeout:?}")
140 })?,
141 None => self.acquire_inner().await,
142 }
143 }
144
145 async fn acquire_inner(&self) -> anyhow::Result<ConnectionPermit> {
147 async fn acquire_one(
151 sem: &Arc<Semaphore>,
152 waited: &mut bool,
153 label: &str,
154 ) -> anyhow::Result<OwnedSemaphorePermit> {
155 match sem.clone().try_acquire_owned() {
156 Ok(p) => Ok(p),
157 Err(TryAcquireError::NoPermits) => {
158 *waited = true;
159 sem.clone()
160 .acquire_owned()
161 .await
162 .map_err(|_| anyhow!("{label} connection semaphore closed"))
163 }
164 Err(_) => Err(anyhow!("{label} connection semaphore closed")),
165 }
166 }
167 let mut waited = false;
168 let start = std::time::Instant::now();
169
170 let factor_specific = match &self.factor_specific {
173 Some(f) => Some(acquire_one(&f.sem, &mut waited, "factor").await?),
174 None => None,
175 };
176 let global = match &self.global {
179 Some(g) => Some(acquire_one(&g.sem, &mut waited, "global").await?),
180 None => None,
181 };
182
183 let factor = self.factor;
184 if waited {
185 spin_telemetry::histogram_f64!(
186 outbound_connection_permit_wait_duration_ms = start.elapsed().as_millis() as f64,
187 kind = factor
188 );
189 }
190 spin_telemetry::counter!(
191 outbound_connection_permits_acquired = 1,
192 kind = factor,
193 waited = waited
194 );
195 self.mark_serving();
196 self.emit_utilization();
197
198 Ok(ConnectionPermit {
199 global_permit: global,
200 factor_specific_permit: factor_specific,
201 semaphore: self.clone(),
202 })
203 }
204
205 pub fn try_acquire(&self) -> Option<ConnectionPermit> {
211 match self.try_acquire_permits() {
212 Ok(permit) => {
213 spin_telemetry::counter!(
214 outbound_connection_permits_acquired = 1,
215 kind = self.factor,
216 waited = false
217 );
218 self.mark_serving();
219 self.emit_utilization();
220 Some(permit)
221 }
222 Err(limit) => {
223 spin_telemetry::counter!(
224 outbound_connection_permits_rejected = 1,
225 kind = self.factor,
226 limit = limit
227 );
228 if !self.rejecting.swap(true, Ordering::Relaxed) {
231 tracing::warn!(
232 kind = self.factor,
233 app_id = %self.app_id,
234 limit = limit,
235 "connection permit rejected: limit exhausted"
236 );
237 }
238 None
239 }
240 }
241 }
242
243 fn try_acquire_permits(&self) -> Result<ConnectionPermit, &'static str> {
249 let global = match &self.global {
251 Some(s) => match s.sem.clone().try_acquire_owned() {
252 Ok(p) => Some(p),
253 Err(_) => return Err("global"),
254 },
255 None => None,
256 };
257 let factor_specific = match &self.factor_specific {
260 Some(s) => match s.sem.clone().try_acquire_owned() {
261 Ok(p) => Some(p),
262 Err(_) => return Err("factor"),
263 },
264 None => None,
265 };
266 Ok(ConnectionPermit {
267 global_permit: global,
268 factor_specific_permit: factor_specific,
269 semaphore: self.clone(),
270 })
271 }
272
273 fn emit_utilization(&self) {
281 if let Some(util) = utilization(self.factor_specific.as_ref()) {
282 spin_telemetry::histogram_f64!(
283 outbound_connection_factor_utilization = util,
284 kind = self.factor
285 );
286 }
287 if let Some(util) = utilization(self.global.as_ref()) {
288 spin_telemetry::histogram_f64!(outbound_connection_global_utilization = util);
289 }
290 }
291}
292
293pub fn metric_histogram_buckets() -> Vec<spin_telemetry::HistogramBuckets> {
302 let boundaries = vec![0.25, 0.5, 0.75, 0.9, 0.95, 0.99];
303 [
304 "outbound_connection_factor_utilization",
305 "outbound_connection_global_utilization",
306 ]
307 .into_iter()
308 .map(|metric_name| spin_telemetry::HistogramBuckets {
309 metric_name,
310 boundaries: boundaries.clone(),
311 })
312 .collect()
313}
314
315fn utilization(limited: Option<&LimitedSemaphore>) -> Option<f64> {
318 let limited = limited?;
319 if limited.limit == 0 {
320 return None;
321 }
322 let in_flight = limited
323 .limit
324 .saturating_sub(limited.sem.available_permits());
325 Some(in_flight as f64 / limited.limit as f64)
326}
327
328#[derive(Debug)]
332pub struct ConnectionPermit {
333 global_permit: Option<OwnedSemaphorePermit>,
334 factor_specific_permit: Option<OwnedSemaphorePermit>,
335 semaphore: ConnectionSemaphore,
338}
339
340impl Drop for ConnectionPermit {
341 fn drop(&mut self) {
342 self.global_permit.take();
346 self.factor_specific_permit.take();
347 self.semaphore.emit_utilization();
348 }
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354
355 #[tokio::test]
356 async fn no_limits_acquire_always_succeeds() {
357 let sem = ConnectionSemaphore::new(None, None, "test", Arc::from("test-app"), None);
358 let permit = sem.acquire().await.expect("should succeed");
359 drop(permit);
360 let _permit2 = sem.acquire().await.expect("should succeed again");
361 }
362
363 #[test]
364 fn no_limits_try_acquire_always_succeeds() {
365 let sem = ConnectionSemaphore::new(None, None, "test", Arc::from("test-app"), None);
366 let permit = sem.try_acquire().expect("should succeed");
367 drop(permit);
368 let _permit2 = sem.try_acquire().expect("should succeed again");
369 }
370
371 #[test]
372 fn global_limit_only_exhausted() {
373 let global = LimitedSemaphore::new(1);
374 let global_sem = global.semaphore();
375 let sem = ConnectionSemaphore::new(Some(global), None, "test", Arc::from("test-app"), None);
376 let permit1 = sem.try_acquire().expect("first should succeed");
377 assert!(
378 sem.try_acquire().is_none(),
379 "second should fail: global exhausted"
380 );
381 drop(permit1);
382 assert_eq!(global_sem.available_permits(), 1);
383 let _permit3 = sem.try_acquire().expect("after release should succeed");
384 }
385
386 #[test]
387 fn factor_limit_only_exhausted() {
388 let sem = ConnectionSemaphore::new(
389 None,
390 Some(LimitedSemaphore::new(1)),
391 "test",
392 Arc::from("test-app"),
393 None,
394 );
395 let permit1 = sem.try_acquire().expect("first should succeed");
396 assert!(
397 sem.try_acquire().is_none(),
398 "second should fail: factor exhausted"
399 );
400 drop(permit1);
401 let _permit3 = sem.try_acquire().expect("after release should succeed");
402 }
403
404 #[test]
405 fn both_limits_global_exhausted_first() {
406 let global = LimitedSemaphore::new(1);
407 let factor = LimitedSemaphore::new(2);
408 let factor_sem = factor.semaphore();
409 let sem = ConnectionSemaphore::new(
410 Some(global),
411 Some(factor),
412 "test",
413 Arc::from("test-app"),
414 None,
415 );
416
417 let permit1 = sem.try_acquire().expect("first should succeed");
418 let factor_before = factor_sem.available_permits();
420
421 assert!(sem.try_acquire().is_none(), "should fail: global exhausted");
423 assert_eq!(
425 factor_sem.available_permits(),
426 factor_before,
427 "factor permits should not be consumed when global is exhausted"
428 );
429 drop(permit1);
430 }
431
432 #[test]
433 fn both_limits_factor_exhausted_global_released() {
434 let global = LimitedSemaphore::new(2);
435 let factor = LimitedSemaphore::new(1);
436 let global_sem = global.semaphore();
437 let sem = ConnectionSemaphore::new(
438 Some(global),
439 Some(factor),
440 "test",
441 Arc::from("test-app"),
442 None,
443 );
444
445 let permit1 = sem.try_acquire().expect("first should succeed");
446 let result = sem.try_acquire();
448 assert!(result.is_none(), "should fail: factor exhausted");
449 assert_eq!(global_sem.available_permits(), 1);
451 drop(permit1);
452 assert_eq!(global_sem.available_permits(), 2);
453 }
454
455 #[tokio::test]
456 async fn acquire_waits_for_release() {
457 let sem = ConnectionSemaphore::new(
458 Some(LimitedSemaphore::new(1)),
459 None,
460 "test",
461 Arc::from("test-app"),
462 None,
463 );
464
465 let permit = sem.try_acquire().expect("first should succeed");
466
467 let sem2 = sem.clone();
468 let handle = tokio::spawn(async move {
469 let _p = sem2.acquire().await.expect("should eventually acquire");
470 });
471
472 drop(permit); handle.await.expect("task should complete");
474 }
475
476 #[tokio::test]
479 async fn acquire_releases_global_while_waiting_for_factor() {
480 let global = LimitedSemaphore::new(1);
481 let factor = LimitedSemaphore::new(1);
482 let global_sem = global.semaphore();
483 let factor_sem = factor.semaphore();
484 let sem = ConnectionSemaphore::new(
485 Some(global),
486 Some(factor),
487 "test",
488 Arc::from("test-app"),
489 None,
490 );
491
492 let _factor_hold = factor_sem.acquire_owned().await.unwrap();
494
495 let sem_clone = sem.clone();
496 let handle = tokio::spawn(async move {
497 sem_clone
498 .acquire()
499 .await
500 .expect("should succeed after factor is released")
501 });
502
503 tokio::task::yield_now().await;
506 tokio::task::yield_now().await;
507
508 assert_eq!(
509 global_sem.available_permits(),
510 1,
511 "global should be free while acquire() waits for factor-specific"
512 );
513
514 drop(_factor_hold);
515 handle.await.expect("task should complete");
516 }
517
518 #[tokio::test]
519 async fn acquire_times_out_when_semaphore_exhausted() {
520 let sem = ConnectionSemaphore::new(
521 Some(LimitedSemaphore::new(1)),
522 None,
523 "test",
524 Arc::from("test-app"),
525 Some(Duration::from_millis(10)),
526 );
527
528 let _permit = sem.try_acquire().expect("first should succeed");
529
530 let err = sem.acquire().await.expect_err("should time out");
531 assert!(
532 err.to_string().contains("timed out"),
533 "error message should mention timed out: {err}"
534 );
535 }
536
537 #[test]
538 fn utilization_reflects_post_release_state() {
539 let limited = LimitedSemaphore::new(2);
540 assert_eq!(utilization(Some(&limited)), Some(0.0));
541 let p1 = limited
542 .semaphore()
543 .try_acquire_owned()
544 .expect("first acquire");
545 assert_eq!(utilization(Some(&limited)), Some(0.5));
546 let p2 = limited
547 .semaphore()
548 .try_acquire_owned()
549 .expect("second acquire");
550 assert_eq!(utilization(Some(&limited)), Some(1.0));
551 drop(p1);
552 assert_eq!(utilization(Some(&limited)), Some(0.5));
553 drop(p2);
554 assert_eq!(utilization(Some(&limited)), Some(0.0));
555 }
556
557 #[test]
558 fn no_utilization_when_limit_is_none() {
559 assert_eq!(utilization(None), None);
560 }
561}