gopls/internal/lsp/filecache: use file locking, not rename

After various dismal experiments, I'm not optimistic that
Windows provides any means to rename a file atomically.

This change causes filecache to use file locking, on
all platforms, to ensure that cache entries are updated
atomically.

The implementation of locking was plundered wholesale from
$GOROOT/src/cmd/go/internal/lockedfile, modified only as
needed to get it to compile outside GOROOT.

The change causes the (new) BenchmarkUncontendedGet to
increase to 32us, up from 16us, on macOS. Probably still
good enough, and preferable to two different implementations.

Fixes golang/go#57747

Change-Id: Icd373e99481789056d70c02cbcd554bd59eb3101
Reviewed-on: https://go-review.googlesource.com/c/tools/+/461800
TryBot-Result: Gopher Robot <gobot@golang.org>
Auto-Submit: Alan Donovan <adonovan@google.com>
Reviewed-by: Bryan Mills <bcmills@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
Run-TryBot: Alan Donovan <adonovan@google.com>
Reviewed-by: Robert Findley <rfindley@google.com>
This commit is contained in:
Alan Donovan 2023-01-12 15:26:01 -05:00 коммит произвёл Gopher Robot
Родитель 561a9be679
Коммит bb7c440efb
15 изменённых файлов: 1554 добавлений и 37 удалений

Просмотреть файл

