Skip to content

Commit dd0f838

Browse files
committed
pebble: lock wal directories on Open
Ensure that we acquire locks for WAL directories when they are configured. Fixes: #4919
1 parent c8cfc93 commit dd0f838

File tree

10 files changed

+403
-143
lines changed

10 files changed

+403
-143
lines changed

db.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -290,8 +290,8 @@ type DB struct {
290290
// objProvider is used to access and manage SSTs.
291291
objProvider objstorage.Provider
292292

293-
fileLock *Lock
294-
dataDir vfs.File
293+
dataDirLock *base.DirLock
294+
dataDir vfs.File
295295

296296
fileCache *fileCacheHandle
297297
newIters tableNewIters
@@ -1734,7 +1734,7 @@ func (d *DB) Close() error {
17341734
panic("pebble: log-writer should be nil in read-only mode")
17351735
}
17361736
err = firstError(err, d.mu.log.manager.Close())
1737-
err = firstError(err, d.fileLock.Close())
1737+
err = firstError(err, d.dataDirLock.Close())
17381738

17391739
// Note that versionSet.close() only closes the MANIFEST. The versions list
17401740
// is still valid for the checks below.

internal/base/directory_lock.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
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

Comments
 (0)