Skip to main content

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}