opendal_core/raw/futures_util.rs
1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements. See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership. The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License. You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied. See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use std::collections::VecDeque;
19use std::sync::Arc;
20use std::sync::atomic::AtomicUsize;
21use std::sync::atomic::Ordering;
22
23use futures::FutureExt;
24
25use crate::*;
26
27/// BoxedFuture is the type alias of [`futures::future::BoxFuture`].
28#[cfg(not(target_arch = "wasm32"))]
29pub type BoxedFuture<'a, T> = futures::future::BoxFuture<'a, T>;
30#[cfg(target_arch = "wasm32")]
31/// BoxedFuture is the type alias of [`futures::future::LocalBoxFuture`].
32pub type BoxedFuture<'a, T> = futures::future::LocalBoxFuture<'a, T>;
33
34/// BoxedStaticFuture is the type alias of [`futures::future::BoxFuture`].
35#[cfg(not(target_arch = "wasm32"))]
36pub type BoxedStaticFuture<T> = futures::future::BoxFuture<'static, T>;
37#[cfg(target_arch = "wasm32")]
38/// BoxedStaticFuture is the type alias of [`futures::future::LocalBoxFuture`].
39pub type BoxedStaticFuture<T> = futures::future::LocalBoxFuture<'static, T>;
40
41/// MaybeSend is a marker to determine whether a type is `Send` or not.
42/// We use this trait to wrap the `Send` requirement for wasm32 target.
43///
44/// # Safety
45///
46/// [`MaybeSend`] is equivalent to `Send` on non-wasm32 target.
47/// And it's empty trait on wasm32 target to indicate that a type is not `Send`.
48#[cfg(not(target_arch = "wasm32"))]
49pub trait MaybeSend: Send {}
50
51/// MaybeSend is a marker to determine whether a type is `Send` or not.
52/// We use this trait to wrap the `Send` requirement for wasm32 target.
53///
54/// # Safety
55///
56/// [`MaybeSend`] is equivalent to `Send` on non-wasm32 target.
57/// And it's empty trait on wasm32 target to indicate that a type is not `Send`.
58#[cfg(target_arch = "wasm32")]
59pub trait MaybeSend {}
60
61#[cfg(not(target_arch = "wasm32"))]
62impl<T: Send> MaybeSend for T {}
63#[cfg(target_arch = "wasm32")]
64impl<T> MaybeSend for T {}
65
66/// ConcurrentTasks executes tasks concurrently and collects outputs in submission order.
67///
68/// Submit inputs with [`Self::execute`] and collect outputs with [`Self::next`].
69/// The queue owns the task handles and tracks their completion for concurrency control.
70///
71/// ConcurrentTasks has two generic types:
72///
73/// - `I` represents the input type of the task.
74/// - `O` represents the output type of the task.
75///
76/// # Implementation Notes
77///
78/// The code patterns below are intentional; please do not modify them unless you fully understand these notes.
79///
80/// ```skip
81/// let result = self
82/// .tasks
83/// .front_mut() // Use `front_mut` instead of `pop_front`
84/// .expect("tasks must be available")
85/// .await;
86/// ...
87/// match result {
88/// Ok(o) => {
89/// let _ = self.tasks.pop_front(); // `pop_front` after got `Ok(o)`
90/// self.results.push_back(o)
91/// }
92/// Err((i, err)) => {
93/// if err.is_temporary() {
94/// let task = self.spawn_task(i);
95/// self.tasks
96/// .front_mut()
97/// .expect("tasks must be available")
98/// .replace(task) // Use replace here to instead of `push_front`
99/// } else {
100/// self.clear();
101/// self.errored = true;
102/// }
103/// return Err(err);
104/// }
105/// }
106/// ```
107///
108/// Please keep in mind that there is no guarantee the task will be `await`ed until completion. It's possible
109/// the task may be dropped before it resolves. Therefore, we should keep the `Task` in the `tasks` queue until
110/// it is resolved.
111///
112/// For example, users may have a timeout for the task, and the task will be dropped if it exceeds the timeout.
113/// If we `pop_front` the task before it resolves, the task will be canceled and the result will be lost.
114pub struct ConcurrentTasks<I, O> {
115 /// The executor to execute the tasks.
116 ///
117 /// If user doesn't provide an executor, the tasks will be executed with the default executor.
118 executor: Executor,
119 /// The factory to create the task.
120 ///
121 /// Caller of ConcurrentTasks must provide a factory to create the task for executing.
122 ///
123 /// The factory must accept an input and return a future that resolves to a tuple of input and
124 /// output result. If the given result is error, the error will be returned to users and the
125 /// task will be retried.
126 factory: fn(I) -> BoxedStaticFuture<(I, Result<O>)>,
127
128 /// `tasks` holds the ongoing tasks.
129 ///
130 /// Please keep in mind that all tasks are running in the background by `Executor`. We only need
131 /// to poll the tasks to see if they are ready.
132 ///
133 /// Dropping task without `await` it will cancel the task.
134 tasks: VecDeque<Task<Result<O, (I, Error)>>>,
135 /// `results` stores the successful results.
136 results: VecDeque<O>,
137
138 /// The maximum number of concurrent tasks.
139 concurrent: usize,
140 /// The maximum number of completed tasks that can be buffered.
141 prefetch: usize,
142 /// Tracks the number of tasks that have finished execution but have not yet been collected.
143 /// This count is subtracted from the total concurrency capacity, ensuring that the system
144 /// always schedules new tasks to maintain the user's desired concurrency level.
145 ///
146 /// Example: If `concurrency = 10` and `completed_but_unretrieved = 3`,
147 /// the system can still spawn 7 new tasks (since 3 slots are "logically occupied"
148 /// by uncollected results).
149 completed_but_unretrieved: Arc<AtomicUsize>,
150 /// hitting the last unrecoverable error.
151 ///
152 /// If concurrent tasks hit an unrecoverable error, it will stop executing new tasks and return
153 /// an unrecoverable error to users.
154 errored: bool,
155}
156
157impl<I: Send + 'static, O: Send + 'static> ConcurrentTasks<I, O> {
158 /// Create a new concurrent tasks with given executor, concurrent, prefetch and factory.
159 ///
160 /// `prefetch` is extra completed tasks allowed in the queue before
161 /// [`Self::execute`] waits to collect the head. Pass `0` when the caller
162 /// does not collect results while still submitting work; otherwise failed
163 /// tasks keep their inputs until collection.
164 ///
165 /// The factory is a function pointer that shouldn't capture any context.
166 pub fn new(
167 executor: Executor,
168 concurrent: usize,
169 prefetch: usize,
170 factory: fn(I) -> BoxedStaticFuture<(I, Result<O>)>,
171 ) -> Self {
172 Self {
173 executor,
174 factory,
175
176 tasks: VecDeque::with_capacity(concurrent),
177 results: VecDeque::with_capacity(concurrent),
178 concurrent,
179 prefetch,
180 completed_but_unretrieved: Arc::default(),
181 errored: false,
182 }
183 }
184
185 /// Return true if the tasks are running concurrently.
186 #[inline]
187 fn is_concurrent(&self) -> bool {
188 self.concurrent > 1
189 }
190
191 /// Clear all tasks and results.
192 ///
193 /// All ongoing tasks will be canceled.
194 pub fn clear(&mut self) {
195 self.tasks.clear();
196 self.results.clear();
197 }
198
199 /// Check if there are remaining space to push new tasks.
200 #[inline]
201 pub fn has_remaining(&self) -> bool {
202 let completed = self.completed_but_unretrieved.load(Ordering::Relaxed);
203 // Allow up to `prefetch` completed tasks to be buffered
204 self.tasks.len() < self.concurrent + completed.min(self.prefetch)
205 }
206
207 /// Chunk if there are remaining results to fetch.
208 #[inline]
209 pub fn has_result(&self) -> bool {
210 !self.results.is_empty()
211 }
212
213 fn spawn_task(&self, input: I) -> Task<Result<O, (I, Error)>> {
214 let completed = self.completed_but_unretrieved.clone();
215 let fut = (self.factory)(input)
216 // Completed tasks can remain queued while the caller produces more work.
217 // Only failures need to retain their input for a retry.
218 .map(|(input, result)| result.map_err(|err| (input, err)))
219 .inspect(move |_| {
220 completed.fetch_add(1, Ordering::Relaxed);
221 });
222
223 self.executor.execute(fut)
224 }
225
226 /// Execute the task with given input.
227 ///
228 /// - Execute the task in the current thread if is not concurrent.
229 /// - Execute the task in the background if there are available slots.
230 /// - Await the first task in the queue if there is no available slots.
231 pub async fn execute(&mut self, input: I) -> Result<()> {
232 if self.errored {
233 return Err(Error::new(
234 ErrorKind::Unexpected,
235 "concurrent tasks met an unrecoverable error",
236 ));
237 }
238
239 // Short path for non-concurrent case.
240 if !self.is_concurrent() {
241 let (_, o) = (self.factory)(input).await;
242 return match o {
243 Ok(o) => {
244 self.results.push_back(o);
245 Ok(())
246 }
247 // We don't need to rebuild the future if it's not concurrent.
248 Err(err) => Err(err),
249 };
250 }
251
252 if !self.has_remaining() {
253 let result = self
254 .tasks
255 .front_mut()
256 .expect("tasks must be available")
257 .await;
258 self.completed_but_unretrieved
259 .fetch_sub(1, Ordering::Relaxed);
260 match result {
261 Ok(o) => {
262 let _ = self.tasks.pop_front();
263 self.results.push_back(o)
264 }
265 Err((i, err)) => {
266 // Retry this task if the error is temporary
267 if err.is_temporary() {
268 let task = self.spawn_task(i);
269 self.tasks
270 .front_mut()
271 .expect("tasks must be available")
272 .replace(task)
273 } else {
274 self.clear();
275 self.errored = true;
276 }
277 return Err(err);
278 }
279 }
280 }
281
282 self.tasks.push_back(self.spawn_task(input));
283 Ok(())
284 }
285
286 /// Fetch the successful result from the result queue.
287 pub async fn next(&mut self) -> Option<Result<O>> {
288 if self.errored {
289 return Some(Err(Error::new(
290 ErrorKind::Unexpected,
291 "concurrent tasks met an unrecoverable error",
292 )));
293 }
294
295 if let Some(result) = self.results.pop_front() {
296 return Some(Ok(result));
297 }
298
299 if let Some(task) = self.tasks.front_mut() {
300 let result = task.await;
301 self.completed_but_unretrieved
302 .fetch_sub(1, Ordering::Relaxed);
303 return match result {
304 Ok(o) => {
305 let _ = self.tasks.pop_front();
306 Some(Ok(o))
307 }
308 Err((i, err)) => {
309 // Retry this task if the error is temporary
310 if err.is_temporary() {
311 let task = self.spawn_task(i);
312 self.tasks
313 .front_mut()
314 .expect("tasks must be available")
315 .replace(task)
316 } else {
317 self.clear();
318 self.errored = true;
319 }
320 Some(Err(err))
321 }
322 };
323 }
324
325 None
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use pretty_assertions::assert_eq;
332 use rand::RngExt;
333 use tokio::time::sleep;
334
335 use super::*;
336 use crate::raw::Duration;
337
338 #[tokio::test]
339 async fn test_concurrent_tasks() {
340 let executor = Executor::new();
341
342 let mut tasks = ConcurrentTasks::new(executor, 16, 8, |(i, dur)| {
343 Box::pin(async move {
344 sleep(dur).await;
345
346 // 5% rate to fail.
347 if rand::rng().random_range(0..100) > 90 {
348 return (
349 (i, dur),
350 Err(Error::new(ErrorKind::Unexpected, "I'm lucky").set_temporary()),
351 );
352 }
353 ((i, dur), Ok(i))
354 })
355 });
356
357 let mut ans = vec![];
358
359 for i in 0..10240 {
360 // Sleep up to 10ms
361 let dur = Duration::from_millis(rand::rng().random_range(0..10));
362 loop {
363 let res = tasks.execute((i, dur)).await;
364 if res.is_ok() {
365 break;
366 }
367 }
368 }
369
370 loop {
371 match tasks.next().await.transpose() {
372 Ok(Some(i)) => ans.push(i),
373 Ok(None) => break,
374 Err(_) => continue,
375 }
376 }
377
378 assert_eq!(ans, (0..10240).collect::<Vec<_>>())
379 }
380
381 #[tokio::test]
382 async fn test_prefetch_backpressure() {
383 let executor = Executor::new();
384 let concurrent = 4;
385 let prefetch = 2;
386
387 // Create a slower task to ensure they don't complete immediately
388 let mut tasks = ConcurrentTasks::new(executor, concurrent, prefetch, |i: usize| {
389 Box::pin(async move {
390 sleep(Duration::from_millis(100)).await;
391 (i, Ok(i))
392 })
393 });
394
395 // Initially, we should have space for concurrent tasks
396 assert!(tasks.has_remaining(), "Should have space initially");
397
398 // Submit concurrent tasks
399 for i in 0..concurrent {
400 assert!(tasks.has_remaining(), "Should have space for task {i}");
401 tasks.execute(i).await.unwrap();
402 }
403
404 // Now we shouldn't have any more space (since no tasks have completed yet)
405 assert!(
406 !tasks.has_remaining(),
407 "Should not have space after submitting concurrent tasks"
408 );
409
410 // Wait for some tasks to complete
411 sleep(Duration::from_millis(150)).await;
412
413 // Now we should have space up to prefetch limit
414 for i in concurrent..concurrent + prefetch {
415 assert!(
416 tasks.has_remaining(),
417 "Should have space for prefetch task {i}"
418 );
419 tasks.execute(i).await.unwrap();
420 }
421
422 // Now has_remaining should return false
423 assert!(
424 !tasks.has_remaining(),
425 "Should not have remaining space after filling up prefetch buffer"
426 );
427
428 // Retrieve one result
429 let result = tasks.next().await;
430 assert!(result.is_some());
431
432 // Now there should be space for one more task
433 assert!(
434 tasks.has_remaining(),
435 "Should have remaining space after retrieving one result"
436 );
437 }
438
439 #[tokio::test]
440 async fn test_prefetch_zero() {
441 let executor = Executor::new();
442 let concurrent = 4;
443 let prefetch = 0; // No prefetching allowed
444
445 let mut tasks = ConcurrentTasks::new(executor, concurrent, prefetch, |i: usize| {
446 Box::pin(async move {
447 sleep(Duration::from_millis(10)).await;
448 (i, Ok(i))
449 })
450 });
451
452 // With prefetch=0, we can only submit up to concurrent tasks
453 for i in 0..concurrent {
454 tasks.execute(i).await.unwrap();
455 }
456
457 // Should not have space for more
458 assert!(
459 !tasks.has_remaining(),
460 "Should not have remaining space with prefetch=0"
461 );
462
463 // Retrieve one result
464 let result = tasks.next().await;
465 assert!(result.is_some());
466
467 // Now there should be space for exactly one more task
468 assert!(
469 tasks.has_remaining(),
470 "Should have remaining space after retrieving one result"
471 );
472
473 // Execute one more
474 tasks.execute(concurrent).await.unwrap();
475
476 // Should be full again
477 assert!(!tasks.has_remaining(), "Should be full again");
478 }
479}