Author(s): [Adrien Delorme]
Last updated: 2019-10-15
Discussion at https://golang.org/issue/33974.
Move already existing code residing in golang/go/src/cmd/go/internal/lockedfile
to x/sync
.
A few open source Go projects are implementing file locking mechanisms but they do not seem to be maintained anymore:
lockedfile
package API is more ergonomic. Incompatibilities with AIX, Solaris and Illumos are preventing file locking on both projects, but it looks like the go team is addressing for lockedfile
.As a result some major projects are doing their own version of it; ex: terraform, boltdb. After some researches it seemed to us that the already existing and maintained lockedfile package is the best ‘open source’ version.
File-locking interacts pretty deeply with the os
package and the system call library in x/sys
, so it makes sense for (a subset of) the same owners to consider the evolution of those packages together. We think it would benefit the mass to make such a package public: since it's already being part of the go code and therefore being maintained; it should be made public.
We propose to copy the golang/go/src/cmd/go/internal/lockedfile to x/exp
. To make it public. Not changing any of the named types for now.
Exported names and comments as can be currently found in 07b4abd:
// Package lockedfile creates and manipulates files whose contents should only // change atomically. package lockedfile // A File is a locked *os.File. // // Closing the file releases the lock. // // If the program exits while a file is locked, the operating system releases // the lock but may not do so promptly: callers must ensure that all locked // files are closed before exiting. type File struct { // contains unexported fields } // Create is like os.Create, but returns a write-locked file. // If the file already exists, it is truncated. func Create(name string) (*File, error) // Edit creates the named file with mode 0666 (before umask), // but does not truncate existing contents. // // If Edit succeeds, methods on the returned File can be used for I/O. // The associated file descriptor has mode O_RDWR and the file is write-locked. func Edit(name string) (*File, error) // Transform invokes t with the result of reading the named file, with its lock // still held. // // If t returns a nil error, Transform then writes the returned contents back to // the file, making a best effort to preserve existing contents on error. // // t must not modify the slice passed to it. func Transform(name string, t func([]byte) ([]byte, error)) (err error) // Open is like os.Open, but returns a read-locked file. func Open(name string) (*File, error) // OpenFile is like os.OpenFile, but returns a locked file. // If flag implies write access (ie: os.O_TRUNC, os.O_WRONLY or os.O_RDWR), the // file is write-locked; otherwise, it is read-locked. func OpenFile(name string, flag int, perm os.FileMode) (*File, error) // Read reads up to len(b) bytes from the File. // It returns the number of bytes read and any error encountered. // At end of file, Read returns 0, io.EOF. // // File can be read-locked or write-locked. func (f *File) Read(b []byte) (n int, err error) // ReadAt reads len(b) bytes from the File starting at byte offset off. // It returns the number of bytes read and the error, if any. // ReadAt always returns a non-nil error when n < len(b). // At end of file, that error is io.EOF. // // File can be read-locked or write-locked. func (f *File) ReadAt(b []byte, off int64) (n int, err error) // Write writes len(b) bytes to the File. // It returns the number of bytes written and an error, if any. // Write returns a non-nil error when n != len(b). // // If File is not write-locked Write returns an error. func (f *File) Write(b []byte) (n int, err error) // WriteAt writes len(b) bytes to the File starting at byte offset off. // It returns the number of bytes written and an error, if any. // WriteAt returns a non-nil error when n != len(b). // // If file was opened with the O_APPEND flag, WriteAt returns an error. // // If File is not write-locked WriteAt returns an error. func (f *File) WriteAt(b []byte, off int64) (n int, err error) // Close unlocks and closes the underlying file. // // Close may be called multiple times; all calls after the first will return a // non-nil error. func (f *File) Close() error // A Mutex provides mutual exclusion within and across processes by locking a // well-known file. Such a file generally guards some other part of the // filesystem: for example, a Mutex file in a directory might guard access to // the entire tree rooted in that directory. // // Mutex does not implement sync.Locker: unlike a sync.Mutex, a lockedfile.Mutex // can fail to lock (e.g. if there is a permission error in the filesystem). // // Like a sync.Mutex, a Mutex may be included as a field of a larger struct but // must not be copied after first use. The Path field must be set before first // use and must not be change thereafter. type Mutex struct { // Path to the well-known lock file. Must be non-empty. // // Path must not change on a locked mutex. Path string // contains filtered or unexported fields } // MutexAt returns a new Mutex with Path set to the given non-empty path. func MutexAt(path string) *Mutex // Lock attempts to lock the Mutex. // // If successful, Lock returns a non-nil unlock function: it is provided as a // return-value instead of a separate method to remind the caller to check the // accompanying error. (See https://golang.org/issue/20803.) func (mu *Mutex) Lock() (unlock func(), err error) // String returns a string containing the path of the mutex. func (mu *Mutex) String() string
The lockedfile.File
implements a subset of the os.File
but with file locking protection.
The lockedfile.Mutex
does not implement sync.Locker
: unlike a sync.Mutex
, a lockedfile.Mutex
can fail to lock (e.g. if there is a permission error in the filesystem).
lockedfile
adds an Edit
and a Transform
function; Edit
is not currently part of the file
package. Edit exists to make it easier to implement locked read-modify-write operation. Transform
simplifies the act of reading and then writing to a locked file.
Making this package public will make it more used. A tiny surge of issues might come in the beginning; at the benefits of everyone. (Unless it's bug free !!).
There exists a https://godoc.org/github.com/rogpeppe/go-internal package that exports a lot of internal packages from the go repo. But if go-internal became wildly popular; in order to have a bug fixed or a feature introduced in; a user would still need to open a PR on the go repo; then the author of go-internal would need to update the package.
There are no retro-compatibility issues since this will be a code addition but ideally we don‘t want to maintain two copies of this package going forward, and we probably don’t want to vendor x/exp
into the cmd
module.
Perhaps that implies that this should go in the x/sys
or x/sync
repo instead?
Adrien Delorme plans to do copy the exported types in the proposal section from cmd/go/internal/lockedfile
to x/sync
.
Adrien Delorme plans to change the references to the lockedfile
package in cmd
.