@@ -8,6 +8,7 @@ use std::collections::BTreeMap;
88use anyhow:: Result ;
99use objectstore_server:: config:: { AuthZ , Config } ;
1010use objectstore_server:: killswitches:: { Killswitch , Killswitches } ;
11+ use objectstore_server:: rate_limits:: { RateLimits , ThroughputLimits , ThroughputRule } ;
1112use objectstore_test:: server:: TestServer ;
1213
1314#[ tokio:: test]
@@ -51,3 +52,221 @@ async fn test_killswitches() -> Result<()> {
5152
5253 Ok ( ( ) )
5354}
55+
56+ #[ tokio:: test]
57+ async fn test_throughput_global_rps_limit ( ) -> Result < ( ) > {
58+ let server = TestServer :: with_config ( Config {
59+ rate_limits : RateLimits {
60+ throughput : ThroughputLimits {
61+ global_rps : Some ( 2 ) ,
62+ burst : 1 ,
63+ ..Default :: default ( )
64+ } ,
65+ ..Default :: default ( )
66+ } ,
67+ ..Default :: default ( )
68+ } )
69+ . await ;
70+
71+ let client = reqwest:: Client :: new ( ) ;
72+
73+ // First three requests without waiting should succeed, using up both the regular and burst budget
74+ for _ in 0 ..3 {
75+ let response = client
76+ . get ( server. url ( "/v1/objects/test/org=1/nonexistent" ) )
77+ . send ( )
78+ . await ?;
79+ assert_eq ! ( response. status( ) , reqwest:: StatusCode :: NOT_FOUND ) ;
80+ }
81+
82+ // Fourth request without waiting should be rate limited
83+ let response = client
84+ . get ( server. url ( "/v1/objects/test/org=1/nonexistent" ) )
85+ . send ( )
86+ . await ?;
87+ assert_eq ! ( response. status( ) , reqwest:: StatusCode :: TOO_MANY_REQUESTS ) ;
88+
89+ // Refill bucket
90+ tokio:: time:: sleep ( tokio:: time:: Duration :: from_secs ( 1 ) ) . await ;
91+
92+ // After waiting, request succeeds
93+ let response = client
94+ . get ( server. url ( "/v1/objects/test/org=1/nonexistent" ) )
95+ . send ( )
96+ . await ?;
97+ assert_eq ! ( response. status( ) , reqwest:: StatusCode :: NOT_FOUND ) ;
98+
99+ Ok ( ( ) )
100+ }
101+
102+ #[ tokio:: test]
103+ async fn test_throughput_usecase_pct_limit ( ) -> Result < ( ) > {
104+ let server = TestServer :: with_config ( Config {
105+ rate_limits : RateLimits {
106+ throughput : ThroughputLimits {
107+ global_rps : Some ( 100 ) ,
108+ burst : 0 ,
109+ usecase_pct : Some ( 2 ) ,
110+ ..Default :: default ( )
111+ } ,
112+ ..Default :: default ( )
113+ } ,
114+ ..Default :: default ( )
115+ } )
116+ . await ;
117+
118+ let client = reqwest:: Client :: new ( ) ;
119+
120+ // First two requests to the same usecase without waiting should succeed
121+ for _ in 0 ..2 {
122+ let response = client
123+ . get ( server. url ( "/v1/objects/test/org=1/nonexistent" ) )
124+ . send ( )
125+ . await ?;
126+ assert_eq ! ( response. status( ) , reqwest:: StatusCode :: NOT_FOUND ) ;
127+ }
128+
129+ // Third request to the same usecase without waiting should fail
130+ let response = client
131+ . get ( server. url ( "/v1/objects/test/org=1/nonexistent" ) )
132+ . send ( )
133+ . await ?;
134+ assert_eq ! ( response. status( ) , reqwest:: StatusCode :: TOO_MANY_REQUESTS ) ;
135+
136+ // Request to a different usecase should succeed
137+ let response = client
138+ . get ( server. url ( "/v1/objects/other/org=1/nonexistent" ) )
139+ . send ( )
140+ . await ?;
141+ assert_eq ! ( response. status( ) , reqwest:: StatusCode :: NOT_FOUND ) ;
142+
143+ // Refill bucket
144+ tokio:: time:: sleep ( tokio:: time:: Duration :: from_secs ( 1 ) ) . await ;
145+
146+ // After waiting, request to first usecase should succeed again
147+ let response = client
148+ . get ( server. url ( "/v1/objects/test/org=1/nonexistent" ) )
149+ . send ( )
150+ . await ?;
151+ assert_eq ! ( response. status( ) , reqwest:: StatusCode :: NOT_FOUND ) ;
152+
153+ Ok ( ( ) )
154+ }
155+
156+ #[ tokio:: test]
157+ async fn test_throughput_scope_pct_limit ( ) -> Result < ( ) > {
158+ let server = TestServer :: with_config ( Config {
159+ rate_limits : RateLimits {
160+ throughput : ThroughputLimits {
161+ global_rps : Some ( 100 ) ,
162+ burst : 0 ,
163+ scope_pct : Some ( 2 ) ,
164+ ..Default :: default ( )
165+ } ,
166+ ..Default :: default ( )
167+ } ,
168+ ..Default :: default ( )
169+ } )
170+ . await ;
171+
172+ let client = reqwest:: Client :: new ( ) ;
173+
174+ // First two requests to the same scope without waiting should succeed
175+ for _ in 0 ..2 {
176+ let response = client
177+ . get ( server. url ( "/v1/objects/test/org=1/nonexistent" ) )
178+ . send ( )
179+ . await ?;
180+ assert_eq ! ( response. status( ) , reqwest:: StatusCode :: NOT_FOUND ) ;
181+ }
182+
183+ // Third request to the same scope without waiting should fail
184+ let response = client
185+ . get ( server. url ( "/v1/objects/test/org=1/nonexistent" ) )
186+ . send ( )
187+ . await ?;
188+ assert_eq ! ( response. status( ) , reqwest:: StatusCode :: TOO_MANY_REQUESTS ) ;
189+
190+ // Request to a different scope should succeed
191+ let response = client
192+ . get ( server. url ( "/v1/objects/test/org=2/nonexistent" ) )
193+ . send ( )
194+ . await ?;
195+ assert_eq ! ( response. status( ) , reqwest:: StatusCode :: NOT_FOUND ) ;
196+
197+ // Refill bucket
198+ tokio:: time:: sleep ( tokio:: time:: Duration :: from_secs ( 1 ) ) . await ;
199+
200+ // After waiting, request to first scope should succeed again
201+ let response = client
202+ . get ( server. url ( "/v1/objects/test/org=1/nonexistent" ) )
203+ . send ( )
204+ . await ?;
205+ assert_eq ! ( response. status( ) , reqwest:: StatusCode :: NOT_FOUND ) ;
206+
207+ Ok ( ( ) )
208+ }
209+
210+ #[ tokio:: test]
211+ async fn test_throughput_rule ( ) -> Result < ( ) > {
212+ let server = TestServer :: with_config ( Config {
213+ rate_limits : RateLimits {
214+ throughput : ThroughputLimits {
215+ global_rps : None ,
216+ burst : 0 ,
217+ rules : vec ! [ ThroughputRule {
218+ usecase: Some ( "restricted" . to_string( ) ) ,
219+ scopes: vec![ ( "org" . to_string( ) , "42" . to_string( ) ) ] ,
220+ rps: Some ( 1 ) ,
221+ pct: None ,
222+ } ] ,
223+ ..Default :: default ( )
224+ } ,
225+ ..Default :: default ( )
226+ } ,
227+ ..Default :: default ( )
228+ } )
229+ . await ;
230+
231+ let client = reqwest:: Client :: new ( ) ;
232+
233+ // First request matching rule should succeed
234+ let response = client
235+ . get ( server. url ( "/v1/objects/restricted/org=42/nonexistent" ) )
236+ . send ( )
237+ . await ?;
238+ assert_eq ! ( response. status( ) , reqwest:: StatusCode :: NOT_FOUND ) ;
239+
240+ // Second request matching rule should fail
241+ let response = client
242+ . get ( server. url ( "/v1/objects/restricted/org=42/nonexistent" ) )
243+ . send ( )
244+ . await ?;
245+ assert_eq ! ( response. status( ) , reqwest:: StatusCode :: TOO_MANY_REQUESTS ) ;
246+
247+ // Different usecase should not be affected by rule
248+ let response = client
249+ . get ( server. url ( "/v1/objects/other/org=42/nonexistent" ) )
250+ . send ( )
251+ . await ?;
252+ assert_eq ! ( response. status( ) , reqwest:: StatusCode :: NOT_FOUND ) ;
253+
254+ // Same usecase but different scope should not be affected by rule
255+ let response = client
256+ . get ( server. url ( "/v1/objects/restricted/org=43/nonexistent" ) )
257+ . send ( )
258+ . await ?;
259+ assert_eq ! ( response. status( ) , reqwest:: StatusCode :: NOT_FOUND ) ;
260+
261+ // Refill bucket
262+ tokio:: time:: sleep ( tokio:: time:: Duration :: from_secs ( 1 ) ) . await ;
263+
264+ // After waiting, request matching rule should succeed again
265+ let response = client
266+ . get ( server. url ( "/v1/objects/restricted/org=42/nonexistent" ) )
267+ . send ( )
268+ . await ?;
269+ assert_eq ! ( response. status( ) , reqwest:: StatusCode :: NOT_FOUND ) ;
270+
271+ Ok ( ( ) )
272+ }
0 commit comments