@@ -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.
3842type CommitStore interface {
@@ -66,24 +70,28 @@ type CommitStore interface {
6670func 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
7580type commitStore struct {
7681 logger * slog.Logger
7782 bucket storage.ReadWriteBucket
83+ locker filelock.Locker
7884}
7985
8086func 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+
269339func (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