Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions pkg/filelocker/filelocker.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,15 +124,21 @@ func (lock fileUploadLock) Lock(ctx context.Context, requestRelease func()) erro

// If we are here, the lock is already held by another entity.
// We create the .stop file to signal the lock holder to release the lock.
// The handle is closed right away: the holder only checks the file's
// existence, and an open handle would block removal on Windows.
file, err := os.Create(lock.requestReleaseFile)
if err != nil {
return err
}
defer file.Close()
file.Close()

select {
case <-ctx.Done():
// Context expired, so we return a timeout
// Context expired, so we return a timeout. Since the lock was never
// successfully acquired, Unlock() will not be called and the .stop
// file would otherwise remain on disk indefinitely. Remove it here
// so future acquisition attempts are not affected by stale state.
_ = os.Remove(lock.requestReleaseFile)
return handler.ErrLockTimeout
case <-time.After(lock.acquirerPollInterval):
// Continue with the next attempt after a short delay
Expand Down
38 changes: 38 additions & 0 deletions pkg/filelocker/filelocker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,44 @@ func TestFileLocker_Timeout(t *testing.T) {
assertEmptyDirectory(dir, a)
}

func TestFileLocker_Timeout_StopFileCleanedUp(t *testing.T) {
// Regression test: when Lock() times out without ever successfully
// acquiring the lock, the .stop file it created to signal the holder
// must be cleaned up. Otherwise the .stop file remains on disk
// indefinitely, which is especially problematic on filesystems where
// flock() can return ErrBusy spuriously (e.g. SMB/CIFS) — subsequent
// retries observe stale state and fail deterministically.
a := assert.New(t)

dir, err := os.MkdirTemp("", "tusd-file-locker")
a.NoError(err)

locker := New(dir)
locker.AcquirerPollInterval = 10 * time.Millisecond

lock1, err := locker.NewLock("one")
a.NoError(err)
a.NoError(lock1.Lock(context.Background(), func() {}))

ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()

lock2, err := locker.NewLock("one")
a.NoError(err)
err = lock2.Lock(ctx, func() {
panic("must not be called")
})
a.Equal(err, handler.ErrLockTimeout)

// The .stop file created by lock2's contended acquisition must not
// linger on disk after the acquisition failed.
_, err = os.Stat(dir + "/one.stop")
a.True(os.IsNotExist(err), "stop file should have been removed after ErrLockTimeout, got err=%v", err)

a.NoError(lock1.Unlock())
assertEmptyDirectory(dir, a)
}

func TestFileLocker_RequestUnlock(t *testing.T) {
a := assert.New(t)

Expand Down
Loading