@ -18,12 +18,13 @@
package filecache
import (
"bytes"
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
"io"
"log"
"math/rand"
"os"
"path/filepath"
"sort"
@ -31,7 +32,7 @@ import (
"sync/atomic"
"time"
"golang.org/x/tools/internal/robustio"
"golang.org/x/tools/internal/lockedfile"
)
// Get retrieves from the cache and returns a newly allocated
@ -40,7 +41,7 @@ import (
// Get returns ErrNotFound if the value was not found.
func Get(kind string, key [32]byte) ([]byte, error) {
name := filename(kind, key)
data, err := robustio.ReadFile(name)
data, err := lockedfile.Read(name)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, ErrNotFound
@ -48,6 +49,16 @@ func Get(kind string, key [32]byte) ([]byte, error) {
return nil, err
}
// Verify that the Write was complete
// by checking the recorded length.
if len(data) < 8 {
return nil, ErrNotFound // cache entry is incomplete
}
if length := binary.LittleEndian.Uint64(data); int(length) != len(data)-8 {
return nil, ErrNotFound // cache entry is incomplete (or too long!)
}
data = data[8:]
// Update file time for use by LRU eviction.
// (This turns every read into a write operation.
// If this is a performance problem, we should
@ -76,41 +87,21 @@ func Set(kind string, key [32]byte, value []byte) error {
return err
}
// The sequence below uses rename to achieve atomic cache
// updates even with concurrent processes.
var cause error
for try := 0; try < 3; try++ {
tmpname := fmt.Sprintf("%s.tmp.%d", name, rand.Int())
tmp, err := os.OpenFile(tmpname, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
if err != nil {
if os.IsExist(err) {
// Create raced with another thread (or stale file).
// Try again.
cause = err
continue
}
return err
}
// In the unlikely event of a short write (e.g. ENOSPC)
// followed by process termination (e.g. a power cut), we
// don't want a reader to see a short file, so we record
// the expected length first and verify it in Get.
var length [8]byte
binary.LittleEndian.PutUint64(length[:], uint64(len(value)))
header := bytes.NewReader(length[:])
payload := bytes.NewReader(value)
_, err = tmp.Write(value)
if closeErr := tmp.Close(); err == nil {
err = closeErr // prefer error from write over close
}
if err != nil {
os.Remove(tmp.Name()) // ignore error
return err
}
err = robustio.Rename(tmp.Name(), name)
if err == nil {
return nil // success
}
cause = err
// Rename raced with another thread. Try again.
os.Remove(tmp.Name()) // ignore error
}
return cause
// Windows doesn't support atomic rename--we tried MoveFile,
// MoveFileEx, ReplaceFileEx, and SetFileInformationByHandle
// of RenameFileInfo, all to no avail--so instead we use
// advisory file locking, which is only about 2x slower even
// on POSIX platforms with atomic rename.
return lockedfile.Write(name, io.MultiReader(header, payload), 0600)
}
var budget int64 = 1e9 // 1GB

Просмотреть файл

@ -187,3 +187,29 @@ func uniqueKey() (key [32]byte) {
}
return
}
func BenchmarkUncontendedGet(b *testing.B) {
const kind = "BenchmarkUncontendedGet"
key := uniqueKey()
var value [8192]byte
if _, err := mathrand.Read(value[:]); err != nil {
b.Fatalf("rand: %v", err)
}
if err := filecache.Set(kind, key, value[:]); err != nil {
b.Fatal(err)
}
b.ResetTimer()
var group errgroup.Group
group.SetLimit(50)
for i := 0; i < b.N; i++ {
group.Go(func() error {
_, err := filecache.Get(kind, key)
return err
})
}
if err := group.Wait(); err != nil {
b.Fatal(err)
}
}

Просмотреть файл

@ -0,0 +1,99 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package filelock provides a platform-independent API for advisory file
// locking. Calls to functions in this package on platforms that do not support
// advisory locks will return errors for which IsNotSupported returns true.
package filelock
import (
"errors"
"io/fs"
"os"
)
// A File provides the minimal set of methods required to lock an open file.
// File implementations must be usable as map keys.
// The usual implementation is *os.File.
type File interface {
// Name returns the name of the file.
Name() string
// Fd returns a valid file descriptor.
// (If the File is an *os.File, it must not be closed.)
Fd() uintptr
// Stat returns the FileInfo structure describing file.
Stat() (fs.FileInfo, error)
}
// Lock places an advisory write lock on the file, blocking until it can be
// locked.
//
// If Lock returns nil, no other process will be able to place a read or write
// lock on the file until this process exits, closes f, or calls Unlock on it.
//
// If f's descriptor is already read- or write-locked, the behavior of Lock is
// unspecified.
//
// Closing the file may or may not release the lock promptly. Callers should
// ensure that Unlock is always called when Lock succeeds.
func Lock(f File) error {
return lock(f, writeLock)
}
// RLock places an advisory read lock on the file, blocking until it can be locked.
//
// If RLock returns nil, no other process will be able to place a write lock on
// the file until this process exits, closes f, or calls Unlock on it.
//
// If f is already read- or write-locked, the behavior of RLock is unspecified.
//
// Closing the file may or may not release the lock promptly. Callers should
// ensure that Unlock is always called if RLock succeeds.
func RLock(f File) error {
return lock(f, readLock)
}
// Unlock removes an advisory lock placed on f by this process.
//
// The caller must not attempt to unlock a file that is not locked.
func Unlock(f File) error {
return unlock(f)
}
// String returns the name of the function corresponding to lt
// (Lock, RLock, or Unlock).
func (lt lockType) String() string {
switch lt {
case readLock:
return "RLock"
case writeLock:
return "Lock"
default:
return "Unlock"
}
}
// IsNotSupported returns a boolean indicating whether the error is known to
// report that a function is not supported (possibly for a specific input).
// It is satisfied by ErrNotSupported as well as some syscall errors.
func IsNotSupported(err error) bool {
return isNotSupported(underlyingError(err))
}
var ErrNotSupported = errors.New("operation not supported")
// underlyingError returns the underlying error for known os error types.
func underlyingError(err error) error {
switch err := err.(type) {
case *fs.PathError:
return err.Err
case *os.LinkError:
return err.Err
case *os.SyscallError:
return err.Err
}
return err
}

Просмотреть файл

@ -0,0 +1,215 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build aix || (solaris && !illumos)
// +build aix solaris,!illumos
// This code implements the filelock API using POSIX 'fcntl' locks, which attach
// to an (inode, process) pair rather than a file descriptor. To avoid unlocking
// files prematurely when the same file is opened through different descriptors,
// we allow only one read-lock at a time.
//
// Most platforms provide some alternative API, such as an 'flock' system call
// or an F_OFD_SETLK command for 'fcntl', that allows for better concurrency and
// does not require per-inode bookkeeping in the application.
package filelock
import (
"errors"
"io"
"io/fs"
"math/rand"
"sync"
"syscall"
"time"
)
type lockType int16
const (
readLock lockType = syscall.F_RDLCK
writeLock lockType = syscall.F_WRLCK
)
type inode = uint64 // type of syscall.Stat_t.Ino
type inodeLock struct {
owner File
queue []<-chan File
}
var (
mu sync.Mutex
inodes = map[File]inode{}
locks = map[inode]inodeLock{}
)
func lock(f File, lt lockType) (err error) {
// POSIX locks apply per inode and process, and the lock for an inode is
// released when *any* descriptor for that inode is closed. So we need to
// synchronize access to each inode internally, and must serialize lock and
// unlock calls that refer to the same inode through different descriptors.
fi, err := f.Stat()
if err != nil {
return err
}
ino := fi.Sys().(*syscall.Stat_t).Ino
mu.Lock()
if i, dup := inodes[f]; dup && i != ino {
mu.Unlock()
return &fs.PathError{
Op: lt.String(),
Path: f.Name(),
Err: errors.New("inode for file changed since last Lock or RLock"),
}
}
inodes[f] = ino
var wait chan File
l := locks[ino]
if l.owner == f {
// This file already owns the lock, but the call may change its lock type.
} else if l.owner == nil {
// No owner: it's ours now.
l.owner = f
} else {
// Already owned: add a channel to wait on.
wait = make(chan File)
l.queue = append(l.queue, wait)
}
locks[ino] = l
mu.Unlock()
if wait != nil {
wait <- f
}
// Spurious EDEADLK errors arise on platforms that compute deadlock graphs at
// the process, rather than thread, level. Consider processes P and Q, with
// threads P.1, P.2, and Q.3. The following trace is NOT a deadlock, but will be
// reported as a deadlock on systems that consider only process granularity:
//
// P.1 locks file A.
// Q.3 locks file B.
// Q.3 blocks on file A.
// P.2 blocks on file B. (This is erroneously reported as a deadlock.)
// P.1 unlocks file A.
// Q.3 unblocks and locks file A.
// Q.3 unlocks files A and B.
// P.2 unblocks and locks file B.
// P.2 unlocks file B.
//
// These spurious errors were observed in practice on AIX and Solaris in
// cmd/go: see https://golang.org/issue/32817.
//
// We work around this bug by treating EDEADLK as always spurious. If there
// really is a lock-ordering bug between the interacting processes, it will
// become a livelock instead, but that's not appreciably worse than if we had
// a proper flock implementation (which generally does not even attempt to
// diagnose deadlocks).
//
// In the above example, that changes the trace to:
//
// P.1 locks file A.
// Q.3 locks file B.
// Q.3 blocks on file A.
// P.2 spuriously fails to lock file B and goes to sleep.
// P.1 unlocks file A.
// Q.3 unblocks and locks file A.
// Q.3 unlocks files A and B.
// P.2 wakes up and locks file B.
// P.2 unlocks file B.
//
// We know that the retry loop will not introduce a *spurious* livelock
// because, according to the POSIX specification, EDEADLK is only to be
// returned when “the lock is blocked by a lock from another process”.
// If that process is blocked on some lock that we are holding, then the
// resulting livelock is due to a real deadlock (and would manifest as such
// when using, for example, the flock implementation of this package).
// If the other process is *not* blocked on some other lock that we are
// holding, then it will eventually release the requested lock.
nextSleep := 1 * time.Millisecond
const maxSleep = 500 * time.Millisecond
for {
err = setlkw(f.Fd(), lt)
if err != syscall.EDEADLK {
break
}
time.Sleep(nextSleep)
nextSleep += nextSleep
if nextSleep > maxSleep {
nextSleep = maxSleep
}
// Apply 10% jitter to avoid synchronizing collisions when we finally unblock.
nextSleep += time.Duration((0.1*rand.Float64() - 0.05) * float64(nextSleep))
}
if err != nil {
unlock(f)
return &fs.PathError{
Op: lt.String(),
Path: f.Name(),
Err: err,
}
}
return nil
}
func unlock(f File) error {
var owner File
mu.Lock()
ino, ok := inodes[f]
if ok {
owner = locks[ino].owner
}
mu.Unlock()
if owner != f {
panic("unlock called on a file that is not locked")
}
err := setlkw(f.Fd(), syscall.F_UNLCK)
mu.Lock()
l := locks[ino]
if len(l.queue) == 0 {
// No waiters: remove the map entry.
delete(locks, ino)
} else {
// The first waiter is sending us their file now.
// Receive it and update the queue.
l.owner = <-l.queue[0]
l.queue = l.queue[1:]
locks[ino] = l
}
delete(inodes, f)
mu.Unlock()
return err
}
// setlkw calls FcntlFlock with F_SETLKW for the entire file indicated by fd.
func setlkw(fd uintptr, lt lockType) error {
for {
err := syscall.FcntlFlock(fd, syscall.F_SETLKW, &syscall.Flock_t{
Type: int16(lt),
Whence: io.SeekStart,
Start: 0,
Len: 0, // All bytes.
})
if err != syscall.EINTR {
return err
}
}
}
func isNotSupported(err error) bool {
return err == syscall.ENOSYS || err == syscall.ENOTSUP || err == syscall.EOPNOTSUPP || err == ErrNotSupported
}

Просмотреть файл

@ -0,0 +1,37 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !(darwin || dragonfly || freebsd || illumos || linux || netbsd || openbsd) && !plan9 && !windows
// +build !darwin,!dragonfly,!freebsd,!illumos,!linux,!netbsd,!openbsd,!plan9,!windows
package filelock
import "io/fs"
type lockType int8
const (
readLock = iota + 1
writeLock
)
func lock(f File, lt lockType) error {
return &fs.PathError{
Op: lt.String(),
Path: f.Name(),
Err: ErrNotSupported,
}
}
func unlock(f File) error {
return &fs.PathError{
Op: "Unlock",
Path: f.Name(),
Err: ErrNotSupported,
}
}
func isNotSupported(err error) bool {
return err == ErrNotSupported
}

Просмотреть файл

@ -0,0 +1,37 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build plan9
// +build plan9
package filelock
import "io/fs"
type lockType int8
const (
readLock = iota + 1
writeLock
)
func lock(f File, lt lockType) error {
return &fs.PathError{
Op: lt.String(),
Path: f.Name(),
Err: ErrNotSupported,
}
}
func unlock(f File) error {
return &fs.PathError{
Op: "Unlock",
Path: f.Name(),
Err: ErrNotSupported,
}
}
func isNotSupported(err error) bool {
return err == ErrNotSupported
}

Просмотреть файл

@ -0,0 +1,209 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !js && !plan9
// +build !js,!plan9
package filelock_test
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"testing"
"time"
"golang.org/x/tools/internal/lockedfile/internal/filelock"
)
func lock(t *testing.T, f *os.File) {
t.Helper()
err := filelock.Lock(f)
t.Logf("Lock(fd %d) = %v", f.Fd(), err)
if err != nil {
t.Fail()
}
}
func rLock(t *testing.T, f *os.File) {
t.Helper()
err := filelock.RLock(f)
t.Logf("RLock(fd %d) = %v", f.Fd(), err)
if err != nil {
t.Fail()
}
}
func unlock(t *testing.T, f *os.File) {
t.Helper()
err := filelock.Unlock(f)
t.Logf("Unlock(fd %d) = %v", f.Fd(), err)
if err != nil {
t.Fail()
}
}
func mustTempFile(t *testing.T) (f *os.File, remove func()) {
t.Helper()
base := filepath.Base(t.Name())
f, err := os.CreateTemp("", base)
if err != nil {
t.Fatalf(`os.CreateTemp("", %q) = %v`, base, err)
}
t.Logf("fd %d = %s", f.Fd(), f.Name())
return f, func() {
f.Close()
os.Remove(f.Name())
}
}
func mustOpen(t *testing.T, name string) *os.File {
t.Helper()
f, err := os.OpenFile(name, os.O_RDWR, 0)
if err != nil {
t.Fatalf("os.Open(%q) = %v", name, err)
}
t.Logf("fd %d = os.Open(%q)", f.Fd(), name)
return f
}
const (
quiescent = 10 * time.Millisecond
probablyStillBlocked = 10 * time.Second
)
func mustBlock(t *testing.T, op string, f *os.File) (wait func(*testing.T)) {
t.Helper()
desc := fmt.Sprintf("%s(fd %d)", op, f.Fd())
done := make(chan struct{})
go func() {
t.Helper()
switch op {
case "Lock":
lock(t, f)
case "RLock":
rLock(t, f)
default:
panic("invalid op: " + op)
}
close(done)
}()
select {
case <-done:
t.Fatalf("%s unexpectedly did not block", desc)
return nil
case <-time.After(quiescent):
t.Logf("%s is blocked (as expected)", desc)
return func(t *testing.T) {
t.Helper()
select {
case <-time.After(probablyStillBlocked):
t.Fatalf("%s is unexpectedly still blocked", desc)
case <-done:
}
}
}
}
func TestLockExcludesLock(t *testing.T) {
t.Parallel()
f, remove := mustTempFile(t)
defer remove()
other := mustOpen(t, f.Name())
defer other.Close()
lock(t, f)
lockOther := mustBlock(t, "Lock", other)
unlock(t, f)
lockOther(t)
unlock(t, other)
}
func TestLockExcludesRLock(t *testing.T) {
t.Parallel()
f, remove := mustTempFile(t)
defer remove()
other := mustOpen(t, f.Name())
defer other.Close()
lock(t, f)
rLockOther := mustBlock(t, "RLock", other)
unlock(t, f)
rLockOther(t)
unlock(t, other)
}
func TestRLockExcludesOnlyLock(t *testing.T) {
t.Parallel()
f, remove := mustTempFile(t)
defer remove()
rLock(t, f)
f2 := mustOpen(t, f.Name())
defer f2.Close()
doUnlockTF := false
switch runtime.GOOS {
case "aix", "solaris":
// When using POSIX locks (as on Solaris), we can't safely read-lock the
// same inode through two different descriptors at the same time: when the
// first descriptor is closed, the second descriptor would still be open but
// silently unlocked. So a second RLock must block instead of proceeding.
lockF2 := mustBlock(t, "RLock", f2)
unlock(t, f)
lockF2(t)
default:
rLock(t, f2)
doUnlockTF = true
}
other := mustOpen(t, f.Name())
defer other.Close()
lockOther := mustBlock(t, "Lock", other)
unlock(t, f2)
if doUnlockTF {
unlock(t, f)
}
lockOther(t)
unlock(t, other)
}
func TestLockNotDroppedByExecCommand(t *testing.T) {
f, remove := mustTempFile(t)
defer remove()
lock(t, f)
other := mustOpen(t, f.Name())
defer other.Close()
// Some kinds of file locks are dropped when a duplicated or forked file
// descriptor is unlocked. Double-check that the approach used by os/exec does
// not accidentally drop locks.
cmd := exec.Command(os.Args[0], "-test.run=^$")
if err := cmd.Run(); err != nil {
t.Fatalf("exec failed: %v", err)
}
lockOther := mustBlock(t, "Lock", other)
unlock(t, f)
lockOther(t)
unlock(t, other)
}

Просмотреть файл

@ -0,0 +1,45 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build darwin || dragonfly || freebsd || illumos || linux || netbsd || openbsd
// +build darwin dragonfly freebsd illumos linux netbsd openbsd
package filelock
import (
"io/fs"
"syscall"
)
type lockType int16
const (
readLock lockType = syscall.LOCK_SH
writeLock lockType = syscall.LOCK_EX
)
func lock(f File, lt lockType) (err error) {
for {
err = syscall.Flock(int(f.Fd()), int(lt))
if err != syscall.EINTR {
break
}
}
if err != nil {
return &fs.PathError{
Op: lt.String(),
Path: f.Name(),
Err: err,
}
}
return nil
}
func unlock(f File) error {
return lock(f, syscall.LOCK_UN)
}
func isNotSupported(err error) bool {
return err == syscall.ENOSYS || err == syscall.ENOTSUP || err == syscall.EOPNOTSUPP || err == ErrNotSupported
}

Просмотреть файл

@ -0,0 +1,67 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build windows
// +build windows
package filelock
import (
"io/fs"
"golang.org/x/sys/windows"
)
type lockType uint32
const (
readLock lockType = 0
writeLock lockType = windows.LOCKFILE_EXCLUSIVE_LOCK
)
const (
reserved = 0
allBytes = ^uint32(0)
)
func lock(f File, lt lockType) error {
// Per https://golang.org/issue/19098, “Programs currently expect the Fd
// method to return a handle that uses ordinary synchronous I/O.”
// However, LockFileEx still requires an OVERLAPPED structure,
// which contains the file offset of the beginning of the lock range.
// We want to lock the entire file, so we leave the offset as zero.
ol := new(windows.Overlapped)
err := windows.LockFileEx(windows.Handle(f.Fd()), uint32(lt), reserved, allBytes, allBytes, ol)
if err != nil {
return &fs.PathError{
Op: lt.String(),
Path: f.Name(),
Err: err,
}
}
return nil
}
func unlock(f File) error {
ol := new(windows.Overlapped)
err := windows.UnlockFileEx(windows.Handle(f.Fd()), reserved, allBytes, allBytes, ol)
if err != nil {
return &fs.PathError{
Op: "Unlock",
Path: f.Name(),
Err: err,
}
}
return nil
}
func isNotSupported(err error) bool {
switch err {
case windows.ERROR_NOT_SUPPORTED, windows.ERROR_CALL_NOT_IMPLEMENTED, ErrNotSupported:
return true
default:
return false
}
}

Просмотреть файл

@ -0,0 +1,187 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package lockedfile creates and manipulates files whose contents should only
// change atomically.
package lockedfile
import (
"fmt"
"io"
"io/fs"
"os"
"runtime"
)
// 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 {
osFile
closed bool
}
// osFile embeds a *os.File while keeping the pointer itself unexported.
// (When we close a File, it must be the same file descriptor that we opened!)
type osFile struct {
*os.File
}
// OpenFile is like os.OpenFile, but returns a locked file.
// If flag includes os.O_WRONLY or os.O_RDWR, the file is write-locked;
// otherwise, it is read-locked.
func OpenFile(name string, flag int, perm fs.FileMode) (*File, error) {
var (
f = new(File)
err error
)
f.osFile.File, err = openFile(name, flag, perm)
if err != nil {
return nil, err
}
// Although the operating system will drop locks for open files when the go
// command exits, we want to hold locks for as little time as possible, and we
// especially don't want to leave a file locked after we're done with it. Our
// Close method is what releases the locks, so use a finalizer to report
// missing Close calls on a best-effort basis.
runtime.SetFinalizer(f, func(f *File) {
panic(fmt.Sprintf("lockedfile.File %s became unreachable without a call to Close", f.Name()))
})
return f, nil
}
// Open is like os.Open, but returns a read-locked file.
func Open(name string) (*File, error) {
return OpenFile(name, os.O_RDONLY, 0)
}
// Create is like os.Create, but returns a write-locked file.
func Create(name string) (*File, error) {
return OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
}
// 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) {
return OpenFile(name, os.O_RDWR|os.O_CREATE, 0666)
}
// 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 {
if f.closed {
return &fs.PathError{
Op: "close",
Path: f.Name(),
Err: fs.ErrClosed,
}
}
f.closed = true
err := closeFile(f.osFile.File)
runtime.SetFinalizer(f, nil)
return err
}
// Read opens the named file with a read-lock and returns its contents.
func Read(name string) ([]byte, error) {
f, err := Open(name)
if err != nil {
return nil, err
}
defer f.Close()
return io.ReadAll(f)
}
// Write opens the named file (creating it with the given permissions if needed),
// then write-locks it and overwrites it with the given content.
func Write(name string, content io.Reader, perm fs.FileMode) (err error) {
f, err := OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
if err != nil {
return err
}
_, err = io.Copy(f, content)
if closeErr := f.Close(); err == nil {
err = closeErr
}
return err
}
// 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) {
f, err := Edit(name)
if err != nil {
return err
}
defer f.Close()
old, err := io.ReadAll(f)
if err != nil {
return err
}
new, err := t(old)
if err != nil {
return err
}
if len(new) > len(old) {
// The overall file size is increasing, so write the tail first: if we're
// about to run out of space on the disk, we would rather detect that
// failure before we have overwritten the original contents.
if _, err := f.WriteAt(new[len(old):], int64(len(old))); err != nil {
// Make a best effort to remove the incomplete tail.
f.Truncate(int64(len(old)))
return err
}
}
// We're about to overwrite the old contents. In case of failure, make a best
// effort to roll back before we close the file.
defer func() {
if err != nil {
if _, err := f.WriteAt(old, 0); err == nil {
f.Truncate(int64(len(old)))
}
}
}()
if len(new) >= len(old) {
if _, err := f.WriteAt(new[:len(old)], 0); err != nil {
return err
}
} else {
if _, err := f.WriteAt(new, 0); err != nil {
return err
}
// The overall file size is decreasing, so shrink the file to its final size
// after writing. We do this after writing (instead of before) so that if
// the write fails, enough filesystem space will likely still be reserved
// to contain the previous contents.
if err := f.Truncate(int64(len(new))); err != nil {
return err
}
}
return nil
}

