Rustの`std::io::Write::write`が`Ok(0)`を返すことについて
はじめに
先日、hyperのストリーム[1]をtokio-rustlsでラップした際に、tokio::io::AsyncWrite::poll_writeが返すOk(0)の解釈の違いによって問題が出たので、そのときに調べたことをまとめます
こちらは厳密にはtokioのAsyncWriteの話ですが、実質同じなのでstd::io::Writeの話として記事を続けます
そのときのissue
起きたこと
こちらのコードはtokio-rustlsにあるTLSストリームの実装の一部です
ここではshutdownの際にwhileループでなにやら今まで書ききれてなかったデータを書き込もうとしています
しかし、tokio-rustlsがhyperのストリームをラップしていた場合、この段階でこのwriteに対して常にOk(0)を返すのでself.session.wants_write()が一生trueのままになり無限ループに陥ってしまいます
結局、https://doc.rust-lang.org/std/io/trait.Write.html#tymethod.write には
A return value of Ok(0) typically means that the underlying object is no longer able to accept bytes and will likely not be able to in the future as well, or that the buffer provided is empty.
と書かれているので、hyper側がOk(0)を返すのは正しい挙動で、rust-tls側が受け取ったOk(0)を特別扱いする必要があるということでまとまりました
std::io::Write::writeがOk(0)を返すことについての第一印象
しかし https://github.com/rustls/tokio-rustls/issues/92#issuecomment-2507878251 のコメントの通り、ストリームがもうこれ以上書き込めないよということを表明するためにOk(0)を返すのはあんまりなデザインだと思わざるを得ません
POSIXのwrite(2)にもそのような話はないっぽい
普通に考えて、仮にstd::io::Write::write_allを自分で実装するとしたら、自分も上記のtokio-rustlsのコードみたいに書く自信があります
できればもうこれ以上書き込めないよというときには https://doc.rust-lang.org/std/io/enum.ErrorKind.html からWriteZeroとかそれっぽいやつを選んで返してほしいものです
ちなみにstd::io::Write::write_allはOk(0)を特別に扱っているのでそれが原因で無限ループすることはありません
rustc内の議論
上記のデザイン上の疑問について調べていたところ、目を引くissueが見つかりました
主題はstd::io::Write::write_allがOk(0)をどう扱うかについてですが
IMHO a sane writer should return an error when it is already at the end. This would be in line with eg. a block device (a typical size-limited object to write to...). Ok(0) doesn't really make any sense to me. If you can't write anything yet, return EWOULDBLOCK (or, well, block), if you can't write anything because of some error, return the error. But don't just do nothing (or purely some unrelated internal stuff) and return 0, that makes no sense.
So in that sense, treating Ok(0) as an error condition is acceptable, because it's something that shouldn't happen anyway. (And yes, IMO the slice impl could just as well be changed)
というコメントがあり、個人的に納得しました。かなりまとめるとwriteにOk(0)を返すのはそもそも異常なので、受け取った側がそれをエラーとして扱うのは妥当だということです
まとめ
個人的には以下のような結論に至りました。
-
std::io::Write::writeに対してOk(0)が帰ってきた場合、受け取った側はそれをエラーとして扱うべき - だからといって
std::io::Write::write実装側がもうこれ以上書き込めないよというときにOk(0)を返すのは良くない。普通にstd::io::Errorを返すべき - そもそも
Result<std::num::NonZeroUsize>返せよ。Breaking Changeなのを差し引いてもさすがにこれはオタクすぎるか -
std::io::Write::write_allはOk(0)をエラーとして扱う。無限ループするわけにはいかないので[2]これは正しい -
std::io::Write::write_allがそういう挙動をしている以上 https://doc.rust-lang.org/std/io/trait.Write.html#tymethod.write に A return value of Ok(0) typically means that the underlying object is no longer able to accept bytes and will likely not be able to in the future as well, or that the buffer provided is empty. と書くのは妥当
-
具体的には
hyperのHTTP/2サーバーでUpgradeされたストリーム ↩︎ -
関連するLibs-API Meeting https://hackmd.io/@rust-libs/SJUBKd-lK ↩︎
Discussion