1+ use std:: pin:: Pin ;
2+ use std:: sync:: Arc ;
13use std:: sync:: Mutex ;
2- use std:: time:: Instant ;
4+ use std:: sync:: atomic:: AtomicU64 ;
5+ use std:: task:: { Context , Poll } ;
6+ use std:: time:: { Duration , Instant } ;
37
8+ use bytes:: Bytes ;
9+ use futures_util:: Stream ;
10+ use objectstore_service:: PayloadStream ;
411use objectstore_service:: id:: ObjectContext ;
512use objectstore_types:: scope:: Scopes ;
613use serde:: { Deserialize , Serialize } ;
@@ -102,6 +109,7 @@ pub struct BandwidthLimits {
102109#[ derive( Debug ) ]
103110pub struct RateLimiter {
104111 config : RateLimits ,
112+ bandwidth : BandwidthRateLimiter ,
105113 global : Option < Mutex < TokenBucket > > ,
106114 // NB: These maps grow unbounded but we accept this as we expect an overall limited
107115 // number of usecases and scopes. We emit gauge metrics to monitor their size.
@@ -110,6 +118,64 @@ pub struct RateLimiter {
110118 rules : papaya:: HashMap < usize , Mutex < TokenBucket > > ,
111119}
112120
121+ #[ derive( Debug ) ]
122+ struct BandwidthRateLimiter {
123+ config : BandwidthLimits ,
124+ /// Accumulator that's incremented every time an operation that uses bandwidth is executed.
125+ accumulator : Arc < AtomicU64 > ,
126+ /// An estimate of the bandwidth that's currently being utilized in bytes per second.
127+ estimate : Arc < AtomicU64 > ,
128+ }
129+
130+ impl BandwidthRateLimiter {
131+ fn new ( config : BandwidthLimits ) -> Self {
132+ let accumulator = Arc :: new ( AtomicU64 :: new ( 0 ) ) ;
133+ let estimate = Arc :: new ( AtomicU64 :: new ( 0 ) ) ;
134+
135+ let accumulator_clone = Arc :: clone ( & accumulator) ;
136+ let estimate_clone = Arc :: clone ( & estimate) ;
137+ tokio:: task:: spawn ( async move {
138+ Self :: estimator ( accumulator_clone, estimate_clone) . await ;
139+ } ) ;
140+
141+ Self {
142+ config,
143+ accumulator,
144+ estimate,
145+ }
146+ }
147+
148+ /// Estimates the current bandwidth utilization using an exponentially weighted moving average.
149+ ///
150+ /// The calculation is based on the increments of `self.accumulator` happened in the last `TICK`.
151+ /// The estimate is stored in `self.estimate`, which can be queried for bandwidth-based rate-limiting.
152+ async fn estimator ( accumulator : Arc < AtomicU64 > , estimate : Arc < AtomicU64 > ) {
153+ const TICK : Duration = Duration :: from_millis ( 50 ) ; // Recompute EWMA on every TICK
154+ const ALPHA : f64 = 0.2 ; // EWMA alpha parameter: 20% weight to new sample, 80% to previous average
155+
156+ let mut interval = tokio:: time:: interval ( TICK ) ;
157+ let to_bps = 1.0 / TICK . as_secs_f64 ( ) ; // Conversion factor from bytes to bps
158+ let mut ewma: f64 = 0.0 ;
159+ loop {
160+ interval. tick ( ) . await ;
161+ let current = accumulator. swap ( 0 , std:: sync:: atomic:: Ordering :: Relaxed ) ;
162+ let bps = ( current as f64 ) * to_bps;
163+ ewma = ALPHA * bps + ( 1.0 - ALPHA ) * ewma;
164+
165+ let ewma_int = ewma. floor ( ) as u64 ;
166+ estimate. store ( ewma_int, std:: sync:: atomic:: Ordering :: Relaxed ) ;
167+ merni:: gauge!( "server.bandwidth.ewma" @b: ewma_int) ;
168+ }
169+ }
170+
171+ fn check ( & self ) -> bool {
172+ let Some ( bps) = self . config . global_bps else {
173+ return true ;
174+ } ;
175+ self . estimate . load ( std:: sync:: atomic:: Ordering :: Relaxed ) <= bps
176+ }
177+ }
178+
113179impl RateLimiter {
114180 pub fn new ( config : RateLimits ) -> Self {
115181 let global = config
@@ -118,19 +184,25 @@ impl RateLimiter {
118184 . map ( |rps| Mutex :: new ( TokenBucket :: new ( rps, config. throughput . burst ) ) ) ;
119185
120186 Self {
121- config,
187+ config : config. clone ( ) ,
188+ bandwidth : BandwidthRateLimiter :: new ( config. bandwidth ) ,
122189 global,
123190 usecases : papaya:: HashMap :: new ( ) ,
124191 scopes : papaya:: HashMap :: new ( ) ,
125192 rules : papaya:: HashMap :: new ( ) ,
126193 }
127194 }
128195
196+ /// Returns a reference to the shared bytes accumulator, used for bandwidth-based rate-limiting.
197+ pub fn bytes_accumulator ( & self ) -> Arc < AtomicU64 > {
198+ Arc :: clone ( & self . bandwidth . accumulator )
199+ }
200+
129201 /// Checks if the given context is within the rate limits.
130202 ///
131203 /// Returns `true` if the context is within the rate limits, `false` otherwise.
132204 pub fn check ( & self , context : & ObjectContext ) -> bool {
133- self . check_throughput ( context) && self . check_bandwidth ( context )
205+ self . check_throughput ( context) && self . bandwidth . check ( )
134206 }
135207
136208 fn check_throughput ( & self , context : & ObjectContext ) -> bool {
@@ -181,11 +253,6 @@ impl RateLimiter {
181253 true
182254 }
183255
184- fn check_bandwidth ( & self , _context : & ObjectContext ) -> bool {
185- // Not implemented
186- true
187- }
188-
189256 fn create_bucket ( & self , rps : u32 ) -> Mutex < TokenBucket > {
190257 Mutex :: new ( TokenBucket :: new ( rps, self . config . throughput . burst ) )
191258 }
@@ -273,3 +340,40 @@ impl TokenBucket {
273340 }
274341 }
275342}
343+
344+ /// A wrapper around a `PayloadStream` that measures bandwidth usage.
345+ ///
346+ /// This behaves exactly as a `PayloadStream`, except that every time an item is polled,
347+ /// the accumulator is incremented by the size of the returned `Bytes` chunk.
348+ pub ( crate ) struct MeteredPayloadStream {
349+ inner : PayloadStream ,
350+ accumulator : Arc < AtomicU64 > ,
351+ }
352+
353+ impl MeteredPayloadStream {
354+ pub fn from ( inner : PayloadStream , accumulator : Arc < AtomicU64 > ) -> Self {
355+ Self { inner, accumulator }
356+ }
357+ }
358+
359+ impl std:: fmt:: Debug for MeteredPayloadStream {
360+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
361+ f. debug_struct ( "MeteredPayloadStream" )
362+ . field ( "accumulator" , & self . accumulator )
363+ . finish ( )
364+ }
365+ }
366+
367+ impl Stream for MeteredPayloadStream {
368+ type Item = std:: io:: Result < Bytes > ;
369+
370+ fn poll_next ( self : Pin < & mut Self > , cx : & mut Context < ' _ > ) -> Poll < Option < Self :: Item > > {
371+ let this = self . get_mut ( ) ;
372+ let res = this. inner . as_mut ( ) . poll_next ( cx) ;
373+ if let Poll :: Ready ( Some ( Ok ( ref bytes) ) ) = res {
374+ this. accumulator
375+ . fetch_add ( bytes. len ( ) as u64 , std:: sync:: atomic:: Ordering :: Relaxed ) ;
376+ }
377+ res
378+ }
379+ }
0 commit comments