deribit_http/
rate_limit.rs1use crate::sleep_compat::sleep;
8use crate::sync_compat::Mutex;
9use crate::time_compat::Instant;
10use std::collections::HashMap;
11use std::sync::Arc;
12use std::time::Duration;
13
14#[derive(Debug, Clone)]
16pub struct RateLimiter {
17 limiters: Arc<Mutex<HashMap<RateLimitCategory, TokenBucket>>>,
18}
19
20#[derive(Debug, Clone, Hash, Eq, PartialEq)]
22pub enum RateLimitCategory {
23 Trading,
25 MarketData,
27 Account,
29 Auth,
31 General,
33}
34
35#[derive(Debug)]
37struct TokenBucket {
38 capacity: u32,
40 tokens: u32,
42 refill_rate: u32,
44 last_refill: Instant,
46}
47
48impl TokenBucket {
49 fn new(capacity: u32, refill_rate: u32) -> Self {
51 Self {
52 capacity,
53 tokens: capacity,
54 refill_rate,
55 last_refill: Instant::now(),
56 }
57 }
58
59 fn try_consume(&mut self) -> bool {
61 self.refill();
62 if self.tokens > 0 {
63 self.tokens -= 1;
64 true
65 } else {
66 false
67 }
68 }
69
70 fn time_until_token(&self) -> Duration {
72 if self.tokens > 0 {
73 Duration::from_secs(0)
74 } else {
75 Duration::from_secs_f64(1.0 / self.refill_rate as f64)
76 }
77 }
78
79 fn refill(&mut self) {
81 let now = Instant::now();
82 let elapsed = now.duration_since(self.last_refill);
83 let tokens_to_add = (elapsed.as_secs_f64() * self.refill_rate as f64) as u32;
84
85 if tokens_to_add > 0 {
86 self.tokens = (self.tokens + tokens_to_add).min(self.capacity);
87 self.last_refill = now;
88 }
89 }
90}
91
92impl RateLimiter {
93 pub fn new() -> Self {
95 let mut limiters = HashMap::new();
96
97 limiters.insert(RateLimitCategory::Trading, TokenBucket::new(250, 200));
100
101 limiters.insert(RateLimitCategory::MarketData, TokenBucket::new(500, 400));
103
104 limiters.insert(RateLimitCategory::Account, TokenBucket::new(200, 150));
106
107 limiters.insert(RateLimitCategory::Auth, TokenBucket::new(50, 30));
109
110 limiters.insert(RateLimitCategory::General, TokenBucket::new(300, 200));
112
113 Self {
114 limiters: Arc::new(Mutex::new(limiters)),
115 }
116 }
117
118 pub async fn wait_for_permission(&self, category: RateLimitCategory) {
120 loop {
121 let wait_time = {
122 let mut limiters = self.limiters.lock().await;
123 let bucket = limiters
124 .get_mut(&category)
125 .expect("Rate limit category should exist");
126
127 if bucket.try_consume() {
128 return; } else {
130 bucket.time_until_token()
131 }
132 };
133
134 sleep(wait_time.max(Duration::from_millis(10))).await;
136 }
137 }
138
139 pub async fn check_permission(&self, category: RateLimitCategory) -> bool {
141 let mut limiters = self.limiters.lock().await;
142 let bucket = limiters
143 .get_mut(&category)
144 .expect("Rate limit category should exist");
145 bucket.try_consume()
146 }
147
148 pub async fn get_tokens(&self, category: RateLimitCategory) -> u32 {
150 let mut limiters = self.limiters.lock().await;
151 let bucket = limiters
152 .get_mut(&category)
153 .expect("Rate limit category should exist");
154 bucket.refill();
155 bucket.tokens
156 }
157}
158
159impl Default for RateLimiter {
160 fn default() -> Self {
161 Self::new()
162 }
163}
164
165pub fn categorize_endpoint(endpoint: &str) -> RateLimitCategory {
167 if endpoint.contains("/private/buy")
168 || endpoint.contains("/private/sell")
169 || endpoint.contains("/private/cancel")
170 || endpoint.contains("/private/edit")
171 {
172 RateLimitCategory::Trading
173 } else if endpoint.contains("/public/ticker")
174 || endpoint.contains("/public/get_order_book")
175 || endpoint.contains("/public/get_last_trades")
176 || endpoint.contains("/public/get_instruments")
177 {
178 RateLimitCategory::MarketData
179 } else if endpoint.contains("/private/get_account_summary")
180 || endpoint.contains("/private/get_positions")
181 || endpoint.contains("/private/get_subaccounts")
182 {
183 RateLimitCategory::Account
184 } else if endpoint.contains("/public/auth") || endpoint.contains("/private/logout") {
185 RateLimitCategory::Auth
186 } else {
187 RateLimitCategory::General
188 }
189}
190
191#[cfg(all(test, not(target_arch = "wasm32")))]
192mod tests {
193 use super::*;
194 use crate::sleep_compat::sleep;
195
196 #[tokio::test]
197 async fn test_token_bucket_basic() {
198 let mut bucket = TokenBucket::new(10, 5);
199
200 for _ in 0..10 {
202 assert!(bucket.try_consume());
203 }
204
205 assert!(!bucket.try_consume());
207 }
208
209 #[tokio::test]
210 async fn test_token_bucket_refill() {
211 let mut bucket = TokenBucket::new(5, 10); for _ in 0..5 {
215 assert!(bucket.try_consume());
216 }
217 assert!(!bucket.try_consume());
218
219 sleep(Duration::from_millis(200)).await;
221
222 assert!(bucket.try_consume());
224 }
225
226 #[tokio::test]
227 async fn test_rate_limiter() {
228 let limiter = RateLimiter::new();
229
230 assert!(limiter.check_permission(RateLimitCategory::Trading).await);
232
233 limiter
235 .wait_for_permission(RateLimitCategory::MarketData)
236 .await;
237 }
239
240 #[test]
241 fn test_endpoint_categorization() {
242 assert_eq!(
243 categorize_endpoint("/private/buy"),
244 RateLimitCategory::Trading
245 );
246 assert_eq!(
247 categorize_endpoint("/public/ticker"),
248 RateLimitCategory::MarketData
249 );
250 assert_eq!(
251 categorize_endpoint("/private/get_account_summary"),
252 RateLimitCategory::Account
253 );
254 assert_eq!(categorize_endpoint("/public/auth"), RateLimitCategory::Auth);
255 assert_eq!(
256 categorize_endpoint("/public/get_time"),
257 RateLimitCategory::General
258 );
259 }
260}