Say what a safe save actually promises, and where it stops
Both sbfsSaveFile and sbfsStreamDone write a temporary, delete the original and rename the temporary onto its name, and the README explained why that order and not the obvious one. It never said what the resulting guarantee is, which invited the stronger reading: "safe save" sounds like it survives anything. It survives everything that goes wrong while it is running - no run long enough, no free blocks, a refused write, a name that turns out to be a directory, a writer that gives up - and in all of those the original is untouched. It does not survive the machine stopping, because the commit is two block writes with a gap between them: stop there and the old file is gone and the new one is still called sbfs.part. The bytes are all present and one rename by hand recovers them, but nothing does that on its own. Written down in the README beside the ordering it qualifies, and at both commit points in sbfs.asm, where a reader is when the question occurs to them. Closing the gap wants a journal or a second directory, which is a lot of disk for two writes; a boot-time consistency check is the cheaper answer if it ever matters, and it would want temporaries flagged in the entry rather than recognised by name.
This commit is contained in:
@@ -757,6 +757,14 @@ A create can be refused for want of a run long enough even on a disk with plenty
|
||||
|
||||
**That is what renaming is for.** It looks like a convenience and it is the safety mechanism: it is the only one of the three operations that moves no data - a name lives in the directory entry, so renaming writes twenty two bytes into one block - which makes it the only one that can be left until last and relied on not to fail.
|
||||
|
||||
#### Exactly what that promises:
|
||||
|
||||
The ordering protects the original against **every way a save can fail while it is running**, and it is worth naming those, because they are the ones that actually happen: there is no run of free blocks long enough, or none at all; the disk refuses a block write; the name turns out to belong to a directory; the writer gives up part way through. In all of them the file that was already there is untouched, and what is lost is the temporary, which nothing had come to depend on yet.
|
||||
|
||||
It is **not** power-loss atomic, and nothing about SBFS claims it is. The commit is two block writes - delete the old entry, then give the temporary its name - and a machine that stops between them leaves the old file gone and the new one under the temporary's name. Both writes are to the directory, so `sbfs.part` or `sbfs.out` is sitting there holding every byte of the work; the data survives and the name does not, and putting it right is one `rename` typed by hand.
|
||||
|
||||
Closing that window means a journal or a second copy of the directory, and both are a great deal of machinery to buy back a two-write gap on a machine with no power failures to speak of. The honest description is the one to write down: **safe against the failures of ordinary operation, not against the machine stopping.** A future consistency check at boot could reclaim an abandoned temporary, and would want the entry to say it is one rather than to be recognised by its name.
|
||||
|
||||
Finding room is a walk through the directory rather than a lookup, because there is no allocation table. With files laid down contiguously the directory already says which blocks are spoken for, and a second copy of that would be a second thing to keep right. The free count in the superblock is kept up to date but it is a note rather than the truth: it can be worked out again from the directory, and the directory is the one to believe.
|
||||
|
||||
A file's length is its block count times 256 plus its tail, which is the same as putting the block count in the high byte and the tail in the low one. Nothing pads a file out, so the bytes after the end of one are whatever else happened to be in that block, and it is the reading program's business to stop where the tail says.
|
||||
|
||||
@@ -2632,6 +2632,11 @@ sbfsStreamFits:
|
||||
|
||||
; Now, and not before, the old one goes. It may not be there at all, which is what
|
||||
; writing something for the first time looks like from here.
|
||||
;
|
||||
; The same two writes and the same gap between them as sbfsSaveFile has, and the same
|
||||
; answer: what stops here is recoverable by hand and by nothing else. The free count
|
||||
; goes back afterwards, and a machine stopping THERE only leaves the note wrong, which
|
||||
; the directory can always settle.
|
||||
CALL sbfsStreamWhere
|
||||
CALL sbfsScanFor
|
||||
BNQ sbfsStreamNoOld
|
||||
@@ -2827,6 +2832,14 @@ sbfsSaveNoTemp:
|
||||
|
||||
; Now, and not before, the old one goes. It may not be there at all, which is what
|
||||
; saving something for the first time looks like from here.
|
||||
;
|
||||
; EVERYTHING ABOVE THIS POINT CAN FAIL AND COST NOTHING. Below it there are two block
|
||||
; writes and a gap between them, and a machine that stops in that gap has deleted the
|
||||
; old file and not yet named the new one. The bytes are all there under the temporary's
|
||||
; name and one rename gets them back, but nothing does that automatically and nothing
|
||||
; here claims to: this is safe against the ways a save fails while it is running, not
|
||||
; against the machine stopping. Closing the gap wants a journal, which is a great deal
|
||||
; of disk to buy back two writes.
|
||||
CALL sbfsSaveWhere
|
||||
CALL sbfsScanFor
|
||||
BNQ sbfsSaveNoOld
|
||||
|
||||
Reference in New Issue
Block a user