Open stapelberg opened 7 years ago
There are a few implementations in the wild, I think they should be analyzed and mentioned in the proposal.
Yes, this seems like something that should start as an external package. https://golang.org/doc/faq#x_in_std
But perhaps we could add something to golang.org/x/tools or golang.org/x/sync.
I analyzed a number of implementations I could find using GitHub’s search feature:
os.Rename + chmod:
os.Rename + chmod + chown:
These implementations unconditionally sync before renaming:
This implementation syncs the containing directory to persist the rename:
These implementations have Windows-specific code:
Action items:
@rasky Does that satisfy your concern? Did you have any particular implementations in mind that you wanted to include in the analysis?
@stapelberg thanks that's a good start. What I miss from the above comment is why those libraries are not "good enough". You seem to know the problem space well, but are not explaining it. When you say for instance "os.Rename + chmod + chown". I'm not sure if that's correct, or a bug, or just a different way of doing it.
I suggest a structure that you can use to edit your proposal above, to provide more information. To clarify, I'm not able to approve your proposal, I'm just trying to help you providing enough information for a maintainer to make a call on this. This is just a suggestion.
Problem statement Atomically create or update a file is important for [...]. (I think everybody knows that, so just a short concise paragraph). A basic implementation is often buggy because [common bugs or side cases not handled] [complex issues about filesystems working differently from system to system] Thus, I propose to add a standard implementation somewhere in "x/".
Existing options There are N main libraries with at least X stars on GitHub. They have the following problems:
Proposal I propose to write a library that does this and that, and fixes all the above problems. A prototype is here (link).
Sounds like we're still waiting on @stapelberg here.
Yes. I’ll follow up with the requested write-up as soon as I find a moment of time for it.
Atomically creating or replacing files is a technique used to prevent intermediate (regular operation) or corrupted (when crashing) files from appearing in e.g. a webserver directory.
Side note: while related, the concept of durability is not covered by this proposal at all. In other words, if you would like to ensure your data made it to your disk(s), you’ll need to call Sync() yourself. The proposal only concerns itself with ensuring that what has been written was written atomically (i.e. either the old data is present, or the new data is present, but never intermediate/corrupt data).
A naive approach to the problem is to create a temporary file followed by a call to os.Rename
. However, there are a number of subtleties which make the correct sequence of operations hard to identify:
The temporary file should be removed when an error occurs, but a remove must not be attempted if the rename succeeded, as a new file might have been created with the same name. This renders a throwaway defer os.Remove(t.Name())
insufficient; state must be kept.
The temporary file must be created on the same file system (same mount point) for the rename to work, but the TMPDIR
environment variable should still be respected, e.g. to direct temporary files into a separate directory outside of the webserver’s document root but on the same file system.
On POSIX operating systems, the fsync
system call must be used to ensure that the os.Rename()
call will not result in a 0-length file.
There are 4 notable libraries on GitHub:
TMPDIR
environment variable.WriteFile(string, io.Reader)
, which makes some use-cases cumbersome or impossible to implement, e.g. streaming data into different parts of the temporary file, or calling Sync() on the file for durability.TMPDIR
environment variable.TMPDIR
environment variable.There are a number of other ad-hoc implementations in applications (as opposed to libraries): https://github.com/golang/go/issues/22397#issuecomment-372579571
Some of these conflate durability with atomicity, get the order of operations wrong, or exhibit issues similar to the ones listed in the 2 libraries above.
I propose to add a package to the x/ namespace which offers atomic file creation/replacement, using explicit names in its method signatures (accompanied by detailed doc comments), addressing all the shortcomings listed above.
Find the prototype at https://go-review.googlesource.com/c/exp/+/72379
/cc @aclements @bradfitz @bcmills
See also #17869.
The temporary file must be created on the same file system (same mount point) for the rename to work, but the
TMPDIR
environment variable should still be respected
What should happen if TMPDIR
is not on the destination file system?
What should happen if TMPDIR is not on the destination file system?
An error will occur, and the user must set TMPDIR
to point to the destination file system.
I’m not sure whether erroring at rename time (more efficient when things go right) or adding explicit checking (more user-friendly when things go wrong) is better. In case the writes are small, there isn’t much difference, so I’m thinking about cases where writes take a long time.
Maybe we could use sync.Once under the assumption that TMPDIR
doesn’t change at runtime to remove performance considerations from an explicit check.
I think that TMPDIR
is more of a user/admin preference, and it is not uncommon to have it point to its own filesystem. Maybe we have different use cases in mind, but requiring the program user to change TMPDIR
seems odd to me.
As the caller I can think of two cases:
TMPDIR
with automatic fallback to same directory)In the second case I would prefer to be able to pass it, rather then change TMPDIR
from inside the Go program, so that I don't have to worry about the implication of changing the environment (concurrent access, child processes).
A minor note about your prototype: in the commented out example you use defer
and log.Fatal
, but deferred functions will not run on log.Fatal
, you probably intended return err
.
You've enumerated some good problems, so it would be nice if we can find a way to address them, but I think the API as proposed still needs work. (I left some comments on the CL, but I wanted to give a higher-level overview here.)
The current draft seems both too subtle and too simple: too subtle because it still requires the caller to know whether they need to call Sync
before renaming (and whether to use your Chmod
or os.Chmod
directly), but too simple because it makes some pretty strong assumptions about how to choose the temporary directory.
Is there some way we can address those issues within the API itself? (For example, some standard way to detect whether a given filesystem is ordered and/or uses write barriers?)
A few more considerations:
os.TempDir
instead of accessing TMPDIR
directly.syscall.Stat_t.Dev
.Sync
before Rename
, and think about optimization if necessary.
Is the performance impact really significant?
If it is, maybe we could recognize common filesystem configurations and avoid Sync in those cases, if a more accurate determination is too difficult.Chmod
serves a purpose other than just changing mode (fchmod hack), then the api shuold call it for you if you don't, and take care of everything.
Does it make a difference if Chmod is called before/between/after the writes, or if the new mode is the same as the old?Thanks for the feedback, everyone.
I have uploaded a new revision of my change which, I believe, addresses all the concerns brought up so far.
A key insight came from discussion with Theodore Ts'o (ext2/3/4 lead developer), who stated:
data=ordered only guarantees the avoidance of stale data (e.g., the previous contents of a data block showing up after a crash, where the previous data could be someone's love letters, medical records, etc.). Without the fsync(2) a zero-length file is a valid and possible outcome after the rename.
The same holds for file systems with write barriers.
The conclusion from our discussion is that the only way to achieve atomicity in a cross-filesystem, cross-platform way, is to rely on the fsync.
If one wants to optimize the fsync away, it seems like one would need to rely on the specific version and mount options of a specific file system, and even then, you wouldn’t get guaranteed atomicity, just a reduced likelihood of crashes violating atomicity.
With all my assumptions being proven wrong, this simplifies the package quite a bit. The Chmod method, and the (incorrect) caveats are gone. I changed the signature of TempFile such that callers can specify a temporary directory.
One remaining assumption we’re making is that people use journaling file systems. If we don’t want to make that assumption, we’d need to also fsync the directory containing the file we wrote, to cover the case where the old inode and the new inode are in separate blocks and hence might be flushed out at different times.
It seems like this can easily belong outside the standard library, or outside golang.org/x, at least until we know how to implement it. I'm concerned that the implementation is still changing, and what about systems without fsync?
It seems like this can easily belong outside the standard library,
Definitely.
or outside golang.org/x, at least until we know how to implement it.
I thought x/exp (where I’m suggesting to add the package for the time being) was a staging ground for packages in x. Am I mistaken?
I'm concerned that the implementation is still changing, and what about systems without fsync?
Which systems lack fsync? It seems like the primary and only portable guarantee for both atomicity and durability.
The Go team nominally maintains the packages in x/exp. We're suggesting putting it in github.com/stapelberg (or wherever) for now.
To the best of my knowledge neither Windows nor Plan 9 support fsync
. But maybe you are using different mechanisms on those systems.
The Go team nominally maintains the packages in x/exp. We're suggesting putting it in github.com/stapelberg (or wherever) for now.
My hope was that we could (eventually) reduce some duplication across the ecosystem by putting this package in a somewhat prominent place.
Is there any path forward where the package could eventually end up in x?
To the best of my knowledge neither Windows nor Plan 9 support fsync. But maybe you are using different mechanisms on those systems.
Plan 9 does seem to support fsync, or are you saying that the current os.File.Sync implementation in file_plan9.go is not sufficient? https://github.com/golang/go/blob/6fe7b434160c84cbac1157073a795ac6e9f30479/src/os/file_plan9.go#L222-L241
On Windows, it seems like we should use syscall.FlushFileBuffers(syscall.Handle(f.Fd()))
(see e.g. https://www.humboldt.co.uk/fsync-across-platforms/).
As @rsc said, it could wind up in x/ when it is implemented and stable.
There is nothing magic about x/. godoc.org lists packages from all over the place. Of the 25 top popular packages that it lists, 3 are from x/.
Okay. Should we leave this issue open, or should I open a new one once the package has stabilized?
I'll put the proposal on hold for now.
FYI: the package is now published at https://github.com/google/go-write
Change https://golang.org/cl/146377 mentions this issue: cmd/go/internal/renameio: add package
See previously #8868 and #17869.
It turns out that os.Rename
is not currently atomic on Windows (see #32188), so an atomic-replace package would be useful there too.
The outcome of my investigation for #32188 was that it appears not to be possible to write a Windows atomic-rename library that avoids spurious errors in all cases: even using the recommended ReplaceFile
system call, readers can still observe any of at least three errors under high write loads.
(That doesn't preclude such a library for POSIX systems, though.)
Reading https://github.com/google/renameio, the idea of doing a Rename to see whether we can later do a rename from /tmp to whereever seems weird, wasteful, and backwards. Just create the temp file in the same directory as the target; it's what everyone does, and the only thing guaranteed to work.
This seems off-topic on this issue, so if you have any follow-up, please open an issue on https://github.com/google/renameio itself.
For https://manpages.debian.org/, we need to create the files outside of the www directory (to prevent rsync from picking them up), which we achieve by setting TMPDIR. Respecting TMPDIR seems like a good thing to me.
If this bothers you, you can override this behavior by just passing filepath.Dir(path) as dir parameter to renameio.TempFile.
On the contrary, it drives home to point that there's a ton of decisions in such a thing, that aren't carved in stone yet, and thus it's not necessarily stdlib time.
You could easily give rsync an exclude filter that makes it ignore temp files.
Okay. Should we leave this issue open, or should I open a new one once the package has stabilized?
I'll put the proposal on hold for now.
It’s been almost two years since we put the proposal on hold, so I think it might be time to dust it off and take a fresh look :)
In the meantime:
https://github.com/google/renameio has amassed 416 stars on GitHub, and is imported by 40 (known) packages. I interpret this as renameio being popular, and solving an issue that’s worth solving.
https://github.com/google/renameio has been largely untouched since 2018, aside from a small bugfix in the “replace a symlink atomically” feature. I’d call it stable.
@bcmills has added a version of renameio to src/cmd/go
, so even the Go tool itself profits from this functionality. Historically, “we needed it for our toolchain” determined what went into the standard library, IIUC. I’m not saying we need to apply the same principle right now, but it certainly seems like another point in favor of the proposal.
With all of these points in favor, can we please move this proposal into the review process again and decide whether we want to move forward with having (a variant of?) renameio in the standard library?
Thank you,
FWIW, I rolled an in-app implementation (for Windows, MacOS, Linux) of atomic create-file, replace-file, and append-to-file. I include a recovery step which runs on startup to complete viable unfinished transactions, and clean up temp files for non-viable ones.
I didn't even consider trying third party libraries, but I would have tried stdlib APIs. I might not have kept using them tho. Doing this correctly requires power-off-crash testing on all platforms (which I plan to do for my app) because ppl believe "atomic" means crash-proof.
For the create-file case, I first write targetless symlinks as placeholders, making it a variation of replace-file. @bcmills has been inadvertently trying to put a stop to this in #39183 (kidding :-)
The append-to-file case rewrites indexes at the end of the file, so it's not purely an append. I didn't devise an API for this case, because nothing leapt out as clearly correct, and I didn't have time to make a study of it.
Re os.Rename(), I make the assumption that it won't completely delete the file in a crash scenario; that you'll end up with the original name, the new one, or both.
See https://go.dev/cl/495800 for an example of a program that went through the rename(2)
, flock(2)
, "short writes are atomic" evolutionary process.
See also https://cs.opensource.google/go/x/tools/+/master:gopls/internal/filecache/filecache.go;l=242;drc=f872b3d6f05822d290bc7bdd29db090fd9d89f5c for a description of the design we settled on for a simple file-based atomic key/value store.
I propose adding a package to atomically create or replace a file to the golang.org/x/ namespace.
Moving the required logic into a Go package slightly reduces code length and duplication across programs, but more importantly documents the intricacies of this pattern in a central place.
An implementation of such a package can be found at https://go-review.googlesource.com/c/exp/+/72379