Skip to content

Commit e59eb51

Browse files
committed
fix(layers/throttle): limit read bandwidth
1 parent a5de706 commit e59eb51

3 files changed

Lines changed: 80 additions & 23 deletions

File tree

core/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/layers/throttle/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,4 @@ opendal-core = { path = "../../core", version = "0.56.0", default-features = fal
3636

3737
[dev-dependencies]
3838
opendal-core = { path = "../../core", version = "0.56.0" }
39+
tokio = { workspace = true, features = ["macros", "rt"] }

core/layers/throttle/src/lib.rs

Lines changed: 78 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -166,36 +166,39 @@ impl<R> ThrottleWrapper<R> {
166166
}
167167
}
168168

169+
async fn wait_for_quota(limiter: &SharedRateLimiter, len: usize) -> Result<()> {
170+
if len == 0 {
171+
return Ok(());
172+
}
173+
174+
if len > u32::MAX as usize {
175+
return Err(Error::new(
176+
ErrorKind::RateLimited,
177+
"request size exceeds throttle quota capacity",
178+
));
179+
}
180+
181+
let buf_length = NonZeroU32::new(len as u32).expect("len is non-zero so NonZeroU32 must exist");
182+
183+
limiter.until_n_ready(buf_length).await.map_err(|_| {
184+
Error::new(
185+
ErrorKind::RateLimited,
186+
"burst size is smaller than the request size",
187+
)
188+
})
189+
}
190+
169191
impl<R: oio::Read> oio::Read for ThrottleWrapper<R> {
170192
async fn read(&mut self) -> Result<Buffer> {
171-
self.inner.read().await
193+
let bs = self.inner.read().await?;
194+
wait_for_quota(&self.limiter, bs.len()).await?;
195+
Ok(bs)
172196
}
173197
}
174198

175199
impl<R: oio::Write> oio::Write for ThrottleWrapper<R> {
176200
async fn write(&mut self, bs: Buffer) -> Result<()> {
177-
let len = bs.len();
178-
if len == 0 {
179-
return self.inner.write(bs).await;
180-
}
181-
182-
if len > u32::MAX as usize {
183-
return Err(Error::new(
184-
ErrorKind::RateLimited,
185-
"request size exceeds throttle quota capacity",
186-
));
187-
}
188-
189-
let buf_length =
190-
NonZeroU32::new(len as u32).expect("len is non-zero so NonZeroU32 must exist");
191-
192-
self.limiter.until_n_ready(buf_length).await.map_err(|_| {
193-
Error::new(
194-
ErrorKind::RateLimited,
195-
"burst size is smaller than the request size",
196-
)
197-
})?;
198-
201+
wait_for_quota(&self.limiter, bs.len()).await?;
199202
self.inner.write(bs).await
200203
}
201204

@@ -207,3 +210,55 @@ impl<R: oio::Write> oio::Write for ThrottleWrapper<R> {
207210
self.inner.close().await
208211
}
209212
}
213+
214+
#[cfg(test)]
215+
mod tests {
216+
use super::*;
217+
use opendal_core::raw::oio::Read;
218+
219+
struct SingleRead {
220+
data: Option<Buffer>,
221+
}
222+
223+
impl oio::Read for SingleRead {
224+
async fn read(&mut self) -> Result<Buffer> {
225+
Ok(self.data.take().unwrap_or_default())
226+
}
227+
}
228+
229+
fn new_limiter(bandwidth: u32, burst: u32) -> SharedRateLimiter {
230+
Arc::new(RateLimiter::direct(
231+
Quota::per_second(NonZeroU32::new(bandwidth).unwrap())
232+
.allow_burst(NonZeroU32::new(burst).unwrap()),
233+
))
234+
}
235+
236+
#[tokio::test]
237+
async fn read_allows_chunks_within_burst() {
238+
let mut reader = ThrottleWrapper::new(
239+
SingleRead {
240+
data: Some(Buffer::from(vec![0; 2])),
241+
},
242+
new_limiter(2, 2),
243+
);
244+
245+
let bs = reader.read().await.expect("read should pass throttle");
246+
assert_eq!(bs.len(), 2);
247+
}
248+
249+
#[tokio::test]
250+
async fn read_rejects_chunks_larger_than_burst() {
251+
let mut reader = ThrottleWrapper::new(
252+
SingleRead {
253+
data: Some(Buffer::from(vec![0; 2])),
254+
},
255+
new_limiter(1, 1),
256+
);
257+
258+
let err = reader
259+
.read()
260+
.await
261+
.expect_err("chunk larger than burst should fail");
262+
assert_eq!(err.kind(), ErrorKind::RateLimited);
263+
}
264+
}

0 commit comments

Comments
 (0)