-
Notifications
You must be signed in to change notification settings - Fork 17.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
proposal: os: add a package to atomically create or replace a file #22397
Comments
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. |
@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.
|
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. |
Problem statementAtomically 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
Existing optionsThere are 4 notable libraries on GitHub:
There are a number of other ad-hoc implementations in applications (as opposed to libraries): #22397 (comment) 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. ProposalI 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 |
See also #17869. |
What should happen if |
An error will occur, and the user must set 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 |
I think that As the caller I can think of two cases:
In the second case I would prefer to be able to pass it, rather then change A minor note about your prototype: in the commented out example you use |
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 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:
|
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:
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? |
Definitely.
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?
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 |
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?
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? Lines 222 to 241 in 6fe7b43
On Windows, it seems like we should use |
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: |
renameio.WriteFile writes files atomically by renaming temporary files. See the subsequent changes for usage examples. Updates #26794 Updates #22397 Change-Id: I4bfe3125a53f58060587f98afbb4260bb1cc3d32 Reviewed-on: https://go-review.googlesource.com/c/146377 Run-TryBot: Bryan C. Mills <bcmills@google.com> Reviewed-by: Giovanni Bajo <rasky@develer.com> Reviewed-by: Russ Cox <rsc@golang.org>
It turns out that |
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 (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. |
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:
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 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. |
- After stumbling upon golang/go#22397 and reading the implementations I realized that Forgejo code doesn't have `Sync()` and it doesn't properly error handle the `Close` function. - (likely) Resolves https://codeberg.org/forgejo/forgejo/issues/1446 (cherry picked from commit 0efcb334c2f123d0869a30d684189eb31e8b983f) (cherry picked from commit 04ef02c0dd98c7437acb39383d311c0901366508) (cherry picked from commit 85f2065c9bc6ded9c21909ec76a9e8fc2d22f462) (cherry picked from commit 8d36b5cce66864e190bad3c9b0973e37ca774a22) (cherry picked from commit 378dc30fb5a88ffe185c54de7e69224289038bff) (cherry picked from commit 2b28bf826e51b8ccb4a693001c03ffe6132f7842) (cherry picked from commit d0625a001e5f8fe202865bec7aadcf0c551d556d) (cherry picked from commit f161a4f60f1cde80a41bece4929836257b9e0423) (cherry picked from commit 7430ca43e57683ca324fb20269a60e05cb393589) (cherry picked from commit ab6d38daf7eeb1dc993bfc0ac1a326af65128168) (cherry picked from commit 0f703fd02e69bdcf2f77e120ff8641f1b8089020) (cherry picked from commit 6931a8f)
- After stumbling upon golang/go#22397 and reading the implementations I realized that Forgejo code doesn't have `Sync()` and it doesn't properly error handle the `Close` function. - (likely) Resolves https://codeberg.org/forgejo/forgejo/issues/1446 (cherry picked from commit 0efcb334c2f123d0869a30d684189eb31e8b983f) (cherry picked from commit 04ef02c0dd98c7437acb39383d311c0901366508) (cherry picked from commit 85f2065c9bc6ded9c21909ec76a9e8fc2d22f462) (cherry picked from commit 8d36b5cce66864e190bad3c9b0973e37ca774a22) (cherry picked from commit 378dc30fb5a88ffe185c54de7e69224289038bff) (cherry picked from commit 2b28bf826e51b8ccb4a693001c03ffe6132f7842) (cherry picked from commit d0625a001e5f8fe202865bec7aadcf0c551d556d) (cherry picked from commit f161a4f60f1cde80a41bece4929836257b9e0423) (cherry picked from commit 7430ca43e57683ca324fb20269a60e05cb393589) (cherry picked from commit ab6d38daf7eeb1dc993bfc0ac1a326af65128168) (cherry picked from commit 0f703fd02e69bdcf2f77e120ff8641f1b8089020) (cherry picked from commit 6931a8f) (cherry picked from commit 5e2065c) (cherry picked from commit 38c812a) (cherry picked from commit 494874e) (cherry picked from commit d396b7f)
- After stumbling upon golang/go#22397 and reading the implementations I realized that Forgejo code doesn't have `Sync()` and it doesn't properly error handle the `Close` function. - (likely) Resolves https://codeberg.org/forgejo/forgejo/issues/1446 (cherry picked from commit 0efcb334c2f123d0869a30d684189eb31e8b983f) (cherry picked from commit 04ef02c0dd98c7437acb39383d311c0901366508) (cherry picked from commit 85f2065c9bc6ded9c21909ec76a9e8fc2d22f462) (cherry picked from commit 8d36b5cce66864e190bad3c9b0973e37ca774a22) (cherry picked from commit 378dc30fb5a88ffe185c54de7e69224289038bff) (cherry picked from commit 2b28bf826e51b8ccb4a693001c03ffe6132f7842) (cherry picked from commit d0625a001e5f8fe202865bec7aadcf0c551d556d) (cherry picked from commit f161a4f60f1cde80a41bece4929836257b9e0423) (cherry picked from commit 7430ca43e57683ca324fb20269a60e05cb393589) (cherry picked from commit ab6d38daf7eeb1dc993bfc0ac1a326af65128168) (cherry picked from commit 0f703fd02e69bdcf2f77e120ff8641f1b8089020) (cherry picked from commit 6931a8f) (cherry picked from commit 5e2065c) (cherry picked from commit 38c812a) (cherry picked from commit 494874e) (cherry picked from commit d396b7f) (cherry picked from commit 7babc6e)
- After stumbling upon golang/go#22397 and reading the implementations I realized that Forgejo code doesn't have `Sync()` and it doesn't properly error handle the `Close` function. - (likely) Resolves https://codeberg.org/forgejo/forgejo/issues/1446 (cherry picked from commit 0efcb334c2f123d0869a30d684189eb31e8b983f) (cherry picked from commit 04ef02c0dd98c7437acb39383d311c0901366508) (cherry picked from commit 85f2065c9bc6ded9c21909ec76a9e8fc2d22f462) (cherry picked from commit 8d36b5cce66864e190bad3c9b0973e37ca774a22) (cherry picked from commit 378dc30fb5a88ffe185c54de7e69224289038bff) (cherry picked from commit 2b28bf826e51b8ccb4a693001c03ffe6132f7842) (cherry picked from commit d0625a001e5f8fe202865bec7aadcf0c551d556d) (cherry picked from commit f161a4f60f1cde80a41bece4929836257b9e0423) (cherry picked from commit 7430ca43e57683ca324fb20269a60e05cb393589) (cherry picked from commit ab6d38daf7eeb1dc993bfc0ac1a326af65128168) (cherry picked from commit 0f703fd02e69bdcf2f77e120ff8641f1b8089020) (cherry picked from commit 6931a8f6bbfa0fe4f68b462f88c4a3db7ea06306) (cherry picked from commit 5e2065c) (cherry picked from commit 38c812acfffe4c83099881a8b47489caba64b42a) (cherry picked from commit 494874e23f2edb90beabe0827dadefa035e35a71) (cherry picked from commit d396b7fd47c20d08844f8de1255a32fe0de25249) (cherry picked from commit 7babc6efe11d0950ac47d3d4724bb5d414bdd6aa) (cherry picked from commit 2d4dbbe7418d6c1a3a47c7e65350cf65fc9dca19)
- After stumbling upon golang/go#22397 and reading the implementations I realized that Forgejo code doesn't have `Sync()` and it doesn't properly error handle the `Close` function. - (likely) Resolves https://codeberg.org/forgejo/forgejo/issues/1446 (cherry picked from commit 0efcb33) (cherry picked from commit 04ef02c) (cherry picked from commit 85f2065) (cherry picked from commit 8d36b5c) (cherry picked from commit 0f406dc) (cherry picked from commit 347a2e7) (cherry picked from commit f6c04d6) (cherry picked from commit cf8b64f)
Atomically replacing a file is a subtle thing to do. Use renameio to implement this, which correctly handles: - Setting permissions - Removing the temporary file on failure - Flushing writes before renaming the file It's not possible to atomically replace a file on Windows (golang/go#22397 (comment)) but we only need the atomicity guarantees on our production platforms (Linux and FreeBSD). renameio is a simple and stable library. I have reviewed the relevant code path and it handles edge cases better than the trivial writeFileAtomic function.
Atomically replacing a file is a subtle thing to do. Use renameio to implement this, which correctly handles: - Setting permissions - Removing the temporary file on failure - Flushing writes before renaming the file It's not possible to atomically replace a file on Windows (golang/go#22397 (comment)) but we only need the atomicity guarantees on our production platforms (Linux and FreeBSD). renameio is a simple and stable library. I have reviewed the relevant code path and it handles edge cases better than the trivial writeFileAtomic function.
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
The text was updated successfully, but these errors were encountered: