diff options
| author | ihc童鞋@提不起劲 <[email protected]> | 2023-02-27 19:37:59 +0800 |
|---|---|---|
| committer | GitHub <[email protected]> | 2023-02-27 19:37:59 +0800 |
| commit | cb724c2c5fa5e49fc7f6c98535862511cb6a40b0 (patch) | |
| tree | ead3007dd54a91822b2c0761c641219a94d2f773 /docs | |
| parent | da2645529f7e890d2a76e3a2dc215a207f5880bf (diff) | |
docs: add io-cancel related docs (#148)
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/en/io-cancel.md | 111 | ||||
| -rw-r--r-- | docs/zh/io-cancel.md | 111 |
2 files changed, 222 insertions, 0 deletions
diff --git a/docs/en/io-cancel.md b/docs/en/io-cancel.md new file mode 100644 index 0000000..cec6d7e --- /dev/null +++ b/docs/en/io-cancel.md @@ -0,0 +1,111 @@ +--- +title: Cancel IO +date: 2023-02-27 15:09:00 +author: ihciah +--- + +# Cancel IO +To support Cancel IO / IO with timeout, we provide a new set of IO Trait. + +This article gives a sample usage and design document. + +## How to use +Taking reads as an example, the following code defines a cancelable asynchronous read Trait. +```rust +/// CancelableAsyncReadRent: async read with an ownership of a buffer and ability to cancel io. +pub trait CancelableAsyncReadRent: AsyncReadRent { + /// The future of read Result<size, buffer> + type CancelableReadFuture<'a, T>: Future<Output = BufResult<usize, T>> + where + Self: 'a, + T: IoBufMut + 'a; + /// The future of readv Result<size, buffer> + type CancelableReadvFuture<'a, T>: Future<Output = BufResult<usize, T>> + where + Self: 'a, + T: IoVecBufMut + 'a; + + fn cancelable_read<T: IoBufMut>( + &mut self, + buf: T, + c: CancelHandle, + ) -> Self::CancelableReadFuture<'_, T>; + + fn cancelable_readv<T: IoVecBufMut>( + &mut self, + buf: T, + c: CancelHandle, + ) -> Self::CancelableReadvFuture<'_, T>; +} +``` + +When doing IO with timeouts, the Trait can be used like this. +```rust +let mut buf = vec![0; 1024]; + +let canceler = monoio::io::Canceller::new(); +let handle = canceler.handle(); + +let timer = monoio::time::sleep(std::time::Duration::from_millis(100)); +monoio::pin!(timer); +let recv = conn.cancelable_read(buf, handle); +monoio::pin!(recv); + +monoio::select! { + _ = &mut timer => { + canceler.cancel(); + let (res, _buf) = recv.await; + if matches!(res, Err(e) if e.raw_os_error() == Some(125)) { + // Canceled success + buf = _buf; + todo!() + } + // Canceled but executed + // Process res and buf + todo!() + }, + r = &mut recv => { + let (res, _buf) = r; + // Process res and buf + todo!() + } +} +``` + +When implementing IO wrapping, just pass through the CancelHandle. + +## Status and issues +The current interface takes ownership of the Buffer and generates the Future, returning the Result and Buffer when the Future ends. + +There is a need to transfer ownership: the Kernel uses the buffer at an uncertain time, so it needs to keep the buffer valid until the op returns. If the buffer is in the hands of the user, then the user can drop it, which can lead to incorrect memory reads and writes. + +However, transferring ownership also introduces a problem: the buffer can only be transferred to the user side after the Future is finished. Another problem with the current implementation is that even with the `async-cancel` feature enabled, the Runtime's cancellation of IO after a direct Drop Future is only as good as it can be (before it is successfully pushed into CancelOp and processed by the Kernel). This may lead to problems such as data loss during timeout reads, which in turn may lead to data errors on the data stream. To summarize, there are two problems: 1. +1. Cancellation of IO will result in loss of Buffer ownership +2. Cancellation of IO is not deterministically cancelled (this is due to the io_uring property itself), which may lead to read and write errors on the data stream + +## Solution +1. Transfer ownership through the interface exposed by the Trait when the Buffer is dropped + Add a method to the `Buf`/`BufMut` trait: `cancel(self)` to transfer the ownership of the buffer when it is dropped inside the runtime. + When implementing `Buf`/`BufMut` on the user side, a slot is maintained using shared ownership (`Rc<RefCell<Option<T>>>`), and the buffer can be retrieved using a handler structure paired with the buffer. + + But this solution only solves the Buffer ownership recovery problem: for example, a tcp accept cancellation may still result in a drop connection; a read may still lose data. Obviously, this is not an ideal solution. 2. + +2. Expose an explicit cancellation interface to recover Buffer and sense the Result through the original Future +Expose a Cancel mechanism to trigger the return of Future. There are also several options for implementation: 1. + 1. return a CancelHandler: where to return the CancelHandler is a problem, and how to implement the CancelHandler when implementing `write_all` is also a problem. 2. + 2. pass in a CancelHandler: helps to solve the problem when implementing wrapper (you can pass this structure in directly, similar to golang `Context`), but there are still problems: how to define the interface, how to throw in the CancelHandler? + 1. add a separate Trait + 2. extend the new interface within the existing Trait and implement it empty by default + 3. modify the existing interface + + Considering the compatibility, we try not to modify the existing interface; extending the existing Trait will still introduce compatibility issues and will lead to unnecessary code implementation when the user does not actually use it. So we decided to add a new Trait and pass CancelHandler on its corresponding fn. + +3. Ostrich tactics + When there is a need for Cancel, we go for readable / writable, which has no side effects and can be cancelled at will. However, even if you go this way, you still need to abstract the Readable/Writable into a trait. + + Disadvantages: not conducive to compound operations to do timeout (such as write_all / read_exact), and poor performance. This solution is not contradictory to the previous one, and users can use it directly that way. + +## Implementation details +There are different logics for different Drivers. +1. uring: push in CancelOp, Kernel consumes and returns the original Future. 2. legacy: add a definition of READ. +2. legacy: new definition of READ_CANCELED and WRITE_CANCELED two marker bits, mark and wake when the target is canceled; task poll when determining the marker bit, if it is CANCELED then directly return Operation Canceled error. diff --git a/docs/zh/io-cancel.md b/docs/zh/io-cancel.md new file mode 100644 index 0000000..1d76737 --- /dev/null +++ b/docs/zh/io-cancel.md @@ -0,0 +1,111 @@ +--- +title: 取消 IO +date: 2023-02-27 15:09:00 +author: ihciah +--- + +# 取消 IO +为了支持取消 IO / 带超时 IO,我们提供了一套新的 IO Trait。 + +本文给出了使用样例与设计文档。 + +## 如何使用 +以读为例,下面这段代码定义了可取消的异步读 Trait: +```rust +/// CancelableAsyncReadRent: async read with a ownership of a buffer and ability to cancel io. +pub trait CancelableAsyncReadRent: AsyncReadRent { + /// The future of read Result<size, buffer> + type CancelableReadFuture<'a, T>: Future<Output = BufResult<usize, T>> + where + Self: 'a, + T: IoBufMut + 'a; + /// The future of readv Result<size, buffer> + type CancelableReadvFuture<'a, T>: Future<Output = BufResult<usize, T>> + where + Self: 'a, + T: IoVecBufMut + 'a; + + fn cancelable_read<T: IoBufMut>( + &mut self, + buf: T, + c: CancelHandle, + ) -> Self::CancelableReadFuture<'_, T>; + + fn cancelable_readv<T: IoVecBufMut>( + &mut self, + buf: T, + c: CancelHandle, + ) -> Self::CancelableReadvFuture<'_, T>; +} +``` + +在做带超时的 IO 时,可以这么使用该 Trait: +```rust +let mut buf = vec![0; 1024]; + +let canceler = monoio::io::Canceller::new(); +let handle = canceler.handle(); + +let timer = monoio::time::sleep(std::time::Duration::from_millis(100)); +monoio::pin!(timer); +let recv = conn.cancelable_read(buf, handle); +monoio::pin!(recv); + +monoio::select! { + _ = &mut timer => { + canceler.cancel(); + let (res, _buf) = recv.await; + if matches!(res, Err(e) if e.raw_os_error() == Some(125)) { + // Canceled success + buf = _buf; + todo!() + } + // Canceled but executed + // Process res and buf + todo!() + }, + r = &mut recv => { + let (res, _buf) = r; + // Process res and buf + todo!() + } +} +``` + +在实现 IO 封装时,透传 CancelHandle 即可。 + +## 现状与问题 +当前接口会拿 Buffer 所有权并生成 Future,当 Future 结束时返回 Result 和 Buffer。 + +转移所有权有其必要性:Kernel 使用 buffer 的时机不确定,所以需要在 op 返回之前持续保证 buffer 有效性。如果 buffer 在用户手上那么用户完全可以 drop,就会导致错误的内存读写。 + +但转移所有权也引入了问题:Buffer 只能在 Future 结束后才能转移到用户侧。当用户想要实现超时 IO 时,无法拿回 Buffer;当前实现的另一个问题是,即便在开启了 `async-cancel` feature 的情况下,直接 Drop Future 后 Runtime 对 IO 的取消操作也仅为尽力保证取消(在成功推入 CancelOp 并被 Kernel 处理前,IO 仍有机会完成),可能会导致超时读时数据丢失等问题,继而导致数据流上的数据错误。总结一下就是这么两个问题: +1. 取消 IO 后会丢失 Buffer 所有权 +2. 取消 IO 并不能确定性地被取消(这个是 io_uring 本身属性导致),可能会导致数据流读写错误 + +## 解决方案 +1. 在 Buffer Drop 时通过其 Trait 暴露的接口转移所有权 + 在 `Buf`/`BufMut` trait 上添加一个方法:`cancel(self)`,在 runtime 内部 drop buffer 时,将 buffer 所有权转移进去。 + 在用户侧实现 `Buf`/`BufMut` 时,利用共享所有权(`Rc<RefCell<Option<T>>>`)维护一个 slot,能够用与 buffer 配对的一个 handler 结构拿回 buffer。 + + 但这个方案只解决 Buffer 所有权回收问题:比如 tcp accept 的取消依旧可能导致 drop connection;read 依旧可能丢失数据。显然这并不是一个理想的方案。 + +2. 暴露显式取消接口,通过原 Future 回收 Buffer 并感知 Result +暴露一个 Cancel 机制,能够主动触发 Future 返回。之后就可以直接 await Future 并在较短时间内拿到对应的 Result 和 Buffer。实现上也有几种方案: + 1. 返回一个 CancelHandler:在哪返回 CancelHandler 是个问题,当实现 `write_all` 的时候如何实现 CancelHandler 也是个问题。 + 2. 传入一个 CancelHandler:有助于解决实现 wrapper 时的问题(可以直接透传这个结构进去,类似 golang `Context`),但依旧存在问题:接口怎么定,怎么把 CancelHandler 丢进去? + 1. 新增一个独立的 Trait + 2. 在现有 Trait 内扩展新的接口,并默认空实现 + 3. 修改现有接口 + + 考虑到兼容性,现有接口尽量不做修改;扩展现有 Trait 依旧会引入兼容性问题,并会在用户未实际使用时导致不必要的代码实现。所以我们决定新增一个 Trait,并在其对应的 fn 上传递 CancelHandler。 + +3. 鸵鸟战术 + 有 Cancel 需求时走 readable / writable,这个没副作用,随便取消。不过即便是走这种方案,仍需要将 Readable/Writable 抽成 trait。 + + 缺点:不利于对复合操作做 timeout(如 write_all / read_exact),且性能不佳。该方案与前面的方案并不矛盾,用户可以直接这么使用。 + +## 实现细节 +对于不同 Driver 有不同的逻辑: +1. uring:推入 CancelOp,Kernel 消费后原 Future 返回。 +2. legacy:新增定义 READ_CANCELED 和 WRITE_CANCELED 两种标记位,在目标被 cancel 时标记并 wake;任务 poll 时判定标志位,如果是 CANCELED 则直接返回 Operation Canceled 错误。 |