Просмотреть файл

@ -0,0 +1,66 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !plan9
// +build !plan9
package lockedfile
import (
"io/fs"
"os"
"golang.org/x/tools/internal/lockedfile/internal/filelock"
)
func openFile(name string, flag int, perm fs.FileMode) (*os.File, error) {
// On BSD systems, we could add the O_SHLOCK or O_EXLOCK flag to the OpenFile
// call instead of locking separately, but we have to support separate locking
// calls for Linux and Windows anyway, so it's simpler to use that approach
// consistently.
f, err := os.OpenFile(name, flag&^os.O_TRUNC, perm)
if err != nil {
return nil, err
}
switch flag & (os.O_RDONLY | os.O_WRONLY | os.O_RDWR) {
case os.O_WRONLY, os.O_RDWR:
err = filelock.Lock(f)
default:
err = filelock.RLock(f)
}
if err != nil {
f.Close()
return nil, err
}
if flag&os.O_TRUNC == os.O_TRUNC {
if err := f.Truncate(0); err != nil {
// The documentation for os.O_TRUNC says “if possible, truncate file when
// opened”, but doesn't define “possible” (golang.org/issue/28699).
// We'll treat regular files (and symlinks to regular files) as “possible”
// and ignore errors for the rest.
if fi, statErr := f.Stat(); statErr != nil || fi.Mode().IsRegular() {
filelock.Unlock(f)
f.Close()
return nil, err
}
}
}
return f, nil
}
func closeFile(f *os.File) error {
// Since locking syscalls operate on file descriptors, we must unlock the file
// while the descriptor is still valid — that is, before the file is closed —
// and avoid unlocking files that are already closed.
err := filelock.Unlock(f)
if closeErr := f.Close(); err == nil {
err = closeErr
}
return err
}

