Skip to content

Commit 97b3a6b

Browse files
committed
chore: taking rate_limiter from Dial 9
1 parent eddf4c5 commit 97b3a6b

2 files changed

Lines changed: 66 additions & 89 deletions

File tree

lambda-runtime/src/layers/api_response.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::{
22
constants::LAMBDA_RUNTIME_INVOCATION_ID,
33
deserializer,
4-
rate_limiter::RateLimiter,
4+
rate_limiter::rate_limited,
55
requests::{EventCompletionRequest, IntoRequest},
66
runtime::LambdaInvocation,
77
Diagnostic, EventErrorRequest, IntoFunctionResponse, LambdaEvent,
@@ -14,7 +14,7 @@ use std::{fmt::Debug, future::Future, marker::PhantomData, pin::Pin, task, time:
1414
use tower::Service;
1515
use tracing::{error, trace, warn};
1616

17-
static MALFORMED_INVOCATION_ID_LIMITER: RateLimiter = RateLimiter::new(Duration::from_secs(60));
17+
const MALFORMED_INVOCATION_ID_LOG_INTERVAL: Duration = Duration::from_secs(60);
1818

1919
/// Tower service that turns the result or an error of a handler function into a Lambda Runtime API
2020
/// response.
@@ -136,13 +136,13 @@ where
136136
Some(value) => match value.to_str() {
137137
Ok(value) => Some(value.to_owned()),
138138
Err(error) => {
139-
if MALFORMED_INVOCATION_ID_LIMITER.allow() {
139+
rate_limited!(MALFORMED_INVOCATION_ID_LOG_INTERVAL, {
140140
warn!(
141141
error = ?error,
142-
rate_limit_interval_ms = MALFORMED_INVOCATION_ID_LIMITER.interval().as_millis(),
142+
rate_limit_interval_ms = MALFORMED_INVOCATION_ID_LOG_INTERVAL.as_millis(),
143143
"Ignoring malformed Lambda runtime invocation ID header; this warning is rate limited"
144144
);
145-
}
145+
});
146146
None
147147
}
148148
},

lambda-runtime/src/rate_limiter.rs

Lines changed: 61 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,111 +1,88 @@
1-
//! Thread-safe, process-local rate limiting for infrequent runtime events.
1+
//! Internal call-site rate limiting for infrequent runtime events.
22
33
use std::{
4-
sync::Mutex,
4+
sync::OnceLock,
55
time::{Duration, Instant},
66
};
77

8-
/// Allows an operation at most once during each configured interval.
9-
///
10-
/// A `RateLimiter` is intended to be shared by concurrent runtime tasks. When
11-
/// stored in a `static`, it is initialized once per Lambda execution environment
12-
/// and retains its state across warm invocations. A new cold-started environment
13-
/// receives a new limiter.
14-
pub(crate) struct RateLimiter {
15-
/// Minimum duration between allowed operations.
16-
interval: Duration,
17-
/// Timestamp of the most recent allowed operation.
18-
last_allowed: Mutex<Option<Instant>>,
19-
}
20-
21-
impl RateLimiter {
22-
/// Creates a rate limiter with the specified minimum interval.
23-
pub(crate) const fn new(interval: Duration) -> RateLimiter {
24-
RateLimiter {
25-
interval,
26-
last_allowed: Mutex::new(None),
27-
}
28-
}
8+
/// Returns monotonic process-relative time for the rate limiter.
9+
pub(crate) fn time_since_epoch() -> Duration {
10+
static EPOCH: OnceLock<Instant> = OnceLock::new();
2911

30-
/// Returns the minimum duration between allowed operations.
31-
pub(crate) const fn interval(&self) -> Duration {
32-
self.interval
33-
}
12+
Instant::now().duration_since(*EPOCH.get_or_init(Instant::now))
13+
}
3414

35-
///
36-
/// The first call is allowed. Subsequent calls are rejected until the
37-
/// configured interval has elapsed since the previous allowed call.
38-
/// Concurrent callers are serialized while checking and updating the last
39-
/// allowed timestamp so only one caller crosses the interval boundary.
40-
pub(crate) fn allow(&self) -> bool {
41-
let mut last_allowed = match self.last_allowed.lock() {
42-
Ok(guard) => guard,
43-
Err(poisoned) => {
44-
// The limiter state is disposable, so reset it and recover instead of
45-
// allowing a poisoned mutex to crash the runtime or suppress future logs.
46-
let mut guard = poisoned.into_inner();
47-
*guard = None;
48-
self.last_allowed.clear_poison();
49-
guard
15+
/// Evaluates a call at most once per interval at each macro call site.
16+
///
17+
/// The limiter state is local to the call site and persists for the lifetime of
18+
/// the process. It is therefore shared across warm invocations in one Lambda
19+
/// execution environment and reset by a cold start.
20+
macro_rules! rate_limited {
21+
($interval:expr, $call:expr) => {{
22+
use std::sync::atomic::{AtomicU64, Ordering};
23+
24+
static NEXT_CALL: AtomicU64 = AtomicU64::new(u64::MIN);
25+
let interval: std::time::Duration = $interval;
26+
let time = $crate::rate_limiter::time_since_epoch();
27+
let next = NEXT_CALL.load(Ordering::Relaxed);
28+
29+
if next <= time.as_secs() {
30+
let new_next = time.checked_add(interval).unwrap_or(std::time::Duration::MAX).as_secs();
31+
32+
if NEXT_CALL
33+
.compare_exchange(next, new_next, Ordering::Relaxed, Ordering::Relaxed)
34+
.is_ok()
35+
{
36+
$call;
5037
}
51-
};
52-
53-
if last_allowed
54-
.as_ref()
55-
.is_some_and(|value| value.elapsed() < self.interval)
56-
{
57-
return false;
5838
}
59-
60-
*last_allowed = Some(Instant::now());
61-
62-
true
63-
}
39+
}};
6440
}
6541

42+
pub(crate) use rate_limited;
43+
6644
#[cfg(test)]
6745
mod tests {
6846
use super::*;
6947
use std::{
70-
panic::{catch_unwind, AssertUnwindSafe},
48+
sync::{
49+
atomic::{AtomicUsize, Ordering},
50+
Arc,
51+
},
7152
thread,
7253
};
7354

7455
#[test]
75-
fn allows_first_call() {
76-
let limiter = RateLimiter::new(Duration::from_secs(60));
77-
78-
assert!(limiter.allow());
79-
}
80-
81-
#[test]
82-
fn rejects_calls_inside_interval() {
83-
let limiter = RateLimiter::new(Duration::from_secs(60));
84-
85-
assert!(limiter.allow());
86-
assert!(!limiter.allow());
87-
}
88-
89-
#[test]
90-
fn allows_call_after_interval() {
91-
let limiter = RateLimiter::new(Duration::from_millis(10));
56+
fn allows_first_call_and_rejects_calls_inside_interval() {
57+
let mut calls = 0;
9258

93-
assert!(limiter.allow());
94-
thread::sleep(Duration::from_millis(15));
59+
for _ in 0..2 {
60+
rate_limited!(Duration::from_secs(60), {
61+
calls += 1;
62+
});
63+
}
9564

96-
assert!(limiter.allow());
65+
assert_eq!(calls, 1);
9766
}
9867

9968
#[test]
100-
fn recovers_from_poisoned_mutex() {
101-
let limiter = RateLimiter::new(Duration::from_secs(60));
102-
103-
let _ = catch_unwind(AssertUnwindSafe(|| {
104-
let _guard = limiter.last_allowed.lock().unwrap();
105-
panic!("poison the limiter mutex");
106-
}));
69+
fn allows_only_one_concurrent_call() {
70+
let calls = Arc::new(AtomicUsize::new(0));
71+
let handles = (0..8)
72+
.map(|_| {
73+
let calls = Arc::clone(&calls);
74+
thread::spawn(move || {
75+
rate_limited!(Duration::from_secs(60), {
76+
calls.fetch_add(1, Ordering::Relaxed);
77+
});
78+
})
79+
})
80+
.collect::<Vec<_>>();
81+
82+
for handle in handles {
83+
handle.join().unwrap();
84+
}
10785

108-
assert!(limiter.allow());
109-
assert!(!limiter.allow());
86+
assert_eq!(calls.load(Ordering::Relaxed), 1);
11087
}
11188
}

0 commit comments

Comments
 (0)