|
| 1 | +// Copyright 2025 The LevelDB-Go and Pebble Authors. All rights reserved. Use |
| 2 | +// of this source code is governed by a BSD-style license that can be found in |
| 3 | +// the LICENSE file. |
| 4 | + |
| 5 | +package base |
| 6 | + |
| 7 | +import ( |
| 8 | + "io" |
| 9 | + "os" |
| 10 | + "sync/atomic" |
| 11 | + |
| 12 | + "github.com/cockroachdb/errors" |
| 13 | + "github.com/cockroachdb/pebble/internal/invariants" |
| 14 | + "github.com/cockroachdb/pebble/vfs" |
| 15 | +) |
| 16 | + |
| 17 | +// AcquireOrValidateDirectoryLock attempts to acquire a lock on the |
| 18 | +// provided directory, or validates a pre-acquired DirLock. |
| 19 | +func AcquireOrValidateDirectoryLock( |
| 20 | + preAcquiredLock *DirLock, dirname string, fs vfs.FS, |
| 21 | +) (*DirLock, error) { |
| 22 | + // If a pre-acquired lock is provided, check that it matches the directory |
| 23 | + // we're trying to open. |
| 24 | + if preAcquiredLock != nil { |
| 25 | + if err := preAcquiredLock.pathMatches(dirname); err != nil { |
| 26 | + return preAcquiredLock, err |
| 27 | + } |
| 28 | + return preAcquiredLock, preAcquiredLock.refForOpen() |
| 29 | + } |
| 30 | + |
| 31 | + // Otherwise, acquire the lock for the directory. |
| 32 | + return LockDirectory(dirname, fs) |
| 33 | +} |
| 34 | + |
| 35 | +// LockDirectory acquires the directory lock in the named directory, preventing |
| 36 | +// another process from opening the database. LockDirectory returns a |
| 37 | +// handle to the held lock that may be passed to Open, skipping lock acquisition |
| 38 | +// during Open. |
| 39 | +// |
| 40 | +// LockDirectory may be used to expand the critical section protected by the |
| 41 | +// database lock to include setup before the call to Open. |
| 42 | +func LockDirectory(dirname string, fs vfs.FS) (*DirLock, error) { |
| 43 | + fileLock, err := fs.Lock(MakeFilepath(fs, dirname, FileTypeLock, DiskFileNum(0))) |
| 44 | + if err != nil { |
| 45 | + return nil, err |
| 46 | + } |
| 47 | + l := &DirLock{dirname: dirname, fileLock: fileLock} |
| 48 | + l.refs.Store(1) |
| 49 | + invariants.SetFinalizer(l, func(obj interface{}) { |
| 50 | + if refs := obj.(*DirLock).refs.Load(); refs > 0 { |
| 51 | + panic(errors.AssertionFailedf("lock for %q finalized with %d refs", dirname, refs)) |
| 52 | + } |
| 53 | + }) |
| 54 | + return l, nil |
| 55 | +} |
| 56 | + |
| 57 | +// DirLock represents a file lock on a directory. It may be passed to Open through |
| 58 | +// Options.Lock to elide lock aquisition during Open. |
| 59 | +type DirLock struct { |
| 60 | + dirname string |
| 61 | + fileLock io.Closer |
| 62 | + // refs is a count of the number of handles on the lock. refs must be 0, 1 |
| 63 | + // or 2. |
| 64 | + // |
| 65 | + // When acquired by the client and passed to Open, refs = 1 and the Open |
| 66 | + // call increments it to 2. When the database is closed, it's decremented to |
| 67 | + // 1. Finally when the original caller, calls Close on the Lock, it's |
| 68 | + // drecemented to zero and the underlying file lock is released. |
| 69 | + // |
| 70 | + // When Open acquires the file lock, refs remains at 1 until the database is |
| 71 | + // closed. |
| 72 | + refs atomic.Int32 |
| 73 | +} |
| 74 | + |
| 75 | +func (l *DirLock) refForOpen() error { |
| 76 | + // During Open, when a user passed in a lock, the reference count must be |
| 77 | + // exactly 1. If it's zero, the lock is no longer held and is invalid. If |
| 78 | + // it's 2, the lock is already in use by another database within the |
| 79 | + // process. |
| 80 | + if !l.refs.CompareAndSwap(1, 2) { |
| 81 | + return errors.Errorf("pebble: unexpected Lock reference count; is the lock already in use?") |
| 82 | + } |
| 83 | + return nil |
| 84 | +} |
| 85 | + |
| 86 | +func (l *DirLock) Refs() int { |
| 87 | + // Return the current reference count. This is used for testing purposes. |
| 88 | + return int(l.refs.Load()) |
| 89 | +} |
| 90 | + |
| 91 | +// Close releases the lock, permitting another process to lock and open the |
| 92 | +// database. Close must not be called until after a database using the Lock has |
| 93 | +// been closed. |
| 94 | +func (l *DirLock) Close() error { |
| 95 | + if l.refs.Add(-1) > 0 { |
| 96 | + return nil |
| 97 | + } |
| 98 | + defer func() { l.fileLock = nil }() |
| 99 | + return l.fileLock.Close() |
| 100 | +} |
| 101 | + |
| 102 | +func (l *DirLock) pathMatches(dirname string) error { |
| 103 | + if dirname == l.dirname { |
| 104 | + return nil |
| 105 | + } |
| 106 | + // Check for relative paths, symlinks, etc. This isn't ideal because we're |
| 107 | + // circumventing the vfs.FS interface here. |
| 108 | + // |
| 109 | + // TODO(jackson): We could add support for retrieving file inodes through Stat |
| 110 | + // calls in the VFS interface on platforms where it's available and use that |
| 111 | + // to differentiate. |
| 112 | + dirStat, err1 := os.Stat(dirname) |
| 113 | + lockDirStat, err2 := os.Stat(l.dirname) |
| 114 | + if err1 == nil && err2 == nil && os.SameFile(dirStat, lockDirStat) { |
| 115 | + return nil |
| 116 | + } |
| 117 | + return errors.Join( |
| 118 | + errors.Newf("pebble: opts.Lock acquired in %q not %q", l.dirname, dirname), |
| 119 | + err1, err2) |
| 120 | +} |
0 commit comments