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}