Просмотреть файл

@ -0,0 +1,95 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build plan9
// +build plan9
package lockedfile
import (
"io/fs"
"math/rand"
"os"
"strings"
"time"
)
// Opening an exclusive-use file returns an error.
// The expected error strings are:
//
// - "open/create -- file is locked" (cwfs, kfs)
// - "exclusive lock" (fossil)
// - "exclusive use file already open" (ramfs)
var lockedErrStrings = [...]string{
"file is locked",
"exclusive lock",
"exclusive use file already open",
}
// Even though plan9 doesn't support the Lock/RLock/Unlock functions to
// manipulate already-open files, IsLocked is still meaningful: os.OpenFile
// itself may return errors that indicate that a file with the ModeExclusive bit
// set is already open.
func isLocked(err error) bool {
s := err.Error()
for _, frag := range lockedErrStrings {
if strings.Contains(s, frag) {
return true
}
}
return false
}
func openFile(name string, flag int, perm fs.FileMode) (*os.File, error) {
// Plan 9 uses a mode bit instead of explicit lock/unlock syscalls.
//
// Per http://man.cat-v.org/plan_9/5/stat: “Exclusive use files may be open
// for I/O by only one fid at a time across all clients of the server. If a
// second open is attempted, it draws an error.”
//
// So we can try to open a locked file, but if it fails we're on our own to
// figure out when it becomes available. We'll use exponential backoff with
// some jitter and an arbitrary limit of 500ms.
// If the file was unpacked or created by some other program, it might not
// have the ModeExclusive bit set. Set it before we call OpenFile, so that we
// can be confident that a successful OpenFile implies exclusive use.
if fi, err := os.Stat(name); err == nil {
if fi.Mode()&fs.ModeExclusive == 0 {
if err := os.Chmod(name, fi.Mode()|fs.ModeExclusive); err != nil {
return nil, err
}
}
} else if !os.IsNotExist(err) {
return nil, err
}
nextSleep := 1 * time.Millisecond
const maxSleep = 500 * time.Millisecond
for {
f, err := os.OpenFile(name, flag, perm|fs.ModeExclusive)
if err == nil {
return f, nil
}
if !isLocked(err) {
return nil, err
}
time.Sleep(nextSleep)
nextSleep += nextSleep
if nextSleep > maxSleep {
nextSleep = maxSleep
}
// Apply 10% jitter to avoid synchronizing collisions.
nextSleep += time.Duration((0.1*rand.Float64() - 0.05) * float64(nextSleep))
}
}
func closeFile(f *os.File) error {
return f.Close()
}

