package lockfile import ( "fmt" "os" "path/filepath" "sync" "time" ) // A Locker represents a file lock where the file is used to cache an // identifier of the last party that made changes to whatever's being protected // by the lock. // // Deprecated: Refer directly to *LockFile, the provided implementation, instead. type Locker interface { // Acquire a writer lock. // The default unix implementation panics if: // - opening the lockfile failed // - tried to lock a read-only lock-file Lock() // Unlock the lock. // The default unix implementation panics if: // - unlocking an unlocked lock // - if the lock counter is corrupted Unlock() // Acquire a reader lock. RLock() // Touch records, for others sharing the lock, that the caller was the // last writer. It should only be called with the lock held. // // Deprecated: Use *LockFile.RecordWrite. Touch() error // Modified() checks if the most recent writer was a party other than the // last recorded writer. It should only be called with the lock held. // Deprecated: Use *LockFile.ModifiedSince. Modified() (bool, error) // TouchedSince() checks if the most recent writer modified the file (likely using Touch()) after the specified time. TouchedSince(when time.Time) bool // IsReadWrite() checks if the lock file is read-write IsReadWrite() bool // AssertLocked() can be used by callers that _know_ that they hold the lock (for reading or writing), for sanity checking. // It might do nothing at all, or it may panic if the caller is not the owner of this lock. AssertLocked() // AssertLockedForWriting() can be used by callers that _know_ that they hold the lock locked for writing, for sanity checking. // It might do nothing at all, or it may panic if the caller is not the owner of this lock for writing. AssertLockedForWriting() } type lockType byte const ( readLock lockType = iota writeLock ) // LockFile represents a file lock where the file is used to cache an // identifier of the last party that made changes to whatever's being protected // by the lock. // // It MUST NOT be created manually. Use GetLockFile or GetROLockFile instead. type LockFile struct { // The following fields are only set when constructing *LockFile, and must never be modified afterwards. // They are safe to access without any other locking. file string ro bool // rwMutex serializes concurrent reader-writer acquisitions in the same process space rwMutex *sync.RWMutex // stateMutex is used to synchronize concurrent accesses to the state below stateMutex *sync.Mutex counter int64 lw LastWrite // A global value valid as of the last .Touch() or .Modified() lockType lockType locked bool // The following fields are only modified on transitions between counter == 0 / counter != 0. // Thus, they can be safely accessed by users _that currently hold the LockFile_ without locking. // In other cases, they need to be protected using stateMutex. fd fileHandle } var ( lockFiles map[string]*LockFile lockFilesLock sync.Mutex ) // GetLockFile opens a read-write lock file, creating it if necessary. The // *LockFile object may already be locked if the path has already been requested // by the current process. func GetLockFile(path string) (*LockFile, error) { return getLockfile(path, false) } // GetLockfile opens a read-write lock file, creating it if necessary. The // Locker object may already be locked if the path has already been requested // by the current process. // // Deprecated: Use GetLockFile func GetLockfile(path string) (Locker, error) { return GetLockFile(path) } // GetROLockFile opens a read-only lock file, creating it if necessary. The // *LockFile object may already be locked if the path has already been requested // by the current process. func GetROLockFile(path string) (*LockFile, error) { return getLockfile(path, true) } // GetROLockfile opens a read-only lock file, creating it if necessary. The // Locker object may already be locked if the path has already been requested // by the current process. // // Deprecated: Use GetROLockFile func GetROLockfile(path string) (Locker, error) { return GetROLockFile(path) } // Lock locks the lockfile as a writer. Panic if the lock is a read-only one. func (l *LockFile) Lock() { if l.ro { panic("can't take write lock on read-only lock file") } else { l.lock(writeLock) } } // LockRead locks the lockfile as a reader. func (l *LockFile) RLock() { l.lock(readLock) } // Unlock unlocks the lockfile. func (l *LockFile) Unlock() { l.stateMutex.Lock() if !l.locked { // Panic when unlocking an unlocked lock. That's a violation // of the lock semantics and will reveal such. panic("calling Unlock on unlocked lock") } l.counter-- if l.counter < 0 { // Panic when the counter is negative. There is no way we can // recover from a corrupted lock and we need to protect the // storage from corruption. panic(fmt.Sprintf("lock %q has been unlocked too often", l.file)) } if l.counter == 0 { // We should only release the lock when the counter is 0 to // avoid releasing read-locks too early; a given process may // acquire a read lock multiple times. l.locked = false // Close the file descriptor on the last unlock, releasing the // file lock. unlockAndCloseHandle(l.fd) } if l.lockType == readLock { l.rwMutex.RUnlock() } else { l.rwMutex.Unlock() } l.stateMutex.Unlock() } func (l *LockFile) AssertLocked() { // DO NOT provide a variant that returns the value of l.locked. // // If the caller does not hold the lock, l.locked might nevertheless be true because another goroutine does hold it, and // we can’t tell the difference. // // Hence, this “AssertLocked” method, which exists only for sanity checks. // Don’t even bother with l.stateMutex: The caller is expected to hold the lock, and in that case l.locked is constant true // with no possible writers. // If the caller does not hold the lock, we are violating the locking/memory model anyway, and accessing the data // without the lock is more efficient for callers, and potentially more visible to lock analysers for incorrect callers. if !l.locked { panic("internal error: lock is not held by the expected owner") } } func (l *LockFile) AssertLockedForWriting() { // DO NOT provide a variant that returns the current lock state. // // The same caveats as for AssertLocked apply equally. l.AssertLocked() // Like AssertLocked, don’t even bother with l.stateMutex. if l.lockType == readLock { panic("internal error: lock is not held for writing") } } // ModifiedSince checks if the lock has been changed since a provided LastWrite value, // and returns the one to record instead. // // If ModifiedSince reports no modification, the previous LastWrite value // is still valid and can continue to be used. // // If this function fails, the LastWriter value of the lock is indeterminate; // the caller should fail and keep using the previously-recorded LastWrite value, // so that it continues failing until the situation is resolved. Similarly, // it should only update the recorded LastWrite value after processing the update: // // lw2, modified, err := state.lock.ModifiedSince(state.lastWrite) // if err != nil { /* fail */ } // state.lastWrite = lw2 // if modified { // if err := reload(); err != nil { /* fail */ } // state.lastWrite = lw2 // } // // The caller must hold the lock (for reading or writing). func (l *LockFile) ModifiedSince(previous LastWrite) (LastWrite, bool, error) { l.AssertLocked() currentLW, err := l.GetLastWrite() if err != nil { return LastWrite{}, false, err } modified := !previous.equals(currentLW) return currentLW, modified, nil } // Modified indicates if the lockfile has been updated since the last time it // was loaded. // NOTE: Unlike ModifiedSince, this returns true the first time it is called on a *LockFile. // Callers cannot, in general, rely on this, because that might have happened for some other // owner of the same *LockFile who created it previously. // // Deprecated: Use *LockFile.ModifiedSince. func (l *LockFile) Modified() (bool, error) { l.stateMutex.Lock() if !l.locked { panic("attempted to check last-writer in lockfile without locking it first") } defer l.stateMutex.Unlock() oldLW := l.lw // Note that this is called with stateMutex held; that’s fine because ModifiedSince doesn’t need to lock it. currentLW, modified, err := l.ModifiedSince(oldLW) if err != nil { return true, err } l.lw = currentLW return modified, nil } // Touch updates the lock file with to record that the current lock holder has modified the lock-protected data. // // Deprecated: Use *LockFile.RecordWrite. func (l *LockFile) Touch() error { lw, err := l.RecordWrite() if err != nil { return err } l.stateMutex.Lock() if !l.locked || (l.lockType == readLock) { panic("attempted to update last-writer in lockfile without the write lock") } defer l.stateMutex.Unlock() l.lw = lw return nil } // IsReadWrite indicates if the lock file is a read-write lock. func (l *LockFile) IsReadWrite() bool { return !l.ro } // getLockFile returns a *LockFile object, possibly (depending on the platform) // working inter-process, and associated with the specified path. // // If ro, the lock is a read-write lock and the returned *LockFile should correspond to the // “lock for reading” (shared) operation; otherwise, the lock is either an exclusive lock, // or a read-write lock and *LockFile should correspond to the “lock for writing” (exclusive) operation. // // WARNING: // - The lock may or MAY NOT be inter-process. // - There may or MAY NOT be an actual object on the filesystem created for the specified path. // - Even if ro, the lock MAY be exclusive. func getLockfile(path string, ro bool) (*LockFile, error) { lockFilesLock.Lock() defer lockFilesLock.Unlock() if lockFiles == nil { lockFiles = make(map[string]*LockFile) } cleanPath, err := filepath.Abs(path) if err != nil { return nil, fmt.Errorf("ensuring that path %q is an absolute path: %w", path, err) } if lockFile, ok := lockFiles[cleanPath]; ok { if ro && lockFile.IsReadWrite() { return nil, fmt.Errorf("lock %q is not a read-only lock", cleanPath) } if !ro && !lockFile.IsReadWrite() { return nil, fmt.Errorf("lock %q is not a read-write lock", cleanPath) } return lockFile, nil } lockFile, err := createLockFileForPath(cleanPath, ro) // platform-dependent LockFile if err != nil { return nil, err } lockFiles[cleanPath] = lockFile return lockFile, nil } // createLockFileForPath returns new *LockFile object, possibly (depending on the platform) // working inter-process and associated with the specified path. // // This function will be called at most once for each path value within a single process. // // If ro, the lock is a read-write lock and the returned *LockFile should correspond to the // “lock for reading” (shared) operation; otherwise, the lock is either an exclusive lock, // or a read-write lock and *LockFile should correspond to the “lock for writing” (exclusive) operation. // // WARNING: // - The lock may or MAY NOT be inter-process. // - There may or MAY NOT be an actual object on the filesystem created for the specified path. // - Even if ro, the lock MAY be exclusive. func createLockFileForPath(path string, ro bool) (*LockFile, error) { // Check if we can open the lock. fd, err := openLock(path, ro) if err != nil { return nil, err } unlockAndCloseHandle(fd) lType := writeLock if ro { lType = readLock } return &LockFile{ file: path, ro: ro, rwMutex: &sync.RWMutex{}, stateMutex: &sync.Mutex{}, lw: newLastWrite(), // For compatibility, the first call of .Modified() will always report a change. lockType: lType, locked: false, }, nil } // openLock opens the file at path and returns the corresponding file // descriptor. The path is opened either read-only or read-write, // depending on the value of ro argument. // // openLock will create the file and its parent directories, // if necessary. func openLock(path string, ro bool) (fd fileHandle, err error) { flags := os.O_CREATE if ro { flags |= os.O_RDONLY } else { flags |= os.O_RDWR } fd, err = openHandle(path, flags) if err == nil { return fd, nil } // the directory of the lockfile seems to be removed, try to create it if os.IsNotExist(err) { if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { return fd, fmt.Errorf("creating lock file directory: %w", err) } return openLock(path, ro) } return fd, &os.PathError{Op: "open", Path: path, Err: err} } // lock locks the lockfile via syscall based on the specified type and // command. func (l *LockFile) lock(lType lockType) { if lType == readLock { l.rwMutex.RLock() } else { l.rwMutex.Lock() } l.stateMutex.Lock() defer l.stateMutex.Unlock() if l.counter == 0 { // If we're the first reference on the lock, we need to open the file again. fd, err := openLock(l.file, l.ro) if err != nil { panic(err) } l.fd = fd // Optimization: only use the (expensive) syscall when // the counter is 0. In this case, we're either the first // reader lock or a writer lock. lockHandle(l.fd, lType) } l.lockType = lType l.locked = true l.counter++ }