Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I no mention of fsync/sync_all. That’s why your disk file system is acting as fast as your in memory file system (for small tests). Both are effectively in-memory.


I guess I wasn't sufficiently clear in the post, but the part I think is interesting is not that tmpfs and SSD bench at the same speed. I am aware of in-memory filesystem caches, and explicitly mention them twice in the last few paragraphs.

The interesting part, to me, was that using the vfs crate or the rsfs crate didn't produce any differences from using tmpfs or an SSD. In theory, those crates completely cut out the actual filesystem and the OS entirely. Somehow, avoiding all those syscalls didn't make it any faster? Not what I expected.

Anyway, if you have examples of in-process filesystem mocks that run faster than the in-memory filesystem cache, I'd love to hear about them.


A Rust-specific danger is that, if you don't explicitly sync a file before dropping it, any errors from syncing are ignored. So if you care about atomicity, call eg `File::sync_all()`.


This is actually true for all programs (on Linux at least) because closing a file does not mean it will be synced and so close(2) may not return an error even if the later sync will error out.

The more general issue (not checking close(2) errors) is mostly true for most programming languages. I can count on one hand how many C programs I've seen that attempt to check the return value from close(2) consistently, let alone programs in languages like Go where handling it is far more effort than ignoring it.

Also, close(2) doesn't consistently return errors. Because filesystem errors are often a property of the whole filesystem and data written during sync has been disassociated from the filesystem, the error usually can't be linked to a particular file descriptor. Most filesystems instead just return EIO if the filesystem had an error at all. This is arguably less useful than not returning an error at all because the error might be triggered by a completely unrelated process and (as above) you might not receive errors that you do care about.

Filesystems also have different approaches to which close(2) calls will get filesystem errors. Some only return the error to the first close(2) call, which means another thread or process could clear the error bit. Other filesystems keep the error bit set until a remount, which means that any program checking close(2) will get lots of spurious errors. From memory there was a data corruption bug in PostgreSQL a few years ago because they were relying on close(2) error semantics that didn't work for all filesystems.


Is that really rust-specific? I would be really surprised if any other languages do fsync() in their destructor either


ETA: See correction below.

To be clear `File::drop()` does sync, it just ignores errors (because `drop()` doesn't have a way of returning an error). It's not really Rust specific I guess, I just don't know off the top of my head what other languages behave this way.


I believe C++'s fstreams also ignore errors on destruction for similar reasons.

I've wondered for a while what it'd take to eliminate such pitfalls in the "traditional" RAII approach. Something equivalent to deleting the "normal" RAII destructor and forcing consumption via a close() could be interesting, but I don't know how easy/hard that would be to pull off.


There's no reason that throwing exceptions in finalisers must be prohibited. It's just bad design.

Modern Java provides the concept of suppressed exceptions. Basically an exception can maintain the list of suppressed exceptions.

When stack unwinds, just allow finaliser to throw an exception. If it threw an exception, either propagate it to the handler, unwinding stack as necessary, or add it to the current exception as a suppressed exception. Exception handler can inspect the suppressed exceptions, if necessary. Exception print routine will print all suppressed exceptions for log visibility.

Java does not do it properly for finally blocks, instead overwriting current exception, probably because the concept of suppressed exception was introduced in the later versions and they wanted to keep the compatibility.

But it can be done properly.


IIRC C++ and Rust don't technically prohibit throwing exceptions out of destructors; it's triggering unwinding during unwinding that's the main problem.

Does make me wonder about the specifics behind that. I had assumed that there are some kind of soundness issues that force that particular approach (e.g., https://github.com/rust-lang/rust/pull/110975, "Any panics while the panic hook is executing will force an immediate abort. This is necessary to avoid potential deadlocks like rustc hangs after ICEing due to memory limit #110771 where a panic happens while holding the backtrace lock."; alternatively, some other kind of soundness issue?), but I don't have the knowledge to say whether this is a fundamental limitation or "just" an implementation quirk that basically got standardized. Rust' first public release was after Java 7, so in principle the precedent was there, for what it's worth.


> To be clear `File::drop()` does sync

It does not. BufWriter<File> flushes its userspace buffer (but doesn't fsync either). If you have a bare File then drop really just closes the file descriptor, that's it.

https://github.com/rust-lang/rust/blob/ee361e8fca1c30e13e7a3...


My bad, thanks for the correction.


For context - cppreference.com doesn't say anything about `fstream` syncing on drop, but it does have an explicit `sync` function. `QFile` from Qt doesn't even have a sync function, which I find odd.


I had always assumed that fstream flushes on destruction, but after digging through the standard all I can conclude is that I'm confused.

According to the standard, fstream doesn't have an explicit destructor, but the standard says "It uses a basic_filebuf<charT, traits> object to control the associated sequences." ~basic_filebuf(), in turn, is defined to call close() (which I think flushes to disk?) and swallow exceptions.

However, I can't seem to find anything that explicitly ties the lifetime of the fstream to the corresponding basic_filebuf. fstream doesn't have an explicitly declared destructor and the standard doesn't require that the basic_filebuf is a member of fstream, so the obvious ways the file would be closed don't seem to be explicitly required. In addition, all of fstream's parents' destructors are specified to perform no operations on the underlying rdbuf(). Which leaves... I don't know?

cppreference says the underlying file is closed, though, which should flush it. And that's what I would expect for an RAII class! But I just can't seem to find the requirement...


On most filesystems close(2) is nearly a noop, so even if you surfaced errors from close it returning successfully would not guarantee an absence of errors.

close without fsync (or direct IO) essentially is telling the OS that you don't need immediate durability and prefer performance instead.


I'd almost never want do to fsync in normal code (unless implementing something transactional)... but I'd want an explicit close almost always (or drop should panic/abort).


I use transactional file operations when for instance when I write tools that change their own human-readable configuration files. If something is important enough to write to disk, then it's probably important enough that you can't tolerate torn writes.


Why? The explicit close does almost nothing.


This is not correct. Programming languages do not and should not call sync automatically.


so the good old `sync; sync; sync;` ?




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: