Skip to content

Commit 47dc928

Browse files
author
Bryan C. Mills
committed
cmd/go/internal/lockedfile: add package and support library
lockedfile.File passes through to os.File, with Open, Create, and OpenFile functions that mimic the corresponding os functions but acquire locks automatically, releasing them when the file is closed. lockedfile.Sentinel is a simplified wrapper around lockedfile.OpenFile for the common use-case of files that signal the status of idempotent tasks. lockedfile.Mutex is a Mutex-like synchronization primitive implemented in terms of file locks. lockedfile.Read is like ioutil.Read, but obtains a read-lock. lockedfile.Write is like ioutil.Write, but obtains a write-lock and can be used for read-only files with idempotent contents. Updates #26794 Change-Id: I50f7132c71d2727862eed54411f3f27e1af55cad Reviewed-on: https://go-review.googlesource.com/c/145178 Run-TryBot: Bryan C. Mills <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Russ Cox <[email protected]>
1 parent a30f8d1 commit 47dc928

13 files changed

+1167
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Copyright 2018 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// Package filelock provides a platform-independent API for advisory file
6+
// locking. Calls to functions in this package on platforms that do not support
7+
// advisory locks will return errors for which IsNotSupported returns true.
8+
package filelock
9+
10+
import (
11+
"errors"
12+
"os"
13+
)
14+
15+
// A File provides the minimal set of methods required to lock an open file.
16+
// File implementations must be usable as map keys.
17+
// The usual implementation is *os.File.
18+
type File interface {
19+
// Name returns the name of the file.
20+
Name() string
21+
22+
// Fd returns a valid file descriptor.
23+
// (If the File is an *os.File, it must not be closed.)
24+
Fd() uintptr
25+
26+
// Stat returns the FileInfo structure describing file.
27+
Stat() (os.FileInfo, error)
28+
}
29+
30+
// Lock places an advisory write lock on the file, blocking until it can be
31+
// locked.
32+
//
33+
// If Lock returns nil, no other process will be able to place a read or write
34+
// lock on the file until this process exits, closes f, or calls Unlock on it.
35+
//
36+
// If f's descriptor is already read- or write-locked, the behavior of Lock is
37+
// unspecified.
38+
//
39+
// Closing the file may or may not release the lock promptly. Callers should
40+
// ensure that Unlock is always called when Lock succeeds.
41+
func Lock(f File) error {
42+
return lock(f, writeLock)
43+
}
44+
45+
// RLock places an advisory read lock on the file, blocking until it can be locked.
46+
//
47+
// If RLock returns nil, no other process will be able to place a write lock on
48+
// the file until this process exits, closes f, or calls Unlock on it.
49+
//
50+
// If f is already read- or write-locked, the behavior of RLock is unspecified.
51+
//
52+
// Closing the file may or may not release the lock promptly. Callers should
53+
// ensure that Unlock is always called if RLock succeeds.
54+
func RLock(f File) error {
55+
return lock(f, readLock)
56+
}
57+
58+
// Unlock removes an advisory lock placed on f by this process.
59+
//
60+
// The caller must not attempt to unlock a file that is not locked.
61+
func Unlock(f File) error {
62+
return unlock(f)
63+
}
64+
65+
// String returns the name of the function corresponding to lt
66+
// (Lock, RLock, or Unlock).
67+
func (lt lockType) String() string {
68+
switch lt {
69+
case readLock:
70+
return "RLock"
71+
case writeLock:
72+
return "Lock"
73+
default:
74+
return "Unlock"
75+
}
76+
}
77+
78+
// IsNotSupported returns a boolean indicating whether the error is known to
79+
// report that a function is not supported (possibly for a specific input).
80+
// It is satisfied by ErrNotSupported as well as some syscall errors.
81+
func IsNotSupported(err error) bool {
82+
return isNotSupported(underlyingError(err))
83+
}
84+
85+
var ErrNotSupported = errors.New("operation not supported")
86+
87+
// underlyingError returns the underlying error for known os error types.
88+
func underlyingError(err error) error {
89+
switch err := err.(type) {
90+
case *os.PathError:
91+
return err.Err
92+
case *os.LinkError:
93+
return err.Err
94+
case *os.SyscallError:
95+
return err.Err
96+
}
97+
return err
98+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright 2018 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// +build !darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!plan9,!solaris,!windows
6+
7+
package filelock
8+
9+
import "os"
10+
11+
type lockType int8
12+
13+
const (
14+
readLock = iota + 1
15+
writeLock
16+
)
17+
18+
func lock(f File, lt lockType) error {
19+
return &os.PathError{
20+
Op: lt.String(),
21+
Path: f.Name(),
22+
Err: ErrNotSupported,
23+
}
24+
}
25+
26+
func unlock(f File) error {
27+
return &os.PathError{
28+
Op: "Unlock",
29+
Path: f.Name(),
30+
Err: ErrNotSupported,
31+
}
32+
}
33+
34+
func isNotSupported(err error) bool {
35+
return err == ErrNotSupported
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright 2018 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// +build plan9
6+
7+
package filelock
8+
9+
import (
10+
"os"
11+
)
12+
13+
type lockType int8
14+
15+
const (
16+
readLock = iota + 1
17+
writeLock
18+
)
19+
20+
func lock(f File, lt lockType) error {
21+
return &os.PathError{
22+
Op: lt.String(),
23+
Path: f.Name(),
24+
Err: ErrNotSupported,
25+
}
26+
}
27+
28+
func unlock(f File) error {
29+
return &os.PathError{
30+
Op: "Unlock",
31+
Path: f.Name(),
32+
Err: ErrNotSupported,
33+
}
34+
}
35+
36+
func isNotSupported(err error) bool {
37+
return err == ErrNotSupported
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
// Copyright 2018 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// This code implements the filelock API using POSIX 'fcntl' locks, which attach
6+
// to an (inode, process) pair rather than a file descriptor. To avoid unlocking
7+
// files prematurely when the same file is opened through different descriptors,
8+
// we allow only one read-lock at a time.
9+
//
10+
// Most platforms provide some alternative API, such as an 'flock' system call
11+
// or an F_OFD_SETLK command for 'fcntl', that allows for better concurrency and
12+
// does not require per-inode bookkeeping in the application.
13+
//
14+
// TODO(bcmills): If we add a build tag for Illumos (see golang.org/issue/20603)
15+
// then Illumos should use F_OFD_SETLK, and the resulting code would be as
16+
// simple as filelock_unix.go. We will still need the code in this file as long
17+
// as Oracle Solaris provides only F_SETLK.
18+
19+
package filelock
20+
21+
import (
22+
"errors"
23+
"io"
24+
"os"
25+
"sync"
26+
"syscall"
27+
)
28+
29+
type lockType int16
30+
31+
const (
32+
readLock lockType = syscall.F_RDLCK
33+
writeLock lockType = syscall.F_WRLCK
34+
)
35+
36+
type inode = uint64 // type of syscall.Stat_t.Ino
37+
38+
type inodeLock struct {
39+
owner File
40+
queue []<-chan File
41+
}
42+
43+
type token struct{}
44+
45+
var (
46+
mu sync.Mutex
47+
inodes = map[File]inode{}
48+
locks = map[inode]inodeLock{}
49+
)
50+
51+
func lock(f File, lt lockType) (err error) {
52+
// POSIX locks apply per inode and process, and the lock for an inode is
53+
// released when *any* descriptor for that inode is closed. So we need to
54+
// synchronize access to each inode internally, and must serialize lock and
55+
// unlock calls that refer to the same inode through different descriptors.
56+
fi, err := f.Stat()
57+
if err != nil {
58+
return err
59+
}
60+
ino := fi.Sys().(*syscall.Stat_t).Ino
61+
62+
mu.Lock()
63+
if i, dup := inodes[f]; dup && i != ino {
64+
mu.Unlock()
65+
return &os.PathError{
66+
Op: lt.String(),
67+
Path: f.Name(),
68+
Err: errors.New("inode for file changed since last Lock or RLock"),
69+
}
70+
}
71+
inodes[f] = ino
72+
73+
var wait chan File
74+
l := locks[ino]
75+
if l.owner == f {
76+
// This file already owns the lock, but the call may change its lock type.
77+
} else if l.owner == nil {
78+
// No owner: it's ours now.
79+
l.owner = f
80+
} else {
81+
// Already owned: add a channel to wait on.
82+
wait = make(chan File)
83+
l.queue = append(l.queue, wait)
84+
}
85+
locks[ino] = l
86+
mu.Unlock()
87+
88+
if wait != nil {
89+
wait <- f
90+
}
91+
92+
err = setlkw(f.Fd(), lt)
93+
94+
if err != nil {
95+
unlock(f)
96+
return &os.PathError{
97+
Op: lt.String(),
98+
Path: f.Name(),
99+
Err: err,
100+
}
101+
}
102+
103+
return nil
104+
}
105+
106+
func unlock(f File) error {
107+
var owner File
108+
109+
mu.Lock()
110+
ino, ok := inodes[f]
111+
if ok {
112+
owner = locks[ino].owner
113+
}
114+
mu.Unlock()
115+
116+
if owner != f {
117+
panic("unlock called on a file that is not locked")
118+
}
119+
120+
err := setlkw(f.Fd(), syscall.F_UNLCK)
121+
122+
mu.Lock()
123+
l := locks[ino]
124+
if len(l.queue) == 0 {
125+
// No waiters: remove the map entry.
126+
delete(locks, ino)
127+
} else {
128+
// The first waiter is sending us their file now.
129+
// Receive it and update the queue.
130+
l.owner = <-l.queue[0]
131+
l.queue = l.queue[1:]
132+
locks[ino] = l
133+
}
134+
delete(inodes, f)
135+
mu.Unlock()
136+
137+
return err
138+
}
139+
140+
// setlkw calls FcntlFlock with F_SETLKW for the entire file indicated by fd.
141+
func setlkw(fd uintptr, lt lockType) error {
142+
for {
143+
err := syscall.FcntlFlock(fd, syscall.F_SETLKW, &syscall.Flock_t{
144+
Type: int16(lt),
145+
Whence: io.SeekStart,
146+
Start: 0,
147+
Len: 0, // All bytes.
148+
})
149+
if err != syscall.EINTR {
150+
return err
151+
}
152+
}
153+
}
154+
155+
func isNotSupported(err error) bool {
156+
return err == syscall.ENOSYS || err == syscall.ENOTSUP || err == syscall.EOPNOTSUPP || err == ErrNotSupported
157+
}

0 commit comments

Comments
 (0)