|
| 1 | +# Proposal: make the internal [lockedfile](https://godoc.org/github.com/golang/go/src/cmd/go/internal/lockedfile/) package public |
| 2 | + |
| 3 | +Author(s): [Adrien Delorme] |
| 4 | + |
| 5 | +Last updated: 2019-10-15 |
| 6 | + |
| 7 | +Discussion at https://golang.org/issue/33974. |
| 8 | + |
| 9 | +## Abstract |
| 10 | + |
| 11 | +Move already existing code residing in |
| 12 | +`golang/go/src/cmd/go/internal/lockedfile` to `x/sync`. |
| 13 | + |
| 14 | +## Background |
| 15 | + |
| 16 | +A few open source Go projects are implementing file locking mechanisms but they |
| 17 | +do not seem to be maintained anymore: |
| 18 | +* https://github.com/gofrs/flock : This repo has lastly accepted PRs in March |
| 19 | + 2019, so this implementation may be maintained and we could argue that the |
| 20 | + `lockedfile` package API is more ergonomic. Incompatibilities with AIX, |
| 21 | + Solaris and Illumos are preventing file locking on both projects, but it |
| 22 | + looks like the go team is addressing for `lockedfile`. |
| 23 | +* https://github.com/juju/fslock : Note that this implementation is both |
| 24 | + unmaintained and LGPL-licensed, so even folks who would like to use it might |
| 25 | + not be able to. Also not that this repo [was selected for removal in |
| 26 | + 2017](https://github.com/juju/fslock/issues/4) |
| 27 | + |
| 28 | + |
| 29 | +As a result some major projects are doing |
| 30 | +their own version of it; ex: |
| 31 | +[terraform](https://github.com/hashicorp/terraform/blob/1ff9a540202b8c36e33db950374bbb4495737d8f/states/statemgr/filesystem_lock_unix.go), |
| 32 | +[boltdb](https://github.com/boltdb/bolt/search?q=flock&unscoped_q=flock). After |
| 33 | +some researches it seemed to us that the already existing and maintained |
| 34 | +[lockedfile](https://godoc.org/github.com/golang/go/src/cmd/go/internal/lockedfile/) |
| 35 | +package is the best 'open source' version. |
| 36 | + |
| 37 | +File-locking interacts pretty deeply with the `os` package and the system call |
| 38 | +library in `x/sys`, so it makes sense for (a subset of) the same owners to |
| 39 | +consider the evolution of those packages together. |
| 40 | +We think it would benefit the mass to make such a package public: since it's |
| 41 | +already being part of the go code and therefore being maintained; it should be |
| 42 | +made public. |
| 43 | + |
| 44 | +## Proposal |
| 45 | + |
| 46 | +We propose to copy the golang/go/src/cmd/go/internal/lockedfile to `x/exp`. To |
| 47 | +make it public. Not changing any of the named types for now. |
| 48 | + |
| 49 | +Exported names and comments as can be currently found in |
| 50 | +[07b4abd](https://github.com/golang/go/tree/07b4abd62e450f19c47266b3a526df49c01ba425/src/cmd/go/internal/lockedfile): |
| 51 | + |
| 52 | +``` |
| 53 | +// Package lockedfile creates and manipulates files whose contents should only |
| 54 | +// change atomically. |
| 55 | +package lockedfile |
| 56 | +
|
| 57 | +// A File is a locked *os.File. |
| 58 | +// |
| 59 | +// Closing the file releases the lock. |
| 60 | +// |
| 61 | +// If the program exits while a file is locked, the operating system releases |
| 62 | +// the lock but may not do so promptly: callers must ensure that all locked |
| 63 | +// files are closed before exiting. |
| 64 | +type File struct { |
| 65 | + // contains unexported fields |
| 66 | +} |
| 67 | +
|
| 68 | +// Create is like os.Create, but returns a write-locked file. |
| 69 | +// If the file already exists, it is truncated. |
| 70 | +func Create(name string) (*File, error) |
| 71 | +
|
| 72 | +// Edit creates the named file with mode 0666 (before umask), |
| 73 | +// but does not truncate existing contents. |
| 74 | +// |
| 75 | +// If Edit succeeds, methods on the returned File can be used for I/O. |
| 76 | +// The associated file descriptor has mode O_RDWR and the file is write-locked. |
| 77 | +func Edit(name string) (*File, error) |
| 78 | +
|
| 79 | +// Transform invokes t with the result of reading the named file, with its lock |
| 80 | +// still held. |
| 81 | +// |
| 82 | +// If t returns a nil error, Transform then writes the returned contents back to |
| 83 | +// the file, making a best effort to preserve existing contents on error. |
| 84 | +// |
| 85 | +// t must not modify the slice passed to it. |
| 86 | +func Transform(name string, t func([]byte) ([]byte, error)) (err error) |
| 87 | +
|
| 88 | +// Open is like os.Open, but returns a read-locked file. |
| 89 | +func Open(name string) (*File, error) |
| 90 | +
|
| 91 | +// OpenFile is like os.OpenFile, but returns a locked file. |
| 92 | +// If flag implies write access (ie: os.O_TRUNC, os.O_WRONLY or os.O_RDWR), the |
| 93 | +// file is write-locked; otherwise, it is read-locked. |
| 94 | +func OpenFile(name string, flag int, perm os.FileMode) (*File, error) |
| 95 | +
|
| 96 | +// Read reads up to len(b) bytes from the File. |
| 97 | +// It returns the number of bytes read and any error encountered. |
| 98 | +// At end of file, Read returns 0, io.EOF. |
| 99 | +// |
| 100 | +// File can be read-locked or write-locked. |
| 101 | +func (f *File) Read(b []byte) (n int, err error) |
| 102 | +
|
| 103 | +// ReadAt reads len(b) bytes from the File starting at byte offset off. |
| 104 | +// It returns the number of bytes read and the error, if any. |
| 105 | +// ReadAt always returns a non-nil error when n < len(b). |
| 106 | +// At end of file, that error is io.EOF. |
| 107 | +// |
| 108 | +// File can be read-locked or write-locked. |
| 109 | +func (f *File) ReadAt(b []byte, off int64) (n int, err error) |
| 110 | +
|
| 111 | +// Write writes len(b) bytes to the File. |
| 112 | +// It returns the number of bytes written and an error, if any. |
| 113 | +// Write returns a non-nil error when n != len(b). |
| 114 | +// |
| 115 | +// If File is not write-locked Write returns an error. |
| 116 | +func (f *File) Write(b []byte) (n int, err error) |
| 117 | +
|
| 118 | +// WriteAt writes len(b) bytes to the File starting at byte offset off. |
| 119 | +// It returns the number of bytes written and an error, if any. |
| 120 | +// WriteAt returns a non-nil error when n != len(b). |
| 121 | +// |
| 122 | +// If file was opened with the O_APPEND flag, WriteAt returns an error. |
| 123 | +// |
| 124 | +// If File is not write-locked WriteAt returns an error. |
| 125 | +func (f *File) WriteAt(b []byte, off int64) (n int, err error) |
| 126 | +
|
| 127 | +// Close unlocks and closes the underlying file. |
| 128 | +// |
| 129 | +// Close may be called multiple times; all calls after the first will return a |
| 130 | +// non-nil error. |
| 131 | +func (f *File) Close() error |
| 132 | +
|
| 133 | +// A Mutex provides mutual exclusion within and across processes by locking a |
| 134 | +// well-known file. Such a file generally guards some other part of the |
| 135 | +// filesystem: for example, a Mutex file in a directory might guard access to |
| 136 | +// the entire tree rooted in that directory. |
| 137 | +// |
| 138 | +// Mutex does not implement sync.Locker: unlike a sync.Mutex, a lockedfile.Mutex |
| 139 | +// can fail to lock (e.g. if there is a permission error in the filesystem). |
| 140 | +// |
| 141 | +// Like a sync.Mutex, a Mutex may be included as a field of a larger struct but |
| 142 | +// must not be copied after first use. The Path field must be set before first |
| 143 | +// use and must not be change thereafter. |
| 144 | +type Mutex struct { |
| 145 | + // Path to the well-known lock file. Must be non-empty. |
| 146 | + // |
| 147 | + // Path must not change on a locked mutex. |
| 148 | + Path string |
| 149 | + // contains filtered or unexported fields |
| 150 | +} |
| 151 | +
|
| 152 | +// MutexAt returns a new Mutex with Path set to the given non-empty path. |
| 153 | +func MutexAt(path string) *Mutex |
| 154 | +
|
| 155 | +// Lock attempts to lock the Mutex. |
| 156 | +// |
| 157 | +// If successful, Lock returns a non-nil unlock function: it is provided as a |
| 158 | +// return-value instead of a separate method to remind the caller to check the |
| 159 | +// accompanying error. (See https://golang.org/issue/20803.) |
| 160 | +func (mu *Mutex) Lock() (unlock func(), err error) |
| 161 | +
|
| 162 | +// String returns a string containing the path of the mutex. |
| 163 | +func (mu *Mutex) String() string |
| 164 | +``` |
| 165 | + |
| 166 | +## Rationale |
| 167 | + |
| 168 | +* The `lockedfile.File` implements a subset of the `os.File` but with file |
| 169 | + locking protection. |
| 170 | + |
| 171 | +* The `lockedfile.Mutex` does not implement `sync.Locker`: unlike a |
| 172 | + `sync.Mutex`, a `lockedfile.Mutex` can fail to lock (e.g. if there is a |
| 173 | + permission error in the filesystem). |
| 174 | + |
| 175 | +* `lockedfile` adds an `Edit` and a `Transform` function; `Edit` is not |
| 176 | + currently part of the `file` package. Edit exists to make it easier to |
| 177 | + implement locked read-modify-write operation. `Transform` simplifies the act |
| 178 | + of reading and then writing to a locked file. |
| 179 | + |
| 180 | + |
| 181 | +* Making this package public will make it more used. A tiny surge of issues |
| 182 | + might come in the beginning; at the benefits of everyone. (Unless it's bug |
| 183 | + free !!). |
| 184 | + |
| 185 | +* There exists a https://godoc.org/github.com/rogpeppe/go-internal package that |
| 186 | + exports a lot of internal packages from the go repo. But if go-internal |
| 187 | + became wildly popular; in order to have a bug fixed or a feature introduced |
| 188 | + in; a user would still need to open a PR on the go repo; then the author of |
| 189 | + go-internal would need to update the package. |
| 190 | + |
| 191 | +## Compatibility |
| 192 | + |
| 193 | +There are no retro-compatibility issues since this will be a code addition but |
| 194 | +ideally we don't want to maintain two copies of this package going forward, and |
| 195 | +we probably don't want to vendor `x/exp` into the `cmd` module. |
| 196 | + |
| 197 | + |
| 198 | + |
| 199 | +Perhaps that implies that this should go in the `x/sys` or `x/sync` repo instead? |
| 200 | + |
| 201 | +## Implementation |
| 202 | + |
| 203 | +Adrien Delorme plans to do copy the exported types in the proposal section from |
| 204 | + `cmd/go/internal/lockedfile` to `x/sync`. |
| 205 | + |
| 206 | +Adrien Delorme plans to change the references to the `lockedfile` package in |
| 207 | +`cmd`. |
0 commit comments