3 ms·
From my personal experience, buffered and unbuffered writers are different enough that I think it’s a bit of a mistake to make them indistinguishable to the typ
by amluto 1y ago
From my personal experience, buffered and unbuffered writers are different enough that I think it’s a bit of a mistake to make them indistinguishable to the type system. An unbuffered writer sends the data out of process immediately. A buffered writer usually doesn’t, so sleeping after a write (or just doing something else and not writing more for a while) will delay the write indefinitely. An unbuffered write does not do this.
This means that plenty of algorithms are correct with unbuffered writers and are incorrect with buffered writers. I’ve been bitten by this and diagnosed bugs caused by this multiple times.
Meanwhile an unbuffered writer has abysmal performance if you write a byte at a time.
I’d rather see an interface (trait, abstract class, whatever the language calls it) for a generic writer, with appropriate warnings that you probably don’t want to use it unless you take specific action to address its shortcomings, and subtypes for buffered and unbuffered writers.
And there could be a conditional buffering wrapper that temporarily adds buffering to a generic writer and is zero-cost if applied to an already buffered writer. A language with enforced borrowing semantics like Rust could make this very hard to misuse. But even Python could do it decently well, e.g.:
w: MaybeBufferedByteWriter
with io.LocalBuffer(w) as bufwriter:
do stuff with bufwriter
- donatj 1y agoI absolutely agree, and would like to add I feel like the ergonomics of the new interface are just very awkward and almost leaky. Buffered and unbuffered IO should just be entirely separately things, and separate interfaces. Then as you mention the standard library can provide an adapter in at least one direction, maybe both. This seems like a blunder to me.
- josephg 1y ago> This means that plenty of algorithms are correct with unbuffered writers and are incorrect with buffered writers. I’ve been bitten by this and diagnosed bugs caused by this multiple times. But write() on POSIX is also a buffered API. Until your program calls fsync / fdatasync, linux isn't required to actually flush anything to the underlying storage medium. And even then, many consumer storage devices will lie and return from fsync immediately before data has actually been flushed. All the OSes that I know of will eagerly write data instead of waiting for fsync, but there's no guarantee the data will be persisted by the time your write() call returns. It usually isn't. If you're relying on write() to durably flush data to disk, you've probably got correctness / data corruption bugs lurking in your code that will show up if power goes out at the wrong time.
- InfiniteRand 1y agoThis has bit me a few times when a Linux system crashes so there’s no final call to fsync implicit or otherwise
- o11c 1y agoI wouldn't call that "buffered", since `write` is guaranteed to appear immediately from the view of other processes that can see the same mount. It's only the disk that needs to be informed to really pick up (could we say "read"?) the changes.
- josephg 1y agoI still call that buffering because the OS buffers the writes to the physical device. Maybe from the POV of other processes the writes don’t appear to be buffered. But I’m not typically reading a database from multiple processes. I do, however, care about my database surviving a system crash. On that note though - What guarantees does the OS provide to reads from other processes? Will other processes see all writes immediately?
- amluto 1y agoI’m not talking about data loss if the host crashes. I’m talking about a much broader sense of correctness. Imagine an RPC server that uses persistent connections. If you reply using a buffered writer and forget to flush, then your tail latency blows up, possibly to infinity. It’s very easy to imagine situations involving multiple threads or processes that simply don’t work if buffers aren’t flushed on time. Imagine a program that is intended to crash on error but writes a log message first. If it buffers and doesn’t flush, then the log message will almost always get lost, whereas if the writer is unbuffered or is flushed, then the message will usually not get lost.
- josephg 1y ago> I’m not talking about data loss if the host crashes. I’m talking about a much broader sense of correctness Then why aren’t you talking about data loss if the host crashes? I consider that in the same bucket as your other examples. If you write a log and then the system crashes before it’s been written out to disk, the log message is lost. If a database issues two writes then the host crashes, what has been written to disk? It could be one write, both, neither, or a skewed write. If the database isn’t extremely careful, that crash could totally corrupt the database files. Correctness matters. Writes are sent to disk (or sent over the network) some time between when you call write and when your call to flush returns. Using a buffered writer API doesn’t (shouldn’t) change that.
- 0xcafefood 1y agoGreat point. It's like the earlier days where remote procedure calls were intended to happen "transparently" but the fact that networking is involved in some procedure calls and not others makes them very different in key ways that should not be hidden.