diff --git a/libgoal/lockedFile.go b/libgoal/lockedFile.go index 93d5507169..36a96b2d7b 100644 --- a/libgoal/lockedFile.go +++ b/libgoal/lockedFile.go @@ -20,102 +20,85 @@ import ( "fmt" "io/ioutil" "os" - "syscall" - "time" ) -// Platform-dependant locker implementation -// How to extend -// 1. Create two new files locker.go and locker_platform.go -// 2. Put appropriate build tags -// 3. Move unixLocker implementation and `newLockedFile` method to locker.go -// 4. Implement platform-specific locker in locker_platform.go -// 5. Ensure `newLockedFile` sets platform-specific locker -// so that lockedFile.read and lockedFile.write work correctly - type locker interface { tryRLock(fd *os.File) error tryLock(fd *os.File) error unlock(fd *os.File) error } -type unixLocker struct { -} - -func (f *unixLocker) tryRLock(fd *os.File) error { - return syscall.Flock(int(fd.Fd()), syscall.LOCK_SH|syscall.LOCK_NB) -} - -func (f *unixLocker) tryLock(fd *os.File) error { - return syscall.Flock(int(fd.Fd()), syscall.LOCK_EX|syscall.LOCK_NB) -} - -func (f *unixLocker) unlock(fd *os.File) error { - return syscall.Flock(int(fd.Fd()), syscall.LOCK_UN) +func newLockedFile(path string) *lockedFile { + return &lockedFile{ + path: path, + locker: makeLocker(), + } } // lockedFile implementation -// It uses non-blocking acquisition with repeats -// and supposed to be platform-agnostic with appropriate locker implementation. +// It a platform-agnostic with appropriate locker implementation. // Each platform needs own specific `newLockedFile` -const maxRepeats = 10 -const sleepInterval = 10 * time.Millisecond type lockedFile struct { path string locker locker } -func newLockedFile(path string) *lockedFile { - return &lockedFile{ - path: path, - locker: &unixLocker{}, - } -} - -func (f *lockedFile) read() ([]byte, error) { +func (f *lockedFile) read() (bytes []byte, err error) { fd, err := os.Open(f.path) if err != nil { - return nil, err + return } - defer fd.Close() + defer func() { + err2 := fd.Close() + if err2 != nil { + err = err2 + } + }() - lockFunc := func() error { return f.locker.tryRLock(fd) } - err = attemptLock(lockFunc) + err = f.locker.tryRLock(fd) if err != nil { - return nil, fmt.Errorf("Can't acquire lock for %s: %s", f.path, err.Error()) + err = fmt.Errorf("Can't acquire read lock for %s: %s", f.path, err.Error()) + return } - defer f.locker.unlock(fd) + defer func() { + err2 := f.locker.unlock(fd) + if err2 != nil { + err = fmt.Errorf("Can't unlock for %s: %s", f.path, err2.Error()) + } + }() - return ioutil.ReadAll(fd) + bytes, err = ioutil.ReadAll(fd) + return } -func (f *lockedFile) write(data []byte, perm os.FileMode) error { +func (f *lockedFile) write(data []byte, perm os.FileMode) (err error) { fd, err := os.OpenFile(f.path, os.O_WRONLY|os.O_CREATE, perm) if err != nil { - return err + return } - defer fd.Close() + defer func() { + err2 := fd.Close() + if err2 != nil { + err = err2 + } + }() - lockFunc := func() error { return f.locker.tryLock(fd) } - err = attemptLock(lockFunc) + err = f.locker.tryLock(fd) if err != nil { return fmt.Errorf("Can't acquire lock for %s: %s", f.path, err.Error()) } - defer f.locker.unlock(fd) - - fd.Truncate(0) - _, err = fd.Write(data) - return err -} - -func attemptLock(lockFunc func() error) error { - var savedError error - for repeatCounter := 0; repeatCounter < maxRepeats; repeatCounter++ { - if savedError = lockFunc(); savedError == nil { - break + defer func() { + err2 := f.locker.unlock(fd) + if err2 != nil { + err = fmt.Errorf("Can't unlock for %s: %s", f.path, err2.Error()) } - time.Sleep(sleepInterval) + }() + + err = fd.Truncate(0) + if err != nil { + return } - return savedError + _, err = fd.Write(data) + return } diff --git a/libgoal/lockedFileLinux.go b/libgoal/lockedFileLinux.go new file mode 100644 index 0000000000..5330723351 --- /dev/null +++ b/libgoal/lockedFileLinux.go @@ -0,0 +1,71 @@ +// Copyright (C) 2019-2020 Algorand, Inc. +// This file is part of go-algorand +// +// go-algorand is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// go-algorand is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with go-algorand. If not, see . + +// +build linux + +package libgoal + +import ( + "io" + "os" + "syscall" + + "golang.org/x/sys/unix" +) + +type linuxLocker struct { +} + +// makeLocker create a unix file locker. +// note that the desired way is to use the OFD locker, which locks on the file descriptor level. +// falling back to the non-OFD lock would allow obtaining two locks by the same process. If this becomes +// and issue, we might want to use flock, which wouldn't work across NFS. +func makeLocker() *linuxLocker { + locker := &linuxLocker{} + return locker +} + +// the FcntlFlock has the most consistent behaviour across platforms, +// and supports both local and network file systems. +func (f *linuxLocker) tryRLock(fd *os.File) error { + flock := &syscall.Flock_t{ + Type: syscall.F_RDLCK, + Whence: int16(io.SeekStart), + Start: 0, + Len: 0, + } + return syscall.FcntlFlock(fd.Fd(), unix.F_OFD_SETLKW, flock) +} + +func (f *linuxLocker) tryLock(fd *os.File) error { + flock := &syscall.Flock_t{ + Type: syscall.F_WRLCK, + Whence: int16(io.SeekStart), + Start: 0, + Len: 0, + } + return syscall.FcntlFlock(fd.Fd(), unix.F_OFD_SETLKW, flock) +} + +func (f *linuxLocker) unlock(fd *os.File) error { + flock := &syscall.Flock_t{ + Type: syscall.F_UNLCK, + Whence: int16(io.SeekStart), + Start: 0, + Len: 0, + } + return syscall.FcntlFlock(fd.Fd(), unix.F_OFD_SETLKW, flock) +} diff --git a/libgoal/lockedFileUnix.go b/libgoal/lockedFileUnix.go new file mode 100644 index 0000000000..917d63209a --- /dev/null +++ b/libgoal/lockedFileUnix.go @@ -0,0 +1,77 @@ +// Copyright (C) 2019-2020 Algorand, Inc. +// This file is part of go-algorand +// +// go-algorand is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// go-algorand is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with go-algorand. If not, see . + +// +build !linux,!windows + +package libgoal + +import ( + "io" + "os" + "syscall" +) + +type unixLocker struct { + setLockWait int +} + +// makeLocker create a unix file locker. +// note that the desired way is to use the OFD locker, which locks on the file descriptor level. +// falling back to the non-OFD lock would allow obtaining two locks by the same process. If this becomes +// and issue, we might want to use flock, which wouldn't work across NFS. +func makeLocker() *unixLocker { + locker := &unixLocker{} + getlk := syscall.Flock_t{Type: syscall.F_RDLCK} + if err := syscall.FcntlFlock(0, 36 /*F_OFD_GETLK*/, &getlk); err == nil { + // constants from /usr/include/bits/fcntl-linux.h + locker.setLockWait = 38 // F_OFD_SETLKW + } else { + locker.setLockWait = syscall.F_SETLKW + } + return locker +} + +// the FcntlFlock has the most unixLocker behaviour across platforms, +// and supports both local and network file systems. +func (f *unixLocker) tryRLock(fd *os.File) error { + flock := &syscall.Flock_t{ + Type: syscall.F_RDLCK, + Whence: int16(io.SeekStart), + Start: 0, + Len: 0, + } + return syscall.FcntlFlock(fd.Fd(), f.setLockWait, flock) +} + +func (f *unixLocker) tryLock(fd *os.File) error { + flock := &syscall.Flock_t{ + Type: syscall.F_WRLCK, + Whence: int16(io.SeekStart), + Start: 0, + Len: 0, + } + return syscall.FcntlFlock(fd.Fd(), f.setLockWait, flock) +} + +func (f *unixLocker) unlock(fd *os.File) error { + flock := &syscall.Flock_t{ + Type: syscall.F_UNLCK, + Whence: int16(io.SeekStart), + Start: 0, + Len: 0, + } + return syscall.FcntlFlock(fd.Fd(), f.setLockWait, flock) +}