Skip to content

Commit 9e3337d

Browse files
authored
Add file locking around commit cache (#4395)
1 parent 9031f1b commit 9e3337d

4 files changed

Lines changed: 378 additions & 5 deletions

File tree

private/buf/bufcli/cache.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ var (
6161
v1beta1CacheModuleDataRelDirPath,
6262
v1beta1CacheModuleLockRelDirPath,
6363
v2CacheModuleRelDirPath,
64+
v3CacheCommitLockRelDirPath,
6465
v3CacheCommitsRelDirPath,
6566
v3CacheModuleLockRelDirPath,
6667
v3CacheModuleRelDirPath,
@@ -119,6 +120,11 @@ var (
119120
//
120121
// Normalized.
121122
v3CacheModuleLockRelDirPath = normalpath.Join("v3", "modulelocks")
123+
// v3CacheCommitLockRelDirPath is the relative path to the lock files directory for commit data.
124+
// This directory is used to store lock files for synchronizing reading and writing commit data from the cache.
125+
//
126+
// Normalized.
127+
v3CacheCommitLockRelDirPath = normalpath.Join("v3", "commitlocks")
122128
// v3CachePluginRelDirPath is the relative path to the files cache directory in its newest iteration.
123129
//
124130
// Normalized.
@@ -289,12 +295,20 @@ func newCommitProvider(
289295
if err != nil {
290296
return nil, err
291297
}
298+
if err := createCacheDir(container.CacheDirPath(), v3CacheCommitLockRelDirPath); err != nil {
299+
return nil, err
300+
}
301+
filelocker, err := filelock.NewLocker(normalpath.Join(container.CacheDirPath(), v3CacheCommitLockRelDirPath))
302+
if err != nil {
303+
return nil, err
304+
}
292305
return bufmodulecache.NewCommitProvider(
293306
container.Logger(),
294307
delegateReader,
295308
bufmodulestore.NewCommitStore(
296309
container.Logger(),
297310
cacheBucket,
311+
filelocker,
298312
),
299313
), nil
300314
}

private/bufpkg/bufmodule/bufmodulecache/bufmodulecache_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ func TestCommitProviderForModuleKeyBasic(t *testing.T) {
4848
bufmodulestore.NewCommitStore(
4949
logger,
5050
storagemem.NewReadWriteBucket(),
51+
filelock.NewNopLocker(),
5152
),
5253
)
5354

@@ -113,6 +114,7 @@ func TestCommitProviderForCommitKeyBasic(t *testing.T) {
113114
bufmodulestore.NewCommitStore(
114115
logger,
115116
storagemem.NewReadWriteBucket(),
117+
filelock.NewNopLocker(),
116118
),
117119
)
118120

private/bufpkg/bufmodule/bufmodulestore/commit_store.go

Lines changed: 85 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,17 @@ import (
2626
"buf.build/go/standard/xlog/xslog"
2727
"github.com/bufbuild/buf/private/bufpkg/bufmodule"
2828
"github.com/bufbuild/buf/private/bufpkg/bufparse"
29+
"github.com/bufbuild/buf/private/pkg/filelock"
2930
"github.com/bufbuild/buf/private/pkg/normalpath"
3031
"github.com/bufbuild/buf/private/pkg/storage"
3132
"github.com/bufbuild/buf/private/pkg/syserror"
3233
"github.com/bufbuild/buf/private/pkg/uuidutil"
3334
)
3435

35-
var externalCommitVersion = "v1"
36+
var (
37+
externalCommitVersion = "v1"
38+
externalCommitLockFileExt = ".lock"
39+
)
3640

3741
// CommitStore reads and writes Commits.
3842
type CommitStore interface {
@@ -66,24 +70,28 @@ type CommitStore interface {
6670
func NewCommitStore(
6771
logger *slog.Logger,
6872
bucket storage.ReadWriteBucket,
73+
locker filelock.Locker,
6974
) CommitStore {
70-
return newCommitStore(logger, bucket)
75+
return newCommitStore(logger, bucket, locker)
7176
}
7277

7378
/// *** PRIVATE ***
7479

7580
type commitStore struct {
7681
logger *slog.Logger
7782
bucket storage.ReadWriteBucket
83+
locker filelock.Locker
7884
}
7985

8086
func newCommitStore(
8187
logger *slog.Logger,
8288
bucket storage.ReadWriteBucket,
89+
locker filelock.Locker,
8390
) *commitStore {
8491
return &commitStore{
8592
logger: logger,
8693
bucket: bucket,
94+
locker: locker,
8795
}
8896
}
8997

@@ -155,7 +163,8 @@ func (p *commitStore) getCommitForCommitKey(
155163
) (_ bufmodule.Commit, retErr error) {
156164
bucket := p.getReadWriteBucketForDir(ctx, commitKey)
157165
path := getCommitStoreFilePath(commitKey)
158-
data, err := storage.ReadPath(ctx, bucket, path)
166+
registryLockPath := getCommitStoreLockPath(commitKey)
167+
data, err := p.readCommitData(ctx, bucket, path, registryLockPath)
159168
p.logDebugCommitKey(
160169
ctx,
161170
commitKey,
@@ -179,7 +188,7 @@ func (p *commitStore) getCommitForCommitKey(
179188
}
180189
if !externalCommit.isValid() {
181190
invalidReason = "invalid"
182-
return nil, err
191+
return nil, fmt.Errorf("invalid commit cache entry for %s", path)
183192
}
184193
digest, err := bufmodule.ParseDigest(externalCommit.Digest)
185194
if err != nil {
@@ -188,7 +197,7 @@ func (p *commitStore) getCommitForCommitKey(
188197
}
189198
if commitKey.DigestType() != digest.Type() {
190199
invalidReason = "mismatched digest type"
191-
return nil, err
200+
return nil, fmt.Errorf("mismatched digest type for %s: got %s, expected %s", path, digest.Type(), commitKey.DigestType())
192201
}
193202
moduleFullName, err := bufparse.NewFullName(
194203
commitKey.Registry(),
@@ -238,6 +247,29 @@ func (p *commitStore) putCommit(
238247
}
239248
bucket := p.getReadWriteBucketForDir(ctx, commitKey)
240249
path := getCommitStoreFilePath(commitKey)
250+
registryLockPath := getCommitStoreLockPath(commitKey)
251+
// Check if the commit already exists under a shared lock before acquiring
252+
// an exclusive lock.
253+
exists, err := p.commitExistsUnderRLock(ctx, bucket, path, registryLockPath)
254+
if err != nil {
255+
return err
256+
}
257+
if exists {
258+
return nil
259+
}
260+
unlocker, err := p.locker.Lock(ctx, registryLockPath)
261+
if err != nil {
262+
return err
263+
}
264+
defer func() {
265+
retErr = errors.Join(retErr, unlocker.Unlock())
266+
}()
267+
// Re-check after acquiring the exclusive lock, as another process may have
268+
// written the commit between releasing the shared lock and acquiring the
269+
// exclusive lock.
270+
if _, err := bucket.Stat(ctx, path); err == nil {
271+
return nil
272+
}
241273
externalCommit := externalCommit{
242274
Version: externalCommitVersion,
243275
Owner: moduleKey.FullName().Owner(),
@@ -266,6 +298,44 @@ func (p *commitStore) getReadWriteBucketForDir(ctx context.Context, commitKey bu
266298
return storage.MapReadWriteBucket(p.bucket, storage.MapOnPrefix(dirPath))
267299
}
268300

301+
func (p *commitStore) readCommitData(
302+
ctx context.Context,
303+
bucket storage.ReadBucket,
304+
path string,
305+
registryLockPath string,
306+
) (_ []byte, retErr error) {
307+
unlocker, err := p.locker.RLock(ctx, registryLockPath)
308+
if err != nil {
309+
return nil, err
310+
}
311+
defer func() {
312+
retErr = errors.Join(retErr, unlocker.Unlock())
313+
}()
314+
return storage.ReadPath(ctx, bucket, path)
315+
}
316+
317+
func (p *commitStore) commitExistsUnderRLock(
318+
ctx context.Context,
319+
bucket storage.ReadBucket,
320+
path string,
321+
registryLockPath string,
322+
) (_ bool, retErr error) {
323+
unlocker, err := p.locker.RLock(ctx, registryLockPath)
324+
if err != nil {
325+
return false, err
326+
}
327+
defer func() {
328+
retErr = errors.Join(retErr, unlocker.Unlock())
329+
}()
330+
if _, err := bucket.Stat(ctx, path); err != nil {
331+
if errors.Is(err, fs.ErrNotExist) {
332+
return false, nil
333+
}
334+
return false, err
335+
}
336+
return true, nil
337+
}
338+
269339
func (p *commitStore) deleteInvalidCommitFile(
270340
ctx context.Context,
271341
commitKey bufmodule.CommitKey,
@@ -317,6 +387,16 @@ func getCommitStoreFilePath(commitKey bufmodule.CommitKey) string {
317387
return uuidutil.ToDashless(commitKey.CommitID()) + ".json"
318388
}
319389

390+
// Returns the lock file path for the commit's registry.
391+
//
392+
// This is "digestType/registry.lock", e.g. "shake256/buf.build.lock".
393+
func getCommitStoreLockPath(commitKey bufmodule.CommitKey) string {
394+
return normalpath.Join(
395+
commitKey.DigestType().String(),
396+
commitKey.Registry()+externalCommitLockFileExt,
397+
)
398+
}
399+
320400
// externalCommit is the store representation of a Commit.
321401
//
322402
// We could use a protobuf Message for this.

0 commit comments

Comments
 (0)