Просмотреть файл

@ -0,0 +1,270 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// js does not support inter-process file locking.
//
//go:build !js
// +build !js
package lockedfile_test
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"testing"
"time"
"golang.org/x/tools/internal/lockedfile"
)
func mustTempDir(t *testing.T) (dir string, remove func()) {
t.Helper()
dir, err := os.MkdirTemp("", filepath.Base(t.Name()))
if err != nil {
t.Fatal(err)
}
return dir, func() { os.RemoveAll(dir) }
}
const (
quiescent = 10 * time.Millisecond
probablyStillBlocked = 10 * time.Second
)
func mustBlock(t *testing.T, desc string, f func()) (wait func(*testing.T)) {
t.Helper()
done := make(chan struct{})
go func() {
f()
close(done)
}()
select {
case <-done:
t.Fatalf("%s unexpectedly did not block", desc)
return nil
case <-time.After(quiescent):
return func(t *testing.T) {
t.Helper()
select {
case <-time.After(probablyStillBlocked):
t.Fatalf("%s is unexpectedly still blocked after %v", desc, probablyStillBlocked)
case <-done:
}
}
}
}
func TestMutexExcludes(t *testing.T) {
t.Parallel()
dir, remove := mustTempDir(t)
defer remove()
path := filepath.Join(dir, "lock")
mu := lockedfile.MutexAt(path)
t.Logf("mu := MutexAt(_)")
unlock, err := mu.Lock()
if err != nil {
t.Fatalf("mu.Lock: %v", err)
}
t.Logf("unlock, _ := mu.Lock()")
mu2 := lockedfile.MutexAt(mu.Path)
t.Logf("mu2 := MutexAt(mu.Path)")
wait := mustBlock(t, "mu2.Lock()", func() {
unlock2, err := mu2.Lock()
if err != nil {
t.Errorf("mu2.Lock: %v", err)
return
}
t.Logf("unlock2, _ := mu2.Lock()")
t.Logf("unlock2()")
unlock2()
})
t.Logf("unlock()")
unlock()
wait(t)
}
func TestReadWaitsForLock(t *testing.T) {
t.Parallel()
dir, remove := mustTempDir(t)
defer remove()
path := filepath.Join(dir, "timestamp.txt")
f, err := lockedfile.Create(path)
if err != nil {
t.Fatalf("Create: %v", err)
}
defer f.Close()
const (
part1 = "part 1\n"
part2 = "part 2\n"
)
_, err = f.WriteString(part1)
if err != nil {
t.Fatalf("WriteString: %v", err)
}
t.Logf("WriteString(%q) = <nil>", part1)
wait := mustBlock(t, "Read", func() {
b, err := lockedfile.Read(path)
if err != nil {
t.Errorf("Read: %v", err)
return
}
const want = part1 + part2
got := string(b)
if got == want {
t.Logf("Read(_) = %q", got)
} else {
t.Errorf("Read(_) = %q, _; want %q", got, want)
}
})
_, err = f.WriteString(part2)
if err != nil {
t.Errorf("WriteString: %v", err)
} else {
t.Logf("WriteString(%q) = <nil>", part2)
}
f.Close()
wait(t)
}
func TestCanLockExistingFile(t *testing.T) {
t.Parallel()
dir, remove := mustTempDir(t)
defer remove()
path := filepath.Join(dir, "existing.txt")
if err := os.WriteFile(path, []byte("ok"), 0777); err != nil {
t.Fatalf("os.WriteFile: %v", err)
}
f, err := lockedfile.Edit(path)
if err != nil {
t.Fatalf("first Edit: %v", err)
}
wait := mustBlock(t, "Edit", func() {
other, err := lockedfile.Edit(path)
if err != nil {
t.Errorf("second Edit: %v", err)
}
other.Close()
})
f.Close()
wait(t)
}
// TestSpuriousEDEADLK verifies that the spurious EDEADLK reported in
// https://golang.org/issue/32817 no longer occurs.
func TestSpuriousEDEADLK(t *testing.T) {
// P.1 locks file A.
// Q.3 locks file B.
// Q.3 blocks on file A.
// P.2 blocks on file B. (Spurious EDEADLK occurs here.)
// P.1 unlocks file A.
// Q.3 unblocks and locks file A.
// Q.3 unlocks files A and B.
// P.2 unblocks and locks file B.
// P.2 unlocks file B.
dirVar := t.Name() + "DIR"
if dir := os.Getenv(dirVar); dir != "" {
// Q.3 locks file B.
b, err := lockedfile.Edit(filepath.Join(dir, "B"))
if err != nil {
t.Fatal(err)
}
defer b.Close()
if err := os.WriteFile(filepath.Join(dir, "locked"), []byte("ok"), 0666); err != nil {
t.Fatal(err)
}
// Q.3 blocks on file A.
a, err := lockedfile.Edit(filepath.Join(dir, "A"))
// Q.3 unblocks and locks file A.
if err != nil {
t.Fatal(err)
}
defer a.Close()
// Q.3 unlocks files A and B.
return
}
dir, remove := mustTempDir(t)
defer remove()
// P.1 locks file A.
a, err := lockedfile.Edit(filepath.Join(dir, "A"))
if err != nil {
t.Fatal(err)
}
cmd := exec.Command(os.Args[0], "-test.run="+t.Name())
cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%s", dirVar, dir))
qDone := make(chan struct{})
waitQ := mustBlock(t, "Edit A and B in subprocess", func() {
out, err := cmd.CombinedOutput()
if err != nil {
t.Errorf("%v:\n%s", err, out)
}
close(qDone)
})
// Wait until process Q has either failed or locked file B.
// Otherwise, P.2 might not block on file B as intended.
locked:
for {
if _, err := os.Stat(filepath.Join(dir, "locked")); !os.IsNotExist(err) {
break locked
}
select {
case <-qDone:
break locked
case <-time.After(1 * time.Millisecond):
}
}
waitP2 := mustBlock(t, "Edit B", func() {
// P.2 blocks on file B. (Spurious EDEADLK occurs here.)
b, err := lockedfile.Edit(filepath.Join(dir, "B"))
// P.2 unblocks and locks file B.
if err != nil {
t.Error(err)
return
}
// P.2 unlocks file B.
b.Close()
})
// P.1 unlocks file A.
a.Close()
waitQ(t)
waitP2(t)
}

