haste_repository/pg/
rate_limit.rs1use std::{pin::Pin, sync::LazyLock};
2
3use crate::pg::{
4 PGConnection,
5 utilities::{commit_transaction, create_transaction},
6};
7use haste_rate_limit::{RateLimit, RateLimitError};
8use moka::future::{Cache, CacheBuilder};
9use sqlx::PgExecutor;
10
11#[derive(Clone, Copy)]
12enum RateLimitState {
13 Count(i32),
14 Max,
15}
16
17static MEMORY: LazyLock<Cache<String, RateLimitState>> = LazyLock::new(
18 || {
20 CacheBuilder::new(10_000)
21 .time_to_idle(std::time::Duration::from_secs(30))
22 .build()
23 },
24);
25
26async fn check_rate_limit_remote_with_executor<'a, 'e, E>(
27 executor: E,
28 rate_key: &'a str,
29 max: i32,
30 points: i32,
31 window_in_seconds: i32,
32) -> Result<i32, haste_rate_limit::RateLimitError>
33where
34 E: PgExecutor<'e>,
35{
36 let result =
37 sqlx::query_as::<_, (i32,)>("SELECT check_rate_limit($1, $2, $3, $4) as current_limit")
38 .bind(rate_key)
39 .bind(max)
40 .bind(points)
41 .bind(window_in_seconds)
42 .fetch_one(executor)
43 .await
44 .map_err(|e| RateLimitError::Error(e.to_string()))?;
45
46 Ok(result.0)
47}
48
49async fn check_rate_limit_remote(
50 pg: PGConnection,
51 rate_key: &str,
52 max: i32,
53 points: i32,
54 window_in_seconds: i32,
55) -> Result<i32, haste_rate_limit::RateLimitError> {
56 match &pg {
57 PGConnection::Pool(_pool, _) => {
58 let tx = create_transaction(&pg, true)
59 .await
60 .map_err(|e| RateLimitError::Error(e.to_string()))?;
61 let res = {
62 let mut conn = tx.lock().await;
63 check_rate_limit_remote_with_executor(
64 &mut **conn,
65 rate_key,
66 max,
67 points,
68 window_in_seconds,
69 )
70 .await?
71 };
72 commit_transaction(tx)
73 .await
74 .map_err(|e| RateLimitError::Error(e.to_string()))?;
75 Ok(res)
76 }
77 PGConnection::Transaction(tx, _) => {
78 let mut tx = tx.lock().await;
79 check_rate_limit_remote_with_executor(
80 &mut **tx,
81 rate_key,
82 max,
83 points,
84 window_in_seconds,
85 )
86 .await
87 }
88 }
89}
90
91async fn check_rate_limit(
92 connection: PGConnection,
93 rate_key: &str,
94 max: i32,
95 points: i32,
96 window_in_seconds: i32,
97) -> Result<i32, haste_rate_limit::RateLimitError> {
98 if let Some(current) = MEMORY.get(rate_key).await {
99 let cloned_key = rate_key.to_string();
100 let connection_clone = connection.clone();
101
102 tokio::spawn(async move {
103 let result = check_rate_limit_remote(
104 connection_clone,
105 &cloned_key,
106 max,
107 points,
108 window_in_seconds,
109 )
110 .await;
111
112 if let Ok(points) = result {
113 MEMORY
114 .insert(cloned_key, RateLimitState::Count(points))
115 .await;
116 } else if let Err(e) = result {
117 match e {
118 RateLimitError::Exceeded => {
119 MEMORY.insert(cloned_key, RateLimitState::Max).await;
120 }
121 RateLimitError::Error(e) => {
122 println!("Error checking rate limit: {e:?}");
123 }
124 }
125 }
126 });
127
128 match current {
129 RateLimitState::Count(current) => {
130 let current_score = current + points;
131
132 if current_score > max {
133 Err(RateLimitError::Exceeded)
134 } else {
135 MEMORY
136 .insert(rate_key.to_string(), RateLimitState::Count(current_score))
137 .await;
138 Ok(current_score)
139 }
140 }
141 RateLimitState::Max => Err(RateLimitError::Exceeded),
142 }
143 } else {
144 let result =
145 check_rate_limit_remote(connection, rate_key, max, points, window_in_seconds).await?;
146
147 MEMORY
148 .insert(rate_key.to_string(), RateLimitState::Count(result))
149 .await;
150
151 Ok(result)
152 }
153}
154
155impl RateLimit for PGConnection {
156 fn check<'a>(
159 &'a self,
160 rate_key: &'a str,
161 max: i32,
162 points: i32,
163 window_in_seconds: i32,
164 ) -> Pin<Box<dyn Future<Output = Result<i32, haste_rate_limit::RateLimitError>> + Send + 'a>>
165 {
166 let connection = self.clone();
167 Box::pin(async move {
168 let res =
169 check_rate_limit(connection, rate_key, max, points, window_in_seconds).await?;
170 Ok(res)
171 })
172 }
173}