1use std::collections::HashMap;
2use std::net::SocketAddr;
3use std::sync::Arc;
4use std::time::{Duration, Instant};
5use tokio::sync::RwLock;
6use tracing::{debug, warn};
7
8#[derive(Debug, Clone, Copy)]
10pub struct RateLimitWindow {
11 pub max_requests: u32,
13 pub window_duration: Duration,
15 pub burst: u32,
17}
18
19impl RateLimitWindow {
20 pub fn new(max_requests: u32, window_duration: Duration) -> Self {
22 Self {
23 max_requests,
24 window_duration,
25 burst: 0,
26 }
27 }
28
29 pub fn with_burst(mut self, burst: u32) -> Self {
31 self.burst = burst;
32 self
33 }
34}
35
36impl Default for RateLimitWindow {
37 fn default() -> Self {
38 Self {
39 max_requests: 100,
40 window_duration: Duration::from_secs(60),
41 burst: 10,
42 }
43 }
44}
45
46#[derive(Debug, Clone)]
48pub enum RateLimitResult {
49 Allowed { remaining: u32, reset_at: Instant },
51 Denied { retry_after: Duration, limit: u32 },
53}
54
55#[derive(Debug)]
57struct RateLimitBucket {
58 requests: Vec<Instant>,
60 window: RateLimitWindow,
62}
63
64impl RateLimitBucket {
65 fn new(window: RateLimitWindow) -> Self {
66 Self {
67 requests: Vec::with_capacity((window.max_requests + window.burst) as usize),
68 window,
69 }
70 }
71
72 fn prune_expired(&mut self, now: Instant) {
73 let cutoff = now - self.window.window_duration;
74 self.requests.retain(|&t| t > cutoff);
75 }
76
77 fn check_and_record(&mut self, now: Instant, limit_override: Option<u32>) -> RateLimitResult {
82 self.prune_expired(now);
83
84 let limit = limit_override.unwrap_or(self.window.max_requests + self.window.burst);
85 let current_count = self.requests.len() as u32;
86
87 if current_count >= limit {
88 let reported_limit = limit_override.unwrap_or(self.window.max_requests);
89 if let Some(oldest) = self.requests.first() {
91 let retry_after =
92 (*oldest + self.window.window_duration).saturating_duration_since(now);
93 RateLimitResult::Denied {
94 retry_after,
95 limit: reported_limit,
96 }
97 } else {
98 RateLimitResult::Denied {
99 retry_after: self.window.window_duration,
100 limit: reported_limit,
101 }
102 }
103 } else {
104 self.requests.push(now);
105 let reset_at = now + self.window.window_duration;
106 RateLimitResult::Allowed {
107 remaining: limit - current_count - 1,
108 reset_at,
109 }
110 }
111 }
112}
113
114#[derive(Debug, Clone)]
116pub struct RateLimiterConfig {
117 pub handshake_per_ip: RateLimitWindow,
119 pub connections_per_consumer: RateLimitWindow,
122 pub connections_per_account: RateLimitWindow,
125 pub subscriptions_per_connection: RateLimitWindow,
127 pub messages_per_connection: RateLimitWindow,
129 pub snapshots_per_connection: RateLimitWindow,
131 pub enabled: bool,
133}
134
135impl Default for RateLimiterConfig {
136 fn default() -> Self {
137 Self {
138 handshake_per_ip: RateLimitWindow::new(60, Duration::from_secs(60)).with_burst(10),
139 connections_per_consumer: RateLimitWindow::new(30, Duration::from_secs(60))
140 .with_burst(5),
141 connections_per_account: RateLimitWindow::new(100, Duration::from_secs(60))
142 .with_burst(20),
143 subscriptions_per_connection: RateLimitWindow::new(120, Duration::from_secs(60))
144 .with_burst(10),
145 messages_per_connection: RateLimitWindow::new(1000, Duration::from_secs(60))
146 .with_burst(100),
147 snapshots_per_connection: RateLimitWindow::new(30, Duration::from_secs(60))
148 .with_burst(5),
149 enabled: true,
150 }
151 }
152}
153
154impl RateLimiterConfig {
155 fn window_from_env(prefix: &str) -> Option<RateLimitWindow> {
157 let max = std::env::var(format!("{prefix}_MAX")).ok()?.parse().ok()?;
158 let secs = std::env::var(format!("{prefix}_WINDOW_SECS"))
159 .ok()?
160 .parse()
161 .ok()?;
162 Some(RateLimitWindow::new(max, Duration::from_secs(secs)))
163 }
164
165 fn window_from_env_with_alias(prefix: &str, deprecated: &str) -> Option<RateLimitWindow> {
168 if let Some(window) = Self::window_from_env(prefix) {
169 return Some(window);
170 }
171 let window = Self::window_from_env(deprecated)?;
172 tracing::warn!(
173 deprecated_prefix = deprecated,
174 replacement_prefix = prefix,
175 "deprecated rate-limit environment variables are set; \
176 rename them before the alias is removed"
177 );
178 Some(window)
179 }
180
181 pub fn from_env() -> Self {
183 let mut config = Self::default();
184
185 if let Some(window) = Self::window_from_env("ARETE_RATE_LIMIT_HANDSHAKE_PER_IP") {
187 config.handshake_per_ip = window;
188 }
189
190 if let Some(window) = Self::window_from_env_with_alias(
193 "ARETE_RATE_LIMIT_CONNECTIONS_PER_CONSUMER",
194 "ARETE_RATE_LIMIT_CONNECTIONS_PER_SUBJECT",
195 ) {
196 config.connections_per_consumer = window;
197 }
198
199 if let Some(window) = Self::window_from_env_with_alias(
202 "ARETE_RATE_LIMIT_CONNECTIONS_PER_ACCOUNT",
203 "ARETE_RATE_LIMIT_CONNECTIONS_PER_METERING_KEY",
204 ) {
205 config.connections_per_account = window;
206 }
207
208 if let Some(window) = Self::window_from_env("ARETE_RATE_LIMIT_SUBSCRIPTIONS_PER_CONNECTION")
210 {
211 config.subscriptions_per_connection = window;
212 }
213
214 if let Some(window) = Self::window_from_env("ARETE_RATE_LIMIT_MESSAGES_PER_CONNECTION") {
216 config.messages_per_connection = window;
217 }
218
219 if let Some(window) = Self::window_from_env("ARETE_RATE_LIMIT_SNAPSHOTS_PER_CONNECTION") {
221 config.snapshots_per_connection = window;
222 }
223
224 if let Ok(enabled) = std::env::var("ARETE_RATE_LIMITING_ENABLED") {
226 config.enabled = enabled.parse().unwrap_or(true);
227 }
228
229 config
230 }
231
232 pub fn disabled() -> Self {
234 Self {
235 enabled: false,
236 ..Default::default()
237 }
238 }
239}
240
241#[derive(Debug)]
243pub struct WebSocketRateLimiter {
244 config: RateLimiterConfig,
245 ip_buckets: Arc<RwLock<HashMap<String, RateLimitBucket>>>,
247 consumer_buckets: Arc<RwLock<HashMap<String, RateLimitBucket>>>,
249 account_buckets: Arc<RwLock<HashMap<String, RateLimitBucket>>>,
251 consumer_subscription_buckets: Arc<RwLock<HashMap<String, RateLimitBucket>>>,
253 account_subscription_buckets: Arc<RwLock<HashMap<String, RateLimitBucket>>>,
255 subscription_buckets: Arc<RwLock<HashMap<uuid::Uuid, RateLimitBucket>>>,
257 message_buckets: Arc<RwLock<HashMap<uuid::Uuid, RateLimitBucket>>>,
259 snapshot_buckets: Arc<RwLock<HashMap<uuid::Uuid, RateLimitBucket>>>,
261}
262
263impl WebSocketRateLimiter {
264 pub fn new(config: RateLimiterConfig) -> Self {
266 Self {
267 config,
268 ip_buckets: Arc::new(RwLock::new(HashMap::new())),
269 consumer_buckets: Arc::new(RwLock::new(HashMap::new())),
270 account_buckets: Arc::new(RwLock::new(HashMap::new())),
271 consumer_subscription_buckets: Arc::new(RwLock::new(HashMap::new())),
272 account_subscription_buckets: Arc::new(RwLock::new(HashMap::new())),
273 subscription_buckets: Arc::new(RwLock::new(HashMap::new())),
274 message_buckets: Arc::new(RwLock::new(HashMap::new())),
275 snapshot_buckets: Arc::new(RwLock::new(HashMap::new())),
276 }
277 }
278
279 pub async fn check_handshake(&self, addr: SocketAddr) -> RateLimitResult {
281 if !self.config.enabled {
282 return RateLimitResult::Allowed {
283 remaining: u32::MAX,
284 reset_at: Instant::now() + Duration::from_secs(60),
285 };
286 }
287
288 let ip = addr.ip().to_string();
289 let mut buckets = self.ip_buckets.write().await;
290 let bucket = buckets
291 .entry(ip.clone())
292 .or_insert_with(|| RateLimitBucket::new(self.config.handshake_per_ip));
293
294 let result = bucket.check_and_record(Instant::now(), None);
295
296 match &result {
297 RateLimitResult::Denied { retry_after, limit } => {
298 warn!(
299 ip = %ip,
300 retry_after_secs = retry_after.as_secs(),
301 limit = limit,
302 "Rate limit exceeded for handshake"
303 );
304 }
305 RateLimitResult::Allowed { remaining, .. } => {
306 debug!(
307 ip = %ip,
308 remaining = remaining,
309 "Handshake rate limit check passed"
310 );
311 }
312 }
313
314 result
315 }
316
317 fn allowed_unlimited() -> RateLimitResult {
318 RateLimitResult::Allowed {
319 remaining: u32::MAX,
320 reset_at: Instant::now() + Duration::from_secs(60),
321 }
322 }
323
324 async fn check_keyed_bucket(
325 &self,
326 buckets: &RwLock<HashMap<String, RateLimitBucket>>,
327 window: RateLimitWindow,
328 key: &str,
329 limit_override: Option<u32>,
330 ) -> RateLimitResult {
331 let mut buckets = buckets.write().await;
332 let bucket = buckets
333 .entry(key.to_string())
334 .or_insert_with(|| RateLimitBucket::new(window));
335 bucket.check_and_record(Instant::now(), limit_override)
336 }
337
338 pub async fn check_connection_for_consumer(
344 &self,
345 consumer: &str,
346 limit_override: Option<u32>,
347 ) -> RateLimitResult {
348 if !self.config.enabled {
349 return Self::allowed_unlimited();
350 }
351 self.check_keyed_bucket(
352 &self.consumer_buckets,
353 self.config.connections_per_consumer,
354 consumer,
355 limit_override,
356 )
357 .await
358 }
359
360 pub async fn check_connection_for_account(
366 &self,
367 account: &str,
368 limit_override: Option<u32>,
369 ) -> RateLimitResult {
370 if !self.config.enabled {
371 return Self::allowed_unlimited();
372 }
373 self.check_keyed_bucket(
374 &self.account_buckets,
375 self.config.connections_per_account,
376 account,
377 limit_override,
378 )
379 .await
380 }
381
382 pub async fn check_subscription_create_for_consumer(
388 &self,
389 consumer: &str,
390 limit: Option<u32>,
391 ) -> RateLimitResult {
392 let Some(limit) = limit else {
393 return Self::allowed_unlimited();
394 };
395 if !self.config.enabled {
396 return Self::allowed_unlimited();
397 }
398 self.check_keyed_bucket(
399 &self.consumer_subscription_buckets,
400 RateLimitWindow::new(limit, Duration::from_secs(60)),
401 consumer,
402 Some(limit),
403 )
404 .await
405 }
406
407 pub async fn check_subscription_create_for_account(
413 &self,
414 account: &str,
415 limit: Option<u32>,
416 ) -> RateLimitResult {
417 let Some(limit) = limit else {
418 return Self::allowed_unlimited();
419 };
420 if !self.config.enabled {
421 return Self::allowed_unlimited();
422 }
423 self.check_keyed_bucket(
424 &self.account_subscription_buckets,
425 RateLimitWindow::new(limit, Duration::from_secs(60)),
426 account,
427 Some(limit),
428 )
429 .await
430 }
431
432 #[deprecated(note = "use check_connection_for_consumer with the resolved consumer identity")]
434 pub async fn check_connection_for_subject(&self, subject: &str) -> RateLimitResult {
435 self.check_connection_for_consumer(subject, None).await
436 }
437
438 #[deprecated(note = "use check_connection_for_account with the resolved account identity")]
440 pub async fn check_connection_for_metering_key(&self, metering_key: &str) -> RateLimitResult {
441 self.check_connection_for_account(metering_key, None).await
442 }
443
444 pub async fn check_subscription(&self, client_id: uuid::Uuid) -> RateLimitResult {
446 if !self.config.enabled {
447 return RateLimitResult::Allowed {
448 remaining: u32::MAX,
449 reset_at: Instant::now() + Duration::from_secs(60),
450 };
451 }
452
453 let mut buckets = self.subscription_buckets.write().await;
454 let bucket = buckets
455 .entry(client_id)
456 .or_insert_with(|| RateLimitBucket::new(self.config.subscriptions_per_connection));
457
458 bucket.check_and_record(Instant::now(), None)
459 }
460
461 pub async fn check_message(&self, client_id: uuid::Uuid) -> RateLimitResult {
463 if !self.config.enabled {
464 return RateLimitResult::Allowed {
465 remaining: u32::MAX,
466 reset_at: Instant::now() + Duration::from_secs(60),
467 };
468 }
469
470 let mut buckets = self.message_buckets.write().await;
471 let bucket = buckets
472 .entry(client_id)
473 .or_insert_with(|| RateLimitBucket::new(self.config.messages_per_connection));
474
475 bucket.check_and_record(Instant::now(), None)
476 }
477
478 pub async fn check_snapshot(&self, client_id: uuid::Uuid) -> RateLimitResult {
480 if !self.config.enabled {
481 return RateLimitResult::Allowed {
482 remaining: u32::MAX,
483 reset_at: Instant::now() + Duration::from_secs(60),
484 };
485 }
486
487 let mut buckets = self.snapshot_buckets.write().await;
488 let bucket = buckets
489 .entry(client_id)
490 .or_insert_with(|| RateLimitBucket::new(self.config.snapshots_per_connection));
491
492 bucket.check_and_record(Instant::now(), None)
493 }
494
495 pub async fn cleanup_stale_buckets(&self) {
497 let now = Instant::now();
498
499 {
501 let mut buckets = self.ip_buckets.write().await;
502 buckets.retain(|_, bucket| {
503 bucket.prune_expired(now);
504 !bucket.requests.is_empty()
505 });
506 }
507
508 for buckets in [
510 &self.consumer_buckets,
511 &self.account_buckets,
512 &self.consumer_subscription_buckets,
513 &self.account_subscription_buckets,
514 ] {
515 let mut buckets = buckets.write().await;
516 buckets.retain(|_, bucket| {
517 bucket.prune_expired(now);
518 !bucket.requests.is_empty()
519 });
520 }
521
522 }
525
526 pub async fn remove_client_buckets(&self, client_id: uuid::Uuid) {
528 let mut sub_buckets = self.subscription_buckets.write().await;
529 sub_buckets.remove(&client_id);
530 drop(sub_buckets);
531
532 let mut msg_buckets = self.message_buckets.write().await;
533 msg_buckets.remove(&client_id);
534 drop(msg_buckets);
535
536 let mut snap_buckets = self.snapshot_buckets.write().await;
537 snap_buckets.remove(&client_id);
538 }
539
540 pub fn start_cleanup_task(&self) {
542 let limiter = self.clone();
543 tokio::spawn(async move {
544 let mut interval = tokio::time::interval(Duration::from_secs(60));
545 loop {
546 interval.tick().await;
547 limiter.cleanup_stale_buckets().await;
548 }
549 });
550 }
551}
552
553impl Clone for WebSocketRateLimiter {
554 fn clone(&self) -> Self {
555 Self {
556 config: self.config.clone(),
557 ip_buckets: Arc::clone(&self.ip_buckets),
558 consumer_buckets: Arc::clone(&self.consumer_buckets),
559 account_buckets: Arc::clone(&self.account_buckets),
560 consumer_subscription_buckets: Arc::clone(&self.consumer_subscription_buckets),
561 account_subscription_buckets: Arc::clone(&self.account_subscription_buckets),
562 subscription_buckets: Arc::clone(&self.subscription_buckets),
563 message_buckets: Arc::clone(&self.message_buckets),
564 snapshot_buckets: Arc::clone(&self.snapshot_buckets),
565 }
566 }
567}
568
569#[cfg(test)]
570mod tests {
571 use super::*;
572
573 fn test_config() -> RateLimiterConfig {
574 RateLimiterConfig {
575 enabled: true,
576 handshake_per_ip: RateLimitWindow::new(60, Duration::from_secs(60)).with_burst(10),
577 connections_per_consumer: RateLimitWindow::new(30, Duration::from_secs(60))
578 .with_burst(5),
579 connections_per_account: RateLimitWindow::new(100, Duration::from_secs(60))
580 .with_burst(20),
581 subscriptions_per_connection: RateLimitWindow::new(120, Duration::from_secs(60))
582 .with_burst(10),
583 messages_per_connection: RateLimitWindow::new(1000, Duration::from_secs(60))
584 .with_burst(100),
585 snapshots_per_connection: RateLimitWindow::new(30, Duration::from_secs(60))
586 .with_burst(5),
587 }
588 }
589
590 #[tokio::test]
591 async fn test_rate_limiter_allows_within_limit() {
592 let config = RateLimiterConfig {
593 handshake_per_ip: RateLimitWindow::new(5, Duration::from_secs(60)),
594 ..test_config()
595 };
596 let limiter = WebSocketRateLimiter::new(config);
597
598 let addr: SocketAddr = "127.0.0.1:12345".parse().unwrap();
599
600 for i in 0..5 {
602 let result = limiter.check_handshake(addr).await;
603 match result {
604 RateLimitResult::Allowed { remaining, .. } => {
605 assert_eq!(
606 remaining,
607 4 - i,
608 "Request {} should have {} remaining",
609 i,
610 4 - i
611 );
612 }
613 RateLimitResult::Denied { .. } => {
614 panic!("Request {} should be allowed", i);
615 }
616 }
617 }
618 }
619
620 #[tokio::test]
621 async fn test_rate_limiter_denies_over_limit() {
622 let config = RateLimiterConfig {
623 handshake_per_ip: RateLimitWindow::new(2, Duration::from_secs(60)),
624 ..test_config()
625 };
626 let limiter = WebSocketRateLimiter::new(config);
627
628 let addr: SocketAddr = "127.0.0.1:12345".parse().unwrap();
629
630 limiter.check_handshake(addr).await;
632 limiter.check_handshake(addr).await;
633
634 let result = limiter.check_handshake(addr).await;
636 assert!(
637 matches!(result, RateLimitResult::Denied { .. }),
638 "Third request should be denied"
639 );
640 }
641
642 #[tokio::test]
643 async fn test_rate_limiter_with_burst() {
644 let config = RateLimiterConfig {
645 handshake_per_ip: RateLimitWindow::new(2, Duration::from_secs(60)).with_burst(2),
646 ..test_config()
647 };
648 let limiter = WebSocketRateLimiter::new(config);
649
650 let addr: SocketAddr = "127.0.0.1:12345".parse().unwrap();
651
652 for i in 0..4 {
654 let result = limiter.check_handshake(addr).await;
655 assert!(
656 matches!(result, RateLimitResult::Allowed { .. }),
657 "Request {} should be allowed with burst",
658 i
659 );
660 }
661
662 let result = limiter.check_handshake(addr).await;
664 assert!(
665 matches!(result, RateLimitResult::Denied { .. }),
666 "Fifth request should be denied"
667 );
668 }
669
670 #[tokio::test]
671 async fn test_rate_limiter_disabled() {
672 let limiter = WebSocketRateLimiter::new(RateLimiterConfig::disabled());
673
674 let addr: SocketAddr = "127.0.0.1:12345".parse().unwrap();
675
676 for _ in 0..100 {
678 let result = limiter.check_handshake(addr).await;
679 assert!(
680 matches!(result, RateLimitResult::Allowed { .. }),
681 "Should be allowed when disabled"
682 );
683 }
684 }
685
686 #[tokio::test]
687 async fn test_consumer_rate_limiting() {
688 let config = RateLimiterConfig {
689 connections_per_consumer: RateLimitWindow::new(3, Duration::from_secs(60)),
690 ..test_config()
691 };
692 let limiter = WebSocketRateLimiter::new(config);
693
694 for i in 0..3 {
696 let result = limiter
697 .check_connection_for_consumer("user-123", None)
698 .await;
699 assert!(
700 matches!(result, RateLimitResult::Allowed { remaining, .. } if remaining == 2 - i),
701 "Connection {} should be allowed",
702 i
703 );
704 }
705
706 let result = limiter
708 .check_connection_for_consumer("user-123", None)
709 .await;
710 assert!(
711 matches!(result, RateLimitResult::Denied { .. }),
712 "Fourth connection should be denied"
713 );
714
715 let result = limiter
717 .check_connection_for_consumer("user-456", None)
718 .await;
719 assert!(
720 matches!(result, RateLimitResult::Allowed { .. }),
721 "Different consumer should be allowed"
722 );
723 }
724
725 #[tokio::test]
726 async fn signed_limits_override_configured_connection_windows() {
727 let limiter = WebSocketRateLimiter::new(test_config());
728
729 for _ in 0..2 {
731 assert!(matches!(
732 limiter
733 .check_connection_for_account("account:42", Some(2))
734 .await,
735 RateLimitResult::Allowed { .. }
736 ));
737 }
738 assert!(matches!(
739 limiter
740 .check_connection_for_account("account:42", Some(2))
741 .await,
742 RateLimitResult::Denied { .. }
743 ));
744
745 assert!(matches!(
747 limiter
748 .check_connection_for_account("account:43", Some(2))
749 .await,
750 RateLimitResult::Allowed { .. }
751 ));
752 }
753
754 #[tokio::test]
755 async fn subscription_create_limits_apply_only_when_signed() {
756 let limiter = WebSocketRateLimiter::new(test_config());
757
758 for _ in 0..10 {
760 assert!(matches!(
761 limiter
762 .check_subscription_create_for_account("account:42", None)
763 .await,
764 RateLimitResult::Allowed { .. }
765 ));
766 }
767 assert!(limiter.account_subscription_buckets.read().await.is_empty());
768
769 assert!(matches!(
771 limiter
772 .check_subscription_create_for_account("account:42", Some(1))
773 .await,
774 RateLimitResult::Allowed { .. }
775 ));
776 assert!(matches!(
777 limiter
778 .check_subscription_create_for_account("account:42", Some(1))
779 .await,
780 RateLimitResult::Denied { .. }
781 ));
782 assert!(matches!(
783 limiter
784 .check_subscription_create_for_consumer("consumer:a", Some(1))
785 .await,
786 RateLimitResult::Allowed { .. }
787 ));
788 }
789
790 #[tokio::test]
791 async fn test_cleanup_stale_buckets_removes_expired_buckets() {
792 let limiter = WebSocketRateLimiter::new(test_config());
793 let stale_request = Instant::now() - Duration::from_secs(600);
794
795 {
796 let mut buckets = limiter.ip_buckets.write().await;
797 let mut bucket = RateLimitBucket::new(limiter.config.handshake_per_ip);
798 bucket.requests.push(stale_request);
799 buckets.insert("127.0.0.1".to_string(), bucket);
800 }
801
802 {
803 let mut buckets = limiter.consumer_buckets.write().await;
804 let mut bucket = RateLimitBucket::new(limiter.config.connections_per_consumer);
805 bucket.requests.push(stale_request);
806 buckets.insert("user-123".to_string(), bucket);
807 }
808
809 {
810 let mut buckets = limiter.account_buckets.write().await;
811 let mut bucket = RateLimitBucket::new(limiter.config.connections_per_account);
812 bucket.requests.push(stale_request);
813 buckets.insert("account-123".to_string(), bucket);
814 }
815
816 limiter.cleanup_stale_buckets().await;
817
818 assert!(limiter.ip_buckets.read().await.is_empty());
819 assert!(limiter.consumer_buckets.read().await.is_empty());
820 assert!(limiter.account_buckets.read().await.is_empty());
821 }
822
823 #[tokio::test]
824 async fn test_remove_client_buckets_clears_connection_specific_state() {
825 let limiter = WebSocketRateLimiter::new(test_config());
826 let client_id = uuid::Uuid::new_v4();
827
828 let _ = limiter.check_subscription(client_id).await;
829 let _ = limiter.check_message(client_id).await;
830 let _ = limiter.check_snapshot(client_id).await;
831
832 assert!(limiter
833 .subscription_buckets
834 .read()
835 .await
836 .contains_key(&client_id));
837 assert!(limiter
838 .message_buckets
839 .read()
840 .await
841 .contains_key(&client_id));
842 assert!(limiter
843 .snapshot_buckets
844 .read()
845 .await
846 .contains_key(&client_id));
847
848 limiter.remove_client_buckets(client_id).await;
849
850 assert!(!limiter
851 .subscription_buckets
852 .read()
853 .await
854 .contains_key(&client_id));
855 assert!(!limiter
856 .message_buckets
857 .read()
858 .await
859 .contains_key(&client_id));
860 assert!(!limiter
861 .snapshot_buckets
862 .read()
863 .await
864 .contains_key(&client_id));
865 }
866}