Просмотреть файл

@ -0,0 +1,67 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package lockedfile
import (
"fmt"
"os"
"sync"
)
// 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 string // The path to the well-known lock file. Must be non-empty.
mu sync.Mutex // A redundant mutex. The race detector doesn't know about file locking, so in tests we may need to lock something that it understands.
}
// MutexAt returns a new Mutex with Path set to the given non-empty path.
func MutexAt(path string) *Mutex {
if path == "" {
panic("lockedfile.MutexAt: path must be non-empty")
}
return &Mutex{Path: path}
}
func (mu *Mutex) String() string {
return fmt.Sprintf("lockedfile.Mutex(%s)", mu.Path)
}
// 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) {
if mu.Path == "" {
panic("lockedfile.Mutex: missing Path during Lock")
}
// We could use either O_RDWR or O_WRONLY here. If we choose O_RDWR and the
// file at mu.Path is write-only, the call to OpenFile will fail with a
// permission error. That's actually what we want: if we add an RLock method
// in the future, it should call OpenFile with O_RDONLY and will require the
// files must be readable, so we should not let the caller make any
// assumptions about Mutex working with write-only files.
f, err := OpenFile(mu.Path, os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
return nil, err
}
mu.mu.Lock()
return func() {
mu.mu.Unlock()
f.Close()
}, nil
}

