The specific concern is the durability of the directory entries needed to resolve P/R/start.sqlite. I need to distinguish the persistence of the database contents from the persistence of the names used to reach that database. I am not assuming that a successful SQLite commit, by itself, establishes every parent-directory durability requirement.
So, one thing to understand here is that the file system considers the proper handling of filesystem structure data to be significantly more important than than the commitment of individual file data. Structurally, APFS (like virtually every modern file system) is designed to "guarantee" that its file system state is ALWAYS coherent under ALL circumstances. I mean that quite literally. Twenty+ years ago I put together a rig designed to intentionally destroy volumes. It worked by:
-
Starting the machine up and starting up a whole bunch of long-running copies.
-
Letting the copies run for a few minutes.
-
Using an external hardware device to cut power to the device.
-
Restoring power to the machine, looping the entire process back to #1.
On a standard HFS+ file system (no journal), it would render the drive unmountable in ~4 hours. With a journaled HFS+ volume... it didn't work, even when allowed to run for "days". This was pre-APFS, but APFS would do just as well.
That then leads to here:
I need to distinguish the persistence of the database contents from the persistence of the names used to reach that database. I am not assuming that a successful SQLite commit, by itself, establishes every parent-directory durability requirement.
I THINK what you're worried about here is that the file's data will somehow be written to disk but the directory and file record entries won't be. If so, then the answer is basically "no, that won't happen". First off, as noted above, the file system is significantly more concerned about pushing out metadata/structure data than it is about file-level data.
However, the bigger issue is tied to what a "file" actually is at the file system level. As far as the file system is concerned, "a file" is a particular type of record within its data structures. That happens to point to a location on disk where the actual data blocks are stored, but the record itself IS the file, NOT the data blocks on disk. If the file record doesn't exist, then the data blocks are irrelevant, as that file record is the only tool it has to find them.
Putting that another way, if a catastrophic event (crash, power loss, etc.) occurs, then the file system will be in one of these four states when it's mounted again:
-
None of your content is there.
-
The directory exists and nothing else.
-
The directory and file both exist, but the file's contents are incomplete or missing.
-
Everything is there.
Those are the only states the file system will mount in because it's designed such that IF it happens to be in any transition state other than those, it will force itself to one of those 4 states so that it guarantees its own integrity.
The question that then leads to is "how do I guarantee that I'm at #4" and the answer is, unfortunately, you can't. All you can do is "ask nicely" and "hope". The reason for that is exactly why both "fsync" and "F_FULLFSYNC" exist.
Starting with fsync, its man page says:
fsync() causes all modified data and attributes of fildes to be moved to a permanent storage device. This normally results in all in-core modified copies of buffers for the associated file to be written to a disk.
On the surface, "moved to a permanent storage device" makes it sound like you'd now be "done", as the data has been sent to the storage device. Unfortunately, the problem here is caching. Over time, the performance gap between the overall system and mass storage has gotten larger and larger, so mass storage devices responded by using more and more memory to reduce/mask that gap. That works great, except power loss means the cache is destroyed and your data is gone. That leads to "F_FULLFSYNC":
"Does the same thing as fsync(2) then asks the drive to flush all buffered data to the permanent storage device (arg is ignored). As this drains the entire queue of the device and acts as a barrier, data that had been fsync'd on the same device before is guaranteed to be persisted when this call returns. This is currently implemented on HFS, MS-DOS (FAT), Universal Disk Format (UDF) and APFS file systems. The operation may take quite a while to complete. Certain FireWire drives have also been known to ignore the request to flush their buffered data."
...and the problem there is the words "asks the drive". We do not control the final I/O target, so we've got no way to GUARANTEE it will do what it's told. If you're writing to our devices, then F_FULLFSYNC will ensure that your changes have made it all the way to disk. If it's not one of our devices... then I can't promise anything. F_FULLFSYNC should and generally is sufficient, but that's the strongest promise I can give.
Which objects need to be synchronized, and in what order: the database file, R, P, or P's parent? Please distinguish the case where P already existed from the case where P was newly created during initialization.
An F_FULLFSYNC to a file at the bottom of a newly created hierarchy will implicitly flush the data of newly created parents. Any other behavior would implicitly "encourage" file system corruption. For maximum safety, you could:
-
Issue an fsync to all objects.
-
(optional) Issue an F_BARRIERFSYNC to one file. (This is a new command and not supported on all hardware. See man fcntl for details.)
-
Issue an F_FULLFSYNC to the same file.
Note that issuing multiple F_FULLFSYNCs is pointless, nor does it matter which object you issue the F_FULLFSYNC on. The command is being sent to the drive itself and applies to the entire drive cache.
What guarantees differ between an OS crash and sudden power loss?
The guarantee itself doesn't change but the likelihood of failure does. Using F_FULLFSYNC as an example, it doesn't actually matter what happens to the machine as long as the command itself reached the storage device. The storage device is still going to finish flushing its buffers, even if the machine itself panics. However, if the same drive loses power then, yes, it will lose data.
Is there authoritative documentation applicable to macOS 15.7.2 that describes this contract?
Everything I've discussed here is in the man pages for fsync and fcntl.
Finally, a warning here:
guarantees against arbitrary hardware failure.
The problem here is that "arbitrary hardware failures" ARE the problem. If you assume the target hardware behaves "perfectly", then fsync is all that's required—it sends the data to the drive and the drive writes it out. The problem, and the reason F_FULLFSYNC was created, is that you CAN'T trust the hardware.
__
Kevin Elliott
DTS Engineer, CoreOS/Hardware