After os.Rename returns nil, what still has to be synced for the new file to survive a power loss?
answer
- returning nil is not reaching the platter
- the page cache is lost on power failure
- data first, then the name
- the entry lives in the parent directory
- os.Open the directory and Sync it
basics
~20 sTwo syncs: the temp file's data with f.Sync before the rename, and the parent directory after it, by opening that directory and calling Sync. A successful rename swaps the name in cache only; neither the data nor the new entry is guaranteed on disk.
solid answer
~40 s`os.Rename` returning nil is a statement about what other processes see, not about what is on the platter. Two things can still be lost in a power cut. First, the temp file's data: sync it with `f.Sync()` **before** the rename, otherwise the crash can leave the target name pointing at a file whose blocks were never written - the classic zero-length config after a reboot. Second, the directory entry created by the rename: the parent directory is itself metadata, so after the rename you open it with `os.Open(filepath.Dir(target))` and call `Sync` on that handle, then close it. Do those in that order and the post-crash state is always either the whole old file or the whole new one. Skip the directory sync and the swap can simply be forgotten.
code
go · 9 linesif err := os.Rename(tmp, path); err != nil {
return err
}
d, err := os.Open(filepath.Dir(path))
if err != nil {
return err
}
defer d.Close()
return d.Sync() // commits the new directory entrygo deeper
Know that data written by your program sits in the operating system's cache first, and that f.Sync is the call that asks for it to be pushed to disk.
Explain the ordering: sync the temp file's data before the rename, because the filesystem may commit the name change before the bytes it points at.
Name both syncs and the failure each one prevents, and describe how you would prove it with crash injection rather than by reasoning. Be ready to say what an unsynced publish looks like in a postmortem.
Decide where on the durability spectrum a given write path belongs and make the cost explicit. Two fsyncs per file is cheap for rare config publishes and unaffordable at high write rates, and the answer must be written down, not implied.
## What a successful rename does and does not promise The temp-file-and-rename idiom buys *visibility* atomicity: no process ever observes a partially written file at the target path. That guarantee holds against concurrent readers. It does not hold against the machine losing power, because everything so far may still be sitting in the operating system's page cache. A crash discards that cache. What comes back after the reboot is whatever the filesystem had actually committed, in whatever order it chose to commit it. There are two separate pieces of state at risk, and they need two separate syncs. ## Sync the file's data, before the rename `f.Sync()` on the temp file asks the kernel to push that file's data (and enough metadata to find it) to stable storage, and to report an error if it cannot. It must happen **before** the rename, and the ordering is the whole point. A filesystem does not promise to commit a file's data before it commits an unrelated metadata change. If you rename first and sync afterwards, there is a window where the target name has been committed while the data blocks behind it have not. Come back from a crash and the config file exists, has the right name, and is empty or full of zeros - which is worse than a missing file, because a reader will happily parse it and find nothing. Syncing first means the sequence of durable states is: (old file, no temp) -> (old file, complete temp) -> (new file). Every one of those is a state a reader can cope with; the middle one just leaves litter. ## Sync the parent directory, after the rename The rename does not modify the file - it modifies the *directory* that contains the name. A directory is metadata, and metadata is cached too. If the machine dies after the rename returns but before that directory change is committed, the reboot can produce the pre-rename directory: the target still names the old file, and your publish silently did not happen. Nothing is corrupt, but a controller that reported success has lied. Making the swap durable means fsyncing the directory itself. In Go that is: ```go d, err := os.Open(filepath.Dir(path)) if err != nil { return err } defer d.Close() if err := d.Sync(); err != nil { return err } ``` `os.Open` on a directory returns an `*os.File` you cannot read bytes from, but whose `Sync` reaches the underlying handle, which is exactly what is wanted. This is a POSIX-shaped concern: on Linux and the BSDs it is the documented way to make a directory entry durable, and on Windows the durability model is different enough that portable code usually guards the step. ## What each omission looks like in a postmortem - **No file sync at all**: after an unclean shutdown the config exists but is empty or truncated; the sidecar fails to parse it and the node never becomes ready. This is the failure people remember, because it is permanent - nothing will rewrite the file until the controller notices a change. - **File synced, directory not**: after the crash the file is simply the old version. Confusing rather than fatal, and easy to misdiagnose as "the controller didn't run", because there is no corrupt artefact to find. - **Sync after the rename instead of before**: the rarest and the nastiest, because it is timing-dependent and passes every test on a machine that never crashes. ## How to actually test it Reasoning is not enough here; the ordering bugs only appear under a crash. A crash-injection harness is the honest test. Run the writer as a child process, kill it uncatchably at a chosen point - after the write, after the sync, after the rename - then re-open the target from the parent test and require that it parses as *either* the old document or the new one, never anything else. Loop over the injection points, and loop each point many times. Killing the process only exercises the case where the process dies, not where the machine does, so a thorough version pairs it with a filesystem or device that can be told to drop unsynced writes. ## The cost, and when to skip it Both syncs are real I/O and can dominate the cost of writing a small file. For a config file rewritten when desired state changes, that is irrelevant - it happens rarely and correctness is the entire point. For a hot path writing thousands of small files a second, the same pattern will show up as latency, and then the decision is explicit: batch the writes, sync once per batch, or accept that the last few writes are lossy and say so in the design. What you must not do is skip the syncs silently and describe the result as crash-safe.
- Why sync the temp file before the rename rather than after it?Because a filesystem does not promise to commit a file's data before an unrelated metadata change. Rename first and a crash can leave the target name committed while the data blocks behind it were never written - an empty file under the right name, which a reader will happily parse. Syncing first makes every durable intermediate state a safe one.
- How would you actually test that the rewrite is crash-safe?Crash injection. Run the writer as a child process, kill it uncatchably after the write, after the sync and after the rename, then re-read the target from the test and require it to parse as either the old or the new document. Repeat each injection point many times; killing a process only covers process death, so pair it with something that can drop unsynced writes.
- Is the directory sync worth its cost on every write path?For a config rewritten when desired state changes, yes - it is rare and correctness is the whole point. On a path writing thousands of small files a second the syncs dominate latency, and then you make the tradeoff explicitly: batch and sync once, or document that recent writes are lossy. What you cannot do is drop them and still call the result crash-safe.
saying these in an interview costs you the question
- Treats a nil error from os.Rename as durability
- Syncs the file but never the parent directory
- Believes f.Close flushes to the physical device
- Puts f.Sync after the rename instead of before
- Claims crash-safety with no crash-injection test