golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
124.07k stars 17.68k forks source link

proposal: os: add a package to atomically create or replace a file #22397

Open stapelberg opened 7 years ago

stapelberg commented 7 years ago

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

rasky commented 7 years ago

There are a few implementations in the wild, I think they should be analyzed and mentioned in the proposal.

ianlancetaylor commented 7 years ago

Yes, this seems like something that should start as an external package. https://golang.org/doc/faq#x_in_std

ianlancetaylor commented 7 years ago

But perhaps we could add something to golang.org/x/tools or golang.org/x/sync.

stapelberg commented 6 years ago

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?

rasky commented 6 years ago

@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.

bradfitz commented 6 years ago

Sounds like we're still waiting on @stapelberg here.

stapelberg commented 6 years ago

Yes. I’ll follow up with the requested write-up as soon as I find a moment of time for it.

stapelberg commented 6 years ago

Problem statement

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:

Existing options

There are 4 notable libraries on GitHub:

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.

Proposal

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

rsc commented 6 years ago

/cc @aclements @bradfitz @bcmills

rsc commented 6 years ago

See also #17869.

bcmills commented 6 years ago

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?

stapelberg commented 6 years ago

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.

pam4 commented 6 years ago

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:

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.

bcmills commented 6 years ago

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?)

pam4 commented 6 years ago

A few more considerations:

stapelberg commented 6 years ago

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.

rsc commented 6 years ago

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?

stapelberg commented 6 years ago

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.

ianlancetaylor commented 6 years ago

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.

stapelberg commented 6 years ago

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/).

ianlancetaylor commented 6 years ago

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/.

stapelberg commented 6 years ago

Okay. Should we leave this issue open, or should I open a new one once the package has stabilized?

ianlancetaylor commented 6 years ago

I'll put the proposal on hold for now.

stapelberg commented 6 years ago

FYI: the package is now published at https://github.com/google/go-write

gopherbot commented 6 years ago

Change https://golang.org/cl/146377 mentions this issue: cmd/go/internal/renameio: add package

bcmills commented 5 years ago

See previously #8868 and #17869.

bcmills commented 5 years ago

It turns out that os.Rename is not currently atomic on Windows (see #32188), so an atomic-replace package would be useful there too.

bcmills commented 5 years ago

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.)

tv42 commented 5 years ago

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.

stapelberg commented 5 years ago

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.

tv42 commented 5 years ago

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.

stapelberg commented 4 years ago

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:

  1. 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.

  2. 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.

  3. @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,

networkimprov commented 4 years ago

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.

adonovan commented 1 year ago

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.