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    /// The factory is a function pointer that shouldn't capture any context.
161    pub fn new(
162        executor: Executor,
163        concurrent: usize,
164        prefetch: usize,
165        factory: fn(I) -> BoxedStaticFuture<(I, Result<O>)>,
166    ) -> Self {
167        Self {
168            executor,
169            factory,
170
171            tasks: VecDeque::with_capacity(concurrent),
172            results: VecDeque::with_capacity(concurrent),
173            concurrent,
174            prefetch,
175            completed_but_unretrieved: Arc::default(),
176            errored: false,
177        }
178    }
179
180    /// Return true if the tasks are running concurrently.
181    #[inline]
182    fn is_concurrent(&self) -> bool {
183        self.concurrent > 1
184    }
185
186    /// Clear all tasks and results.
187    ///
188    /// All ongoing tasks will be canceled.
189    pub fn clear(&mut self) {
190        self.tasks.clear();
191        self.results.clear();
192    }
193
194    /// Check if there are remaining space to push new tasks.
195    #[inline]
196    pub fn has_remaining(&self) -> bool {
197        let completed = self.completed_but_unretrieved.load(Ordering::Relaxed);
198        // Allow up to `prefetch` completed tasks to be buffered
199        self.tasks.len() < self.concurrent + completed.min(self.prefetch)
200    }
201
202    /// Chunk if there are remaining results to fetch.
203    #[inline]
204    pub fn has_result(&self) -> bool {
205        !self.results.is_empty()
206    }
207
208    fn spawn_task(&self, input: I) -> Task<Result<O, (I, Error)>> {
209        let completed = self.completed_but_unretrieved.clone();
210        let fut = (self.factory)(input)
211            // Completed tasks can remain queued while the caller produces more work.
212            // Only failures need to retain their input for a retry.
213            .map(|(input, result)| result.map_err(|err| (input, err)))
214            .inspect(move |_| {
215                completed.fetch_add(1, Ordering::Relaxed);
216            });
217
218        self.executor.execute(fut)
219    }
220
221    /// Execute the task with given input.
222    ///
223    /// - Execute the task in the current thread if is not concurrent.
224    /// - Execute the task in the background if there are available slots.
225    /// - Await the first task in the queue if there is no available slots.
226    pub async fn execute(&mut self, input: I) -> Result<()> {
227        if self.errored {
228            return Err(Error::new(
229                ErrorKind::Unexpected,
230                "concurrent tasks met an unrecoverable error",
231            ));
232        }
233
234        // Short path for non-concurrent case.
235        if !self.is_concurrent() {
236            let (_, o) = (self.factory)(input).await;
237            return match o {
238                Ok(o) => {
239                    self.results.push_back(o);
240                    Ok(())
241                }
242                // We don't need to rebuild the future if it's not concurrent.
243                Err(err) => Err(err),
244            };
245        }
246
247        if !self.has_remaining() {
248            let result = self
249                .tasks
250                .front_mut()
251                .expect("tasks must be available")
252                .await;
253            self.completed_but_unretrieved
254                .fetch_sub(1, Ordering::Relaxed);
255            match result {
256                Ok(o) => {
257                    let _ = self.tasks.pop_front();
258                    self.results.push_back(o)
259                }
260                Err((i, err)) => {
261                    // Retry this task if the error is temporary
262                    if err.is_temporary() {
263                        let task = self.spawn_task(i);
264                        self.tasks
265                            .front_mut()
266                            .expect("tasks must be available")
267                            .replace(task)
268                    } else {
269                        self.clear();
270                        self.errored = true;
271                    }
272                    return Err(err);
273                }
274            }
275        }
276
277        self.tasks.push_back(self.spawn_task(input));
278        Ok(())
279    }
280
281    /// Fetch the successful result from the result queue.
282    pub async fn next(&mut self) -> Option<Result<O>> {
283        if self.errored {
284            return Some(Err(Error::new(
285                ErrorKind::Unexpected,
286                "concurrent tasks met an unrecoverable error",
287            )));
288        }
289
290        if let Some(result) = self.results.pop_front() {
291            return Some(Ok(result));
292        }
293
294        if let Some(task) = self.tasks.front_mut() {
295            let result = task.await;
296            self.completed_but_unretrieved
297                .fetch_sub(1, Ordering::Relaxed);
298            return match result {
299                Ok(o) => {
300                    let _ = self.tasks.pop_front();
301                    Some(Ok(o))
302                }
303                Err((i, err)) => {
304                    // Retry this task if the error is temporary
305                    if err.is_temporary() {
306                        let task = self.spawn_task(i);
307                        self.tasks
308                            .front_mut()
309                            .expect("tasks must be available")
310                            .replace(task)
311                    } else {
312                        self.clear();
313                        self.errored = true;
314                    }
315                    Some(Err(err))
316                }
317            };
318        }
319
320        None
321    }
322}
323
324#[cfg(test)]
325mod tests {
326    use pretty_assertions::assert_eq;
327    use rand::RngExt;
328    use tokio::time::sleep;
329
330    use super::*;
331    use crate::raw::Duration;
332
333    #[tokio::test]
334    async fn test_concurrent_tasks() {
335        let executor = Executor::new();
336
337        let mut tasks = ConcurrentTasks::new(executor, 16, 8, |(i, dur)| {
338            Box::pin(async move {
339                sleep(dur).await;
340
341                // 5% rate to fail.
342                if rand::rng().random_range(0..100) > 90 {
343                    return (
344                        (i, dur),
345                        Err(Error::new(ErrorKind::Unexpected, "I'm lucky").set_temporary()),
346                    );
347                }
348                ((i, dur), Ok(i))
349            })
350        });
351
352        let mut ans = vec![];
353
354        for i in 0..10240 {
355            // Sleep up to 10ms
356            let dur = Duration::from_millis(rand::rng().random_range(0..10));
357            loop {
358                let res = tasks.execute((i, dur)).await;
359                if res.is_ok() {
360                    break;
361                }
362            }
363        }
364
365        loop {
366            match tasks.next().await.transpose() {
367                Ok(Some(i)) => ans.push(i),
368                Ok(None) => break,
369                Err(_) => continue,
370            }
371        }
372
373        assert_eq!(ans, (0..10240).collect::<Vec<_>>())
374    }
375
376    #[tokio::test]
377    async fn test_prefetch_backpressure() {
378        let executor = Executor::new();
379        let concurrent = 4;
380        let prefetch = 2;
381
382        // Create a slower task to ensure they don't complete immediately
383        let mut tasks = ConcurrentTasks::new(executor, concurrent, prefetch, |i: usize| {
384            Box::pin(async move {
385                sleep(Duration::from_millis(100)).await;
386                (i, Ok(i))
387            })
388        });
389
390        // Initially, we should have space for concurrent tasks
391        assert!(tasks.has_remaining(), "Should have space initially");
392
393        // Submit concurrent tasks
394        for i in 0..concurrent {
395            assert!(tasks.has_remaining(), "Should have space for task {i}");
396            tasks.execute(i).await.unwrap();
397        }
398
399        // Now we shouldn't have any more space (since no tasks have completed yet)
400        assert!(
401            !tasks.has_remaining(),
402            "Should not have space after submitting concurrent tasks"
403        );
404
405        // Wait for some tasks to complete
406        sleep(Duration::from_millis(150)).await;
407
408        // Now we should have space up to prefetch limit
409        for i in concurrent..concurrent + prefetch {
410            assert!(
411                tasks.has_remaining(),
412                "Should have space for prefetch task {i}"
413            );
414            tasks.execute(i).await.unwrap();
415        }
416
417        // Now has_remaining should return false
418        assert!(
419            !tasks.has_remaining(),
420            "Should not have remaining space after filling up prefetch buffer"
421        );
422
423        // Retrieve one result
424        let result = tasks.next().await;
425        assert!(result.is_some());
426
427        // Now there should be space for one more task
428        assert!(
429            tasks.has_remaining(),
430            "Should have remaining space after retrieving one result"
431        );
432    }
433
434    #[tokio::test]
435    async fn test_prefetch_zero() {
436        let executor = Executor::new();
437        let concurrent = 4;
438        let prefetch = 0; // No prefetching allowed
439
440        let mut tasks = ConcurrentTasks::new(executor, concurrent, prefetch, |i: usize| {
441            Box::pin(async move {
442                sleep(Duration::from_millis(10)).await;
443                (i, Ok(i))
444            })
445        });
446
447        // With prefetch=0, we can only submit up to concurrent tasks
448        for i in 0..concurrent {
449            tasks.execute(i).await.unwrap();
450        }
451
452        // Should not have space for more
453        assert!(
454            !tasks.has_remaining(),
455            "Should not have remaining space with prefetch=0"
456        );
457
458        // Retrieve one result
459        let result = tasks.next().await;
460        assert!(result.is_some());
461
462        // Now there should be space for exactly one more task
463        assert!(
464            tasks.has_remaining(),
465            "Should have remaining space after retrieving one result"
466        );
467
468        // Execute one more
469        tasks.execute(concurrent).await.unwrap();
470
471        // Should be full again
472        assert!(!tasks.has_remaining(), "Should be full again");
473    }
474}