Просмотреть файл

@ -0,0 +1,106 @@
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// js does not support inter-process file locking.
//
//go:build !js
// +build !js
package lockedfile_test
import (
"bytes"
"encoding/binary"
"math/rand"
"path/filepath"
"testing"
"time"
"golang.org/x/tools/internal/lockedfile"
)
func isPowerOf2(x int) bool {
return x > 0 && x&(x-1) == 0
}
func roundDownToPowerOf2(x int) int {
if x <= 0 {
panic("nonpositive x")
}
bit := 1
for x != bit {
x = x &^ bit
bit <<= 1
}
return x
}
func TestTransform(t *testing.T) {
dir, remove := mustTempDir(t)
defer remove()
path := filepath.Join(dir, "blob.bin")
const maxChunkWords = 8 << 10
buf := make([]byte, 2*maxChunkWords*8)
for i := uint64(0); i < 2*maxChunkWords; i++ {
binary.LittleEndian.PutUint64(buf[i*8:], i)
}
if err := lockedfile.Write(path, bytes.NewReader(buf[:8]), 0666); err != nil {
t.Fatal(err)
}
var attempts int64 = 128
if !testing.Short() {
attempts *= 16
}
const parallel = 32
var sem = make(chan bool, parallel)
for n := attempts; n > 0; n-- {
sem <- true
go func() {
defer func() { <-sem }()
time.Sleep(time.Duration(rand.Intn(100)) * time.Microsecond)
chunkWords := roundDownToPowerOf2(rand.Intn(maxChunkWords) + 1)
offset := rand.Intn(chunkWords)
err := lockedfile.Transform(path, func(data []byte) (chunk []byte, err error) {
chunk = buf[offset*8 : (offset+chunkWords)*8]
if len(data)&^7 != len(data) {
t.Errorf("read %d bytes, but each write is an integer multiple of 8 bytes", len(data))
return chunk, nil
}
words := len(data) / 8
if !isPowerOf2(words) {
t.Errorf("read %d 8-byte words, but each write is a power-of-2 number of words", words)
return chunk, nil
}
u := binary.LittleEndian.Uint64(data)
for i := 1; i < words; i++ {
next := binary.LittleEndian.Uint64(data[i*8:])
if next != u+1 {
t.Errorf("wrote sequential integers, but read integer out of sequence at offset %d", i)
return chunk, nil
}
u = next
}
return chunk, nil
})
if err != nil {
t.Errorf("unexpected error from Transform: %v", err)
}
}()
}
for n := parallel; n > 0; n-- {
sem <- true
}
}