b76ff63e53
Now mixing in truncate/fruncate, along with desync<->sync state transitions. Found bugs: - Fixed propagating LFS_F_UNSYNCED/LFS_F_UNFLUSHED state during sync broadcasts. This is important for tracking small files correctly. - We were not clearing the btree erased-state of other opened file handles when we started using it, leading other file handles to have out-of-date erased-state. I considered moving this into lfsr_btree_commit, but file btrees are really the only place where shared references make sense, and it feels weird to scan file btrees every time we commit to the mtree. - Fixed syncs not propagating to other file handles when file is synced with disk. It's interesting that lfsr_file_sync can actually have an effect on the system when the disk in is-sync. - Added O_FLUSH/O_SYNC support to lfsr_file_truncate/fruncate. This omission was just an oversight. Unfortunately this did add quite a bit more complexity to both functions. You may notice in the fix for that last bug, that lfsr_file_ftruncate sort of drops the ball with regards to error-idempotency. This is because, as I was trying to figure out how to recoverably move the buffer around when fruncating small files, I realized we don't handle small files in lfsr_file_write correctly w.r.t. error-idempotency, and that fixing this may be intractable... The issue is how handle overwrites for unflushed buffers. In general, the correct thing to do when an incoming write overlaps our file buffer, is to just write over the buffer with the new data. Ah, but if we do this, how do we get the old data back if we run into an error writing the data to disk? It's gone! For normal files, this is not an issue. We can always flush to disk to reclaim our buffer, and since a flush doesn't change the file contents, it's fine to make this our new fallback state. But for small files, flush is a noop, we keep these entirely in RAM. There are some possible workarounds: - Flush small files to disk before overwriting, sort of defeats the purpose of caching these in RAM... - Reread small files from disk, because that's definitely what you want to do when you hit an error... Also, to always have something we can read from disk implies flush on overwrite, see above. - Sacrificing half our buffer for staging small files. Because RAM cost is totally not a priority... Long story short, rethinking idempotent errors.