Skip to main content

opendal_layer_timeout/
lib.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
18//! Timeout layer implementation for Apache OpenDAL.
19
20#![cfg_attr(docsrs, feature(doc_cfg))]
21#![deny(missing_docs)]
22
23use std::future::Future;
24use std::sync::Arc;
25use std::time::Duration;
26
27use opendal_core::raw::*;
28use opendal_core::*;
29
30/// Add timeouts to operations to avoid slow or unexpectedly hanging work.
31///
32/// For example, a dead connection could hang a database SQL query. `TimeoutLayer`
33/// will break this connection and return an error so users can handle it by
34/// retrying or reporting it.
35///
36/// # Notes
37///
38/// `TimeoutLayer` applies two timeout budgets:
39///
40/// - `timeout` bounds control operations such as `stat`, `create_dir`, `rename`,
41///   and `presign`.
42/// - `io_timeout` bounds operations that open IO bodies, such as `read`, `write`,
43///   and `list`, and every method call on returned readers, writers, listers,
44///   deleters, and copiers.
45///
46/// # Default
47///
48/// - timeout: 60 seconds
49/// - io_timeout: 10 seconds
50///
51/// # Cancellation Safety
52///
53/// `TimeoutLayer` enforces deadlines by dropping the in-flight future when a
54/// timeout is reached. This can break lower layers that rely on a future being
55/// resolved to restore internal state.
56///
57/// For example, while using `TimeoutLayer` with `RetryLayer` at the same time,
58/// please make sure timeout layer is added before retry layer.
59///
60/// ```no_run
61/// # use std::time::Duration;
62/// #
63/// # use opendal_core::services;
64/// # use opendal_core::Operator;
65/// # use opendal_core::Result;
66/// # use opendal_layer_retry::RetryLayer;
67/// # use opendal_layer_timeout::TimeoutLayer;
68/// #
69/// # fn main() -> Result<()> {
70/// let op = Operator::new(services::Memory::default())?
71///     // This is fine: each retry attempt is timed out.
72///     .layer(TimeoutLayer::default().with_io_timeout(Duration::from_nanos(1)))
73///     .layer(RetryLayer::default())
74///     // This is wrong: timeout can drop RetryLayer's future before it restores body state.
75///     .layer(TimeoutLayer::default().with_io_timeout(Duration::from_nanos(1)));
76/// # Ok(())
77/// # }
78/// ```
79///
80/// # Examples
81///
82/// The following example creates a timeout layer with a 10-second timeout for
83/// control operations and a 3-second timeout for IO operations.
84///
85/// ```no_run
86/// # use std::time::Duration;
87/// #
88/// # use opendal_core::services;
89/// # use opendal_core::Operator;
90/// # use opendal_core::Result;
91/// # use opendal_layer_timeout::TimeoutLayer;
92/// #
93/// # fn main() -> Result<()> {
94/// let _ = Operator::new(services::Memory::default())?
95///     .layer(
96///         TimeoutLayer::default()
97///             .with_timeout(Duration::from_secs(10))
98///             .with_io_timeout(Duration::from_secs(3)),
99///     );
100/// # Ok(())
101/// # }
102/// ```
103///
104/// # Implementation Notes
105///
106/// `TimeoutLayer` uses [`tokio::time::timeout`] to bound service calls and IO
107/// body methods. It also supplies an executor timeout so concurrent block write
108/// and copy tasks can fail instead of waiting forever.
109///
110/// This introduces a small amount of overhead for IO operations, but it is needed
111/// to implement timeouts correctly. OpenDAL used to implement this as a
112/// zero-cost deadline check that only stored an [`Instant`] and compared it with
113/// the current time. However, that approach does not work for all cases.
114///
115/// For example, a user's TCP connection could enter the
116/// [Busy ESTAB](https://blog.cloudflare.com/when-tcp-sockets-refuse-to-die)
117/// state. In this state, no IO event will be emitted, so the runtime will never
118/// poll the future again. From the application side, this future hangs until the
119/// connection is closed after reaching the Linux
120/// [net.ipv4.tcp_retries2](https://man7.org/linux/man-pages/man7/tcp.7.html)
121/// limit.
122#[derive(Clone, Debug)]
123pub struct TimeoutLayer {
124    timeout: Duration,
125    io_timeout: Duration,
126}
127
128impl Default for TimeoutLayer {
129    fn default() -> Self {
130        Self {
131            timeout: Duration::from_secs(60),
132            io_timeout: Duration::from_secs(10),
133        }
134    }
135}
136
137impl TimeoutLayer {
138    /// Create a new [`TimeoutLayer`] with default settings.
139    pub fn new() -> Self {
140        Self::default()
141    }
142
143    /// Set the timeout for control operations.
144    ///
145    /// This timeout is for all non-io operations like `stat`, `delete`.
146    pub fn with_timeout(mut self, timeout: Duration) -> Self {
147        self.timeout = timeout;
148        self
149    }
150
151    /// Set the timeout for IO operations and body methods.
152    ///
153    /// This timeout is for all io operations like `read`, `Reader::read` and `Writer::write`.
154    pub fn with_io_timeout(mut self, timeout: Duration) -> Self {
155        self.io_timeout = timeout;
156        self
157    }
158}
159
160impl Layer for TimeoutLayer {
161    fn apply_service(&self, inner: Servicer) -> Servicer {
162        Arc::new(self.layer(inner))
163    }
164
165    fn apply_context(&self, _srv: Servicer, inner: OperationContext) -> OperationContext {
166        // Concurrent block IO paths read this timeout from the operation context's executor.
167        let executor = Executor::with(TimeoutExecutor::new(
168            inner.executor().clone().into_inner(),
169            self.io_timeout,
170        ));
171        inner.with_executor(executor)
172    }
173}
174
175impl TimeoutLayer {
176    fn layer(&self, inner: Servicer) -> TimeoutService {
177        TimeoutService {
178            inner,
179            timeout: self.timeout,
180            io_timeout: self.io_timeout,
181        }
182    }
183}
184
185#[doc(hidden)]
186#[derive(Debug)]
187pub struct TimeoutService {
188    inner: Servicer,
189    timeout: Duration,
190    io_timeout: Duration,
191}
192
193impl TimeoutService {
194    async fn timeout<F: Future<Output = Result<T>>, T>(&self, op: Operation, fut: F) -> Result<T> {
195        tokio::time::timeout(self.timeout, fut).await.map_err(|_| {
196            Error::new(ErrorKind::Unexpected, "operation timeout reached")
197                .with_operation(op)
198                .with_context("timeout", self.timeout.as_secs_f64().to_string())
199                .set_temporary()
200        })?
201    }
202}
203
204impl Service for TimeoutService {
205    type Reader = TimeoutWrapper<oio::Reader>;
206    type Writer = TimeoutWrapper<oio::Writer>;
207    type Lister = TimeoutWrapper<oio::Lister>;
208    type Deleter = TimeoutWrapper<oio::Deleter>;
209    type Copier = TimeoutWrapper<oio::Copier>;
210
211    fn info(&self) -> ServiceInfo {
212        self.inner.info()
213    }
214
215    fn capability(&self) -> Capability {
216        self.inner.capability()
217    }
218
219    async fn create_dir(
220        &self,
221        ctx: &OperationContext,
222        path: &str,
223        args: OpCreateDir,
224    ) -> Result<RpCreateDir> {
225        self.timeout(Operation::CreateDir, self.inner.create_dir(ctx, path, args))
226            .await
227    }
228
229    fn read(&self, ctx: &OperationContext, path: &str, args: OpRead) -> Result<Self::Reader> {
230        self.inner
231            .read(ctx, path, args)
232            .map(|r| TimeoutWrapper::new(r, self.io_timeout))
233    }
234
235    fn write(&self, ctx: &OperationContext, path: &str, args: OpWrite) -> Result<Self::Writer> {
236        self.inner
237            .write(ctx, path, args)
238            .map(|r| TimeoutWrapper::new(r, self.io_timeout))
239    }
240
241    fn copy(
242        &self,
243        ctx: &OperationContext,
244        from: &str,
245        to: &str,
246        args: OpCopy,
247        opts: OpCopier,
248    ) -> Result<Self::Copier> {
249        self.inner
250            .copy(ctx, from, to, args, opts)
251            .map(|c| TimeoutWrapper::new(c, self.io_timeout))
252    }
253
254    async fn rename(
255        &self,
256        ctx: &OperationContext,
257        from: &str,
258        to: &str,
259        args: OpRename,
260    ) -> Result<RpRename> {
261        self.timeout(Operation::Rename, self.inner.rename(ctx, from, to, args))
262            .await
263    }
264
265    async fn stat(&self, ctx: &OperationContext, path: &str, args: OpStat) -> Result<RpStat> {
266        self.timeout(Operation::Stat, self.inner.stat(ctx, path, args))
267            .await
268    }
269
270    fn delete(&self, ctx: &OperationContext) -> Result<Self::Deleter> {
271        self.inner
272            .delete(ctx)
273            .map(|r| TimeoutWrapper::new(r, self.io_timeout))
274    }
275
276    fn list(&self, ctx: &OperationContext, path: &str, args: OpList) -> Result<Self::Lister> {
277        self.inner
278            .list(ctx, path, args)
279            .map(|r| TimeoutWrapper::new(r, self.io_timeout))
280    }
281
282    async fn presign(
283        &self,
284        ctx: &OperationContext,
285        path: &str,
286        args: OpPresign,
287    ) -> Result<RpPresign> {
288        self.timeout(Operation::Presign, self.inner.presign(ctx, path, args))
289            .await
290    }
291}
292
293struct TimeoutExecutor {
294    exec: Arc<dyn Execute>,
295    timeout: Duration,
296}
297
298impl TimeoutExecutor {
299    fn new(exec: Arc<dyn Execute>, timeout: Duration) -> Self {
300        Self { exec, timeout }
301    }
302}
303
304impl Execute for TimeoutExecutor {
305    fn execute(&self, f: BoxedStaticFuture<()>) {
306        self.exec.execute(f)
307    }
308
309    fn timeout(&self) -> Option<BoxedStaticFuture<()>> {
310        Some(Box::pin(tokio::time::sleep(self.timeout)))
311    }
312}
313
314#[doc(hidden)]
315pub struct TimeoutWrapper<R> {
316    inner: R,
317
318    timeout: Duration,
319}
320
321impl<R> TimeoutWrapper<R> {
322    fn new(inner: R, timeout: Duration) -> Self {
323        Self { inner, timeout }
324    }
325
326    #[inline]
327    async fn io_timeout<F: Future<Output = Result<T>>, T>(
328        timeout: Duration,
329        op: &'static str,
330        fut: F,
331    ) -> Result<T> {
332        tokio::time::timeout(timeout, fut).await.map_err(|_| {
333            Error::new(ErrorKind::Unexpected, "io operation timeout reached")
334                .with_operation(op)
335                .with_context("timeout", timeout.as_secs_f64().to_string())
336                .set_temporary()
337        })?
338    }
339}
340
341impl<R: oio::ReadStream> oio::ReadStream for TimeoutWrapper<R> {
342    async fn read(&mut self) -> Result<Buffer> {
343        let fut = self.inner.read();
344        Self::io_timeout(self.timeout, Operation::Read.into_static(), fut).await
345    }
346}
347
348impl<R: oio::Read> oio::Read for TimeoutWrapper<R> {
349    async fn open(&self, range: BytesRange) -> Result<(RpRead, Box<dyn oio::ReadStreamDyn>)> {
350        let (rp, stream) = Self::io_timeout(
351            self.timeout,
352            Operation::Read.into_static(),
353            self.inner.open(range),
354        )
355        .await?;
356        Ok((
357            rp,
358            Box::new(TimeoutWrapper::new(stream, self.timeout)) as Box<dyn oio::ReadStreamDyn>,
359        ))
360    }
361
362    async fn read(&self, range: BytesRange) -> Result<(RpRead, Buffer)> {
363        Self::io_timeout(
364            self.timeout,
365            Operation::Read.into_static(),
366            self.inner.read(range),
367        )
368        .await
369    }
370}
371
372impl<R: oio::Write> oio::Write for TimeoutWrapper<R> {
373    async fn write(&mut self, bs: Buffer) -> Result<()> {
374        let fut = self.inner.write(bs);
375        Self::io_timeout(self.timeout, Operation::Write.into_static(), fut).await
376    }
377
378    async fn close(&mut self) -> Result<Metadata> {
379        let fut = self.inner.close();
380        Self::io_timeout(self.timeout, Operation::Write.into_static(), fut).await
381    }
382
383    async fn abort(&mut self) -> Result<()> {
384        let fut = self.inner.abort();
385        Self::io_timeout(self.timeout, Operation::Write.into_static(), fut).await
386    }
387}
388
389impl<R: oio::List> oio::List for TimeoutWrapper<R> {
390    async fn next(&mut self) -> Result<Option<oio::Entry>> {
391        let fut = self.inner.next();
392        Self::io_timeout(self.timeout, Operation::List.into_static(), fut).await
393    }
394}
395
396impl<R: oio::Delete> oio::Delete for TimeoutWrapper<R> {
397    async fn delete(&mut self, path: &str, args: OpDelete) -> Result<()> {
398        let fut = self.inner.delete(path, args);
399        Self::io_timeout(self.timeout, Operation::Delete.into_static(), fut).await
400    }
401
402    async fn close(&mut self) -> Result<()> {
403        let fut = self.inner.close();
404        Self::io_timeout(self.timeout, Operation::Delete.into_static(), fut).await
405    }
406}
407
408impl<C: oio::Copy> oio::Copy for TimeoutWrapper<C> {
409    async fn next(&mut self) -> Result<Option<usize>> {
410        let fut = self.inner.next();
411        Self::io_timeout(self.timeout, Operation::Copy.into_static(), fut).await
412    }
413
414    async fn close(&mut self) -> Result<Metadata> {
415        let fut = self.inner.close();
416        Self::io_timeout(self.timeout, Operation::Copy.into_static(), fut).await
417    }
418
419    async fn abort(&mut self) -> Result<()> {
420        let fut = self.inner.abort();
421        Self::io_timeout(self.timeout, Operation::Copy.into_static(), fut).await
422    }
423}
424
425#[cfg(test)]
426mod tests {
427    use std::future::pending;
428
429    use futures::StreamExt;
430    use tokio::time::timeout;
431
432    use super::*;
433
434    #[derive(Debug, Clone, Default)]
435    struct MockService;
436
437    impl Service for MockService {
438        type Reader = MockReader;
439        type Writer = ();
440        type Lister = MockLister;
441        type Deleter = MockDeleter;
442        type Copier = MockCopier;
443
444        fn info(&self) -> ServiceInfo {
445            ServiceInfo::with_scheme("mock")
446        }
447
448        fn capability(&self) -> Capability {
449            Capability {
450                read: true,
451                delete: true,
452                list: true,
453                copy: true,
454                ..Default::default()
455            }
456        }
457
458        async fn create_dir(
459            &self,
460            _: &OperationContext,
461            _: &str,
462            _: OpCreateDir,
463        ) -> Result<RpCreateDir> {
464            Err(Error::new(
465                ErrorKind::Unsupported,
466                "operation is not supported",
467            ))
468        }
469
470        async fn stat(&self, _: &OperationContext, _: &str, _: OpStat) -> Result<RpStat> {
471            Err(Error::new(
472                ErrorKind::Unsupported,
473                "operation is not supported",
474            ))
475        }
476
477        /// Return a reader whose operations never complete.
478        fn read(&self, _ctx: &OperationContext, _: &str, _: OpRead) -> Result<Self::Reader> {
479            Ok(MockReader)
480        }
481
482        fn write(&self, _ctx: &OperationContext, _: &str, _: OpWrite) -> Result<Self::Writer> {
483            Err(Error::new(
484                ErrorKind::Unsupported,
485                "operation is not supported",
486            ))
487        }
488
489        /// Return a deleter whose operations never complete.
490        fn delete(&self, _ctx: &OperationContext) -> Result<Self::Deleter> {
491            Ok(MockDeleter)
492        }
493
494        fn list(&self, _ctx: &OperationContext, _: &str, _: OpList) -> Result<Self::Lister> {
495            Ok(MockLister)
496        }
497
498        fn copy(
499            &self,
500            _: &OperationContext,
501            _: &str,
502            _: &str,
503            _: OpCopy,
504            _: OpCopier,
505        ) -> Result<Self::Copier> {
506            Ok(MockCopier)
507        }
508
509        async fn rename(
510            &self,
511            _: &OperationContext,
512            _: &str,
513            _: &str,
514            _: OpRename,
515        ) -> Result<RpRename> {
516            Err(Error::new(
517                ErrorKind::Unsupported,
518                "operation is not supported",
519            ))
520        }
521
522        async fn presign(&self, _: &OperationContext, _: &str, _: OpPresign) -> Result<RpPresign> {
523            Err(Error::new(
524                ErrorKind::Unsupported,
525                "operation is not supported",
526            ))
527        }
528    }
529
530    #[derive(Debug, Clone, Default)]
531    struct MockReader;
532
533    impl oio::Read for MockReader {
534        async fn open(&self, _: BytesRange) -> Result<(RpRead, Box<dyn oio::ReadStreamDyn>)> {
535            pending().await
536        }
537
538        async fn read(&self, _: BytesRange) -> Result<(RpRead, Buffer)> {
539            pending().await
540        }
541    }
542
543    #[derive(Debug, Clone, Default)]
544    struct MockLister;
545
546    impl oio::List for MockLister {
547        async fn next(&mut self) -> Result<Option<oio::Entry>> {
548            pending().await
549        }
550    }
551
552    #[derive(Debug, Clone, Default)]
553    struct MockDeleter;
554
555    impl oio::Delete for MockDeleter {
556        async fn delete(&mut self, _: &str, _: OpDelete) -> Result<()> {
557            pending().await
558        }
559
560        async fn close(&mut self) -> Result<()> {
561            Ok(())
562        }
563    }
564
565    #[derive(Debug, Clone, Default)]
566    struct MockCopier;
567
568    impl oio::Copy for MockCopier {
569        async fn next(&mut self) -> Result<Option<usize>> {
570            pending().await
571        }
572
573        async fn close(&mut self) -> Result<Metadata> {
574            pending().await
575        }
576
577        async fn abort(&mut self) -> Result<()> {
578            pending().await
579        }
580    }
581
582    #[tokio::test]
583    async fn test_delete_timeout() {
584        let srv = MockService;
585        let op = Operator::from_parts(OperationContext::default(), Arc::new(srv))
586            .layer(TimeoutLayer::default().with_io_timeout(Duration::from_secs(1)));
587
588        let fut = async {
589            let res = op.delete("test").await;
590            assert!(res.is_err());
591            let err = res.unwrap_err();
592            assert_eq!(err.kind(), ErrorKind::Unexpected);
593            assert!(err.to_string().contains("timeout"))
594        };
595
596        timeout(Duration::from_secs(2), fut)
597            .await
598            .expect("this test should not exceed 2 seconds")
599    }
600
601    #[tokio::test]
602    async fn test_io_timeout() {
603        let srv = MockService;
604        let op = Operator::from_parts(OperationContext::default(), Arc::new(srv))
605            .layer(TimeoutLayer::default().with_io_timeout(Duration::from_secs(1)));
606
607        let reader = op.reader("test").await.unwrap();
608
609        let res = reader.read(0..4).await;
610        assert!(res.is_err());
611        let err = res.unwrap_err();
612        assert_eq!(err.kind(), ErrorKind::Unexpected);
613        assert!(err.to_string().contains("timeout"))
614    }
615
616    #[tokio::test]
617    async fn test_list_timeout() {
618        let srv = MockService;
619        let op = Operator::from_parts(OperationContext::default(), Arc::new(srv)).layer(
620            TimeoutLayer::default()
621                .with_timeout(Duration::from_secs(1))
622                .with_io_timeout(Duration::from_secs(1)),
623        );
624
625        let mut lister = op.lister("test").await.unwrap();
626
627        let res = lister.next().await.unwrap();
628        assert!(res.is_err());
629        let err = res.unwrap_err();
630        assert_eq!(err.kind(), ErrorKind::Unexpected);
631        assert!(err.to_string().contains("timeout"))
632    }
633
634    #[tokio::test]
635    async fn test_delete_io_timeout() {
636        use oio::Delete;
637
638        let mut deleter = TimeoutWrapper::new(MockDeleter, Duration::from_secs(1));
639
640        let res = deleter.delete("test", OpDelete::default()).await;
641        assert!(res.is_err());
642        let err = res.unwrap_err();
643        assert_eq!(err.kind(), ErrorKind::Unexpected);
644        assert!(err.to_string().contains("timeout"));
645    }
646
647    #[tokio::test]
648    async fn test_copy_io_timeout() {
649        use oio::Copy;
650
651        let service = TimeoutLayer::default()
652            .with_io_timeout(Duration::from_millis(100))
653            .apply_service(Arc::new(MockService));
654        let ctx = OperationContext::new();
655        let mut copier = service
656            .copy(&ctx, "f", "t", OpCopy::default(), OpCopier::default())
657            .unwrap();
658
659        let err = copier.next().await.unwrap_err();
660        assert!(err.to_string().contains("timeout"));
661    }
662
663    #[tokio::test]
664    async fn test_list_timeout_raw() {
665        use oio::List;
666
667        let timeout_layer = TimeoutLayer::default()
668            .with_timeout(Duration::from_secs(1))
669            .with_io_timeout(Duration::from_secs(1));
670        let service = timeout_layer.apply_service(Arc::new(MockService));
671        let ctx = OperationContext::new();
672
673        let mut lister = service.list(&ctx, "test", OpList::default()).unwrap();
674
675        let res = lister.next().await;
676        assert!(res.is_err());
677        let err = res.unwrap_err();
678        assert_eq!(err.kind(), ErrorKind::Unexpected);
679        assert!(err.to_string().contains("timeout"));
680    }
681}