Say a temporary is one in the entry, not in its name
Saving something that already exists writes a temporary, deletes the original and gives the temporary its name, so that nothing is lost if the writing fails. The temporary was told apart from a real file by being called sbfs.part or sbfs.out - and those are legal names. Starting a save deleted whatever answered to one as stale scratch, so saving anything at all in a directory destroyed your own file of that name there, silently. Flag bit 0x04 now says it. The property is not in the contents - the same bytes become the finished file the instant the rename lands - so it belongs in the entry, which is the thing the commit changes. sbfsCreateTempAt is the door temporaries come in by, the commit writes the flags flat along with the name, and cleanup wipes what it finds only if the entry says it is ours. Anything else stops the save instead. The bit is also the recovery. Both listings show an unfinished write rather than sizing it, because the size in the entry is the room that was asked for and not what was written: "<unfinished>" from dir, and a line from SplitDisk saying the blocks are held and a rename brings the data back. That was the gap in what the last commit documented - the data survived a crash and nothing would show you where it was. Four new agreement checks, three of which fail with the guards removed. The fourth needed rebuilding first: both tests started on one disk, and the first save ate the sbfs.part that was the second test's SOURCE, so the copy failed for want of a file, never opened a stream, and passed while reporting on nothing. A disk each. The fifth check forges the wreckage by setting the flag on a finished file, since nothing here can crash a save half way through. No version bump: a committed file never carries the bit, so a disk this writes is byte for byte the disk the old code wrote, which the whole-image comparisons confirm. Only the wreckage differs, and older code reads that as an ordinary file - which is what it did before.
This commit is contained in:
@@ -761,9 +761,26 @@ A create can be refused for want of a run long enough even on a disk with plenty
|
||||
|
||||
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.
|
||||
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. `dir` marks it `<unfinished>` so that it can be found, which is the whole of the recovery this format offers.
|
||||
|
||||
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.
|
||||
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.**
|
||||
|
||||
#### What tells a temporary from a file:
|
||||
|
||||
While the save runs the temporary is an ordinary entry in every way that matters - it holds real blocks and answers to a name - and the only thing that makes it different is that nobody has committed it yet. That is **not a property of its contents.** The same bytes become the finished file the instant the rename lands, so there is nothing to put inside it that would be true. It belongs in the entry, which is the thing the commit changes, and it is **flag bit `0x04`**.
|
||||
|
||||
It used to be told apart by being *called* `sbfs.part` or `sbfs.out`, and those are names anybody is entitled to give a file of their own. Starting a save deleted whatever answered to one, as stale scratch - so saving anything at all in a directory destroyed your file of that name there, silently, and the first you would know of it is going to look for it. A file that does not carry the flag now belongs to somebody, and the save is **refused** rather than helping itself to the name.
|
||||
|
||||
The same bit is what makes an interrupted save recoverable. Both listings show an unfinished write rather than sizing it, because the size in the entry is the room it asked for and not what was written into it:
|
||||
|
||||
```
|
||||
> dir
|
||||
stranded.txt <unfinished>
|
||||
```
|
||||
|
||||
Rename it to keep the data, delete it to give the blocks back. Nothing reclaims it on its own; a boot-time consistency check could, and this is the field it would read.
|
||||
|
||||
**A committed file never carries the bit**, so a disk this writes is byte for byte the disk the older code wrote - the agreement tests compare whole images and say so. Only the wreckage of a save that stopped looks different, and code that has never heard of the flag reads that as an ordinary file, which is exactly what it did before.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -356,6 +356,19 @@ dirCheck:
|
||||
MVQA
|
||||
CALL printSpaces
|
||||
|
||||
; A save that stopped before it committed says so instead of saying a size, because the
|
||||
; size it holds is the room it asked for rather than what was written into it. Asked
|
||||
; first, since it is the one thing here that is not really a file yet.
|
||||
;
|
||||
; IT IS SHOWN RATHER THAN HIDDEN, and that is the whole of the recovery this format
|
||||
; offers: the bytes are all there under that name, so seeing it is what lets somebody
|
||||
; rename it back. Left off the listing it would be blocks nobody could account for.
|
||||
SETD.0 SbfsFoundFlags
|
||||
LDA.0
|
||||
INIB 0x04
|
||||
AND
|
||||
BNQ dirUnfinished
|
||||
|
||||
; A directory says so instead of saying a size. It has no blocks, so the arithmetic
|
||||
; below would call it a file of no bytes - which is a different thing that happens to
|
||||
; look the same from here.
|
||||
@@ -406,6 +419,12 @@ dirInBlocks:
|
||||
CALL newLine
|
||||
BRI dirStep
|
||||
|
||||
dirUnfinished:
|
||||
SETD.0 UnfinishedText
|
||||
CALL printString
|
||||
CALL newLine
|
||||
BRI dirStep
|
||||
|
||||
dirIsDirectory:
|
||||
SETD.0 DirFolders
|
||||
LDA.0
|
||||
@@ -3312,6 +3331,8 @@ Farewell:
|
||||
"halted"
|
||||
DirectoryText:
|
||||
"<dir>"
|
||||
UnfinishedText:
|
||||
"<unfinished>"
|
||||
BlocksText:
|
||||
" blocks"
|
||||
NotDirectory:
|
||||
|
||||
@@ -9,6 +9,12 @@
|
||||
; between them but the written specification, so anything that changes here has to change
|
||||
; there in the same breath.
|
||||
;
|
||||
; ---- What an entry's flags mean ----
|
||||
;
|
||||
; 0x01 In use. A zero here is a free slot, whatever else the bytes hold.
|
||||
; 0x02 A directory, which has no blocks at all.
|
||||
; 0x04 A file being written and not yet committed. See sbfsSaveFile.
|
||||
;
|
||||
; The disk's buffer is registered as bank 3, and every block read lands there and is then
|
||||
; blitted where it is wanted. The CPU never touches the buffer directly, because nothing
|
||||
; can: memory a device brings is reachable only through the controller.
|
||||
@@ -1559,6 +1565,22 @@ sbfsCreate:
|
||||
BNQ sbfsCreateFailed
|
||||
|
||||
sbfsCreateAt:
|
||||
; A plain file, unless somebody came in at the other door. Set here rather than left
|
||||
; over from last time, so that one temporary does not make the next ordinary file one.
|
||||
INIA 0x01
|
||||
SETD.0 SbfsMakeFlags
|
||||
STA.0
|
||||
BRI sbfsCreateGo
|
||||
|
||||
; The same, for the half written file a save puts down before it dares touch the original.
|
||||
; It is an ordinary entry in every other way - it holds blocks and answers to a name - and
|
||||
; the flag is the whole of what says it is not finished. See sbfsSaveFile.
|
||||
sbfsCreateTempAt:
|
||||
INIA 0x05
|
||||
SETD.0 SbfsMakeFlags
|
||||
STA.0
|
||||
|
||||
sbfsCreateGo:
|
||||
CALL sbfsFileExtent
|
||||
CALL sbfsAllocate
|
||||
BNQ sbfsCreateFailed
|
||||
@@ -1614,8 +1636,9 @@ sbfsCreateFill:
|
||||
; DP2 is on the entry. Fill it in, then put the whole block back on the disk.
|
||||
PSHD.2
|
||||
POPD.3
|
||||
INIA 0x01
|
||||
STA.3 ; In use.
|
||||
SETD.0 SbfsMakeFlags
|
||||
LDA.0
|
||||
STA.3 ; In use, and possibly not finished being written.
|
||||
|
||||
PSHD.3
|
||||
POPD.1
|
||||
@@ -2450,11 +2473,17 @@ sbfsStreamStart:
|
||||
BNQ sbfsStreamNo
|
||||
sbfsStreamFresh:
|
||||
|
||||
; A temporary left by a stream that did not finish would be in the way. Whether there was
|
||||
; one is not worth asking about, since either answer leads here.
|
||||
; A temporary left by a stream that did not finish would be in the way, so it goes - but
|
||||
; only if it really is one. See sbfsSaveFile: the flag says so and the name does not,
|
||||
; because "sbfs.out" is a name somebody may have chosen for themselves.
|
||||
CALL sbfsStreamTemp
|
||||
CALL sbfsScanFor
|
||||
BNQ sbfsStreamNoTemp
|
||||
SETD.0 SbfsFoundFlags
|
||||
LDA.0
|
||||
INIB 0x04
|
||||
AND
|
||||
BRQ sbfsStreamNo ; Somebody's own file, under a name we wanted. Left alone.
|
||||
CALL sbfsWipeFound
|
||||
sbfsStreamNoTemp:
|
||||
|
||||
@@ -2468,7 +2497,7 @@ sbfsStreamNoTemp:
|
||||
STA.1
|
||||
|
||||
CALL sbfsStreamTemp
|
||||
CALL sbfsCreateAt
|
||||
CALL sbfsCreateTempAt
|
||||
BNQ sbfsStreamNo
|
||||
|
||||
; WHERE THE TEMPORARY BEGINS, KEPT NOW. Nothing moves a file once it is made, so every
|
||||
@@ -2650,6 +2679,13 @@ sbfsStreamNoOld:
|
||||
CALL sbfsScanFor
|
||||
BNQ sbfsStreamNo
|
||||
|
||||
; Finished: the flag goes down in the same block write that gives it its name and its
|
||||
; size. Flat rather than cleared, for the reason sbfsSaveFile gives.
|
||||
PSHD.3
|
||||
POPD.1
|
||||
INIA 0x01
|
||||
STA.1
|
||||
|
||||
PSHD.3
|
||||
POPD.1
|
||||
DPUP.1 0d06
|
||||
@@ -2805,11 +2841,20 @@ sbfsSaveFile:
|
||||
BNQ sbfsSaveFailed
|
||||
sbfsSaveNotThere:
|
||||
|
||||
; A temporary left behind by a save that did not finish would be in the way. Whether
|
||||
; there was one is not worth asking about, since either answer leads here.
|
||||
; A temporary left behind by a save that did not finish would be in the way, so it goes.
|
||||
;
|
||||
; WHAT MAKES IT ONE IS THE FLAG AND NOT THE NAME. "sbfs.part" is a name a person is
|
||||
; perfectly entitled to give a file of their own, and this used to delete whatever
|
||||
; answered to it - so saving anything at all, once, quietly destroyed that file. The
|
||||
; entry now says what it is, and something that is not ours stops the save instead.
|
||||
CALL sbfsSaveTemp
|
||||
CALL sbfsScanFor
|
||||
BNQ sbfsSaveNoTemp
|
||||
SETD.0 SbfsFoundFlags
|
||||
LDA.0
|
||||
INIB 0x04
|
||||
AND
|
||||
BRQ sbfsSaveFailed ; Somebody's own file, under a name we wanted. Left alone.
|
||||
CALL sbfsWipeFound
|
||||
sbfsSaveNoTemp:
|
||||
|
||||
@@ -2822,7 +2867,7 @@ sbfsSaveNoTemp:
|
||||
STA.1
|
||||
|
||||
CALL sbfsSaveTemp
|
||||
CALL sbfsCreateAt
|
||||
CALL sbfsCreateTempAt
|
||||
BNQ sbfsSaveFailed
|
||||
|
||||
SETD.2 SbfsSaveData
|
||||
@@ -2852,6 +2897,14 @@ sbfsSaveNoOld:
|
||||
CALL sbfsScanFor
|
||||
BNQ sbfsSaveFailed
|
||||
|
||||
; FINISHED, WHICH IS THE FLAG AND THE NAME TOGETHER. Written flat rather than by
|
||||
; clearing the one bit: a temporary is a file, so in use is the only other thing it can
|
||||
; ever have been, and the whole byte is known.
|
||||
PSHD.3
|
||||
POPD.1
|
||||
INIA 0x01
|
||||
STA.1
|
||||
|
||||
PSHD.3
|
||||
POPD.1
|
||||
DPUP.1 0d06
|
||||
@@ -2987,6 +3040,11 @@ SbfsUpParent:
|
||||
SbfsFoundFlags:
|
||||
0x00
|
||||
|
||||
; What the next entry made is to be marked with. sbfsCreateAt sets it to a plain file and
|
||||
; sbfsCreateTempAt to a temporary, so it is never read without having just been written.
|
||||
SbfsMakeFlags:
|
||||
0x00
|
||||
|
||||
; Whether what is in SbfsFileStart and the rest really describes where the walk is now.
|
||||
; Going up leaves them describing the entry it came from, and only the end of a path is
|
||||
; close enough to care.
|
||||
|
||||
Reference in New Issue
Block a user