restic/internal/repository/lock_test.go

304 lines
8.7 KiB
Go
Raw Normal View History

2024-02-24 16:26:29 +01:00
package repository
import (
"context"
"fmt"
"runtime"
"strings"
2023-07-15 18:18:35 +02:00
"sync"
"testing"
"time"
"github.com/restic/restic/internal/backend"
"github.com/restic/restic/internal/backend/mem"
2023-07-15 18:18:35 +02:00
"github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/restic"
2024-02-24 16:26:29 +01:00
rtest "github.com/restic/restic/internal/test"
)
2024-02-24 16:26:29 +01:00
type backendWrapper func(r backend.Backend) (backend.Backend, error)
func openLockTestRepo(t *testing.T, wrapper backendWrapper) (*Repository, backend.Backend) {
2024-02-24 16:26:29 +01:00
be := backend.Backend(mem.New())
// initialize repo
TestRepositoryWithBackend(t, be, 0, Options{})
2024-02-24 16:26:29 +01:00
// reopen repository to allow injecting a backend wrapper
if wrapper != nil {
2024-02-24 16:26:29 +01:00
var err error
be, err = wrapper(be)
rtest.OK(t, err)
}
return TestOpenBackend(t, be), be
}
func checkedLockRepo(ctx context.Context, t *testing.T, repo *Repository, lockerInst *locker, retryLock time.Duration) (*Unlocker, context.Context) {
2024-02-24 17:07:14 +01:00
lock, wrappedCtx, err := lockerInst.Lock(ctx, repo, false, retryLock, func(msg string) {}, func(format string, args ...interface{}) {})
2025-01-05 12:53:20 +01:00
rtest.OK(t, err)
rtest.OK(t, wrappedCtx.Err())
if lock.info.lock.Stale() {
t.Fatal("lock returned stale lock")
}
return lock, wrappedCtx
}
func TestLock(t *testing.T) {
2024-02-24 17:07:14 +01:00
t.Parallel()
repo, _ := openLockTestRepo(t, nil)
2024-02-24 17:07:14 +01:00
lock, wrappedCtx := checkedLockRepo(context.Background(), t, repo, lockerInst, 0)
lock.Unlock()
if wrappedCtx.Err() == nil {
t.Fatal("unlock did not cancel context")
}
}
func TestLockCancel(t *testing.T) {
2024-02-24 17:07:14 +01:00
t.Parallel()
repo, _ := openLockTestRepo(t, nil)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
2024-02-24 17:07:14 +01:00
lock, wrappedCtx := checkedLockRepo(ctx, t, repo, lockerInst, 0)
cancel()
if wrappedCtx.Err() == nil {
t.Fatal("canceled parent context did not cancel context")
}
2024-02-24 16:26:29 +01:00
// Unlock should not crash
lock.Unlock()
}
func TestLockConflict(t *testing.T) {
2024-02-24 17:07:14 +01:00
t.Parallel()
repo, be := openLockTestRepo(t, nil)
repo2 := TestOpenBackend(t, be)
2024-02-24 16:26:29 +01:00
lock, _, err := Lock(context.Background(), repo, true, 0, func(msg string) {}, func(format string, args ...interface{}) {})
2025-01-05 12:53:20 +01:00
rtest.OK(t, err)
defer lock.Unlock()
2024-02-24 16:26:29 +01:00
_, _, err = Lock(context.Background(), repo2, false, 0, func(msg string) {}, func(format string, args ...interface{}) {})
if err == nil {
t.Fatal("second lock should have failed")
}
2025-01-05 12:53:20 +01:00
rtest.Assert(t, restic.IsAlreadyLocked(err), "unexpected error %v", err)
}
type writeOnceBackend struct {
backend.Backend
written bool
}
func (b *writeOnceBackend) Save(ctx context.Context, h backend.Handle, rd backend.RewindReader) error {
if b.written {
return fmt.Errorf("fail after first write")
}
b.written = true
return b.Backend.Save(ctx, h, rd)
}
func TestLockFailedRefresh(t *testing.T) {
2024-02-24 17:07:14 +01:00
t.Parallel()
repo, _ := openLockTestRepo(t, func(r backend.Backend) (backend.Backend, error) {
return &writeOnceBackend{Backend: r}, nil
})
// reduce locking intervals to be suitable for testing
2024-02-24 17:07:14 +01:00
li := &locker{
retrySleepStart: lockerInst.retrySleepStart,
retrySleepMax: lockerInst.retrySleepMax,
refreshInterval: 20 * time.Millisecond,
refreshabilityTimeout: 100 * time.Millisecond,
}
lock, wrappedCtx := checkedLockRepo(context.Background(), t, repo, li, 0)
select {
case <-wrappedCtx.Done():
// expected lock refresh failure
case <-time.After(time.Second):
t.Fatal("failed lock refresh did not cause context cancellation")
}
2024-02-24 16:26:29 +01:00
// Unlock should not crash
lock.Unlock()
}
type loggingBackend struct {
backend.Backend
t *testing.T
}
func (b *loggingBackend) Save(ctx context.Context, h backend.Handle, rd backend.RewindReader) error {
b.t.Logf("save %v @ %v", h, time.Now())
err := b.Backend.Save(ctx, h, rd)
b.t.Logf("save finished %v @ %v", h, time.Now())
return err
}
func TestLockSuccessfulRefresh(t *testing.T) {
2024-02-24 17:07:14 +01:00
t.Parallel()
repo, _ := openLockTestRepo(t, func(r backend.Backend) (backend.Backend, error) {
return &loggingBackend{
Backend: r,
t: t,
}, nil
})
t.Logf("test for successful lock refresh %v", time.Now())
// reduce locking intervals to be suitable for testing
2024-02-24 17:07:14 +01:00
li := &locker{
retrySleepStart: lockerInst.retrySleepStart,
retrySleepMax: lockerInst.retrySleepMax,
refreshInterval: 60 * time.Millisecond,
refreshabilityTimeout: 500 * time.Millisecond,
}
lock, wrappedCtx := checkedLockRepo(context.Background(), t, repo, li, 0)
select {
case <-wrappedCtx.Done():
// don't call t.Fatal to allow the lock to be properly cleaned up
t.Error("lock refresh failed", time.Now())
// Dump full stacktrace
buf := make([]byte, 1024*1024)
n := runtime.Stack(buf, true)
buf = buf[:n]
t.Log(string(buf))
2024-02-24 17:07:14 +01:00
case <-time.After(2 * li.refreshabilityTimeout):
// expected lock refresh to work
}
2024-02-24 16:26:29 +01:00
// Unlock should not crash
lock.Unlock()
}
2023-07-15 18:18:35 +02:00
type slowBackend struct {
backend.Backend
2023-07-15 18:18:35 +02:00
m sync.Mutex
sleep time.Duration
}
func (b *slowBackend) Save(ctx context.Context, h backend.Handle, rd backend.RewindReader) error {
2023-07-15 18:18:35 +02:00
b.m.Lock()
sleep := b.sleep
b.m.Unlock()
time.Sleep(sleep)
return b.Backend.Save(ctx, h, rd)
}
func TestLockSuccessfulStaleRefresh(t *testing.T) {
2024-02-24 17:07:14 +01:00
t.Parallel()
2023-07-15 18:18:35 +02:00
var sb *slowBackend
repo, _ := openLockTestRepo(t, func(r backend.Backend) (backend.Backend, error) {
2023-07-15 18:18:35 +02:00
sb = &slowBackend{Backend: r}
return sb, nil
})
t.Logf("test for successful lock refresh %v", time.Now())
// reduce locking intervals to be suitable for testing
2024-02-24 17:07:14 +01:00
li := &locker{
retrySleepStart: lockerInst.retrySleepStart,
retrySleepMax: lockerInst.retrySleepMax,
refreshInterval: 10 * time.Millisecond,
refreshabilityTimeout: 50 * time.Millisecond,
}
lock, wrappedCtx := checkedLockRepo(context.Background(), t, repo, li, 0)
2023-07-15 18:18:35 +02:00
// delay lock refreshing long enough that the lock would expire
sb.m.Lock()
2024-02-24 17:07:14 +01:00
sb.sleep = li.refreshabilityTimeout + li.refreshInterval
2023-07-15 18:18:35 +02:00
sb.m.Unlock()
select {
case <-wrappedCtx.Done():
// don't call t.Fatal to allow the lock to be properly cleaned up
t.Error("lock refresh failed", time.Now())
2024-02-24 17:07:14 +01:00
case <-time.After(li.refreshabilityTimeout):
2023-07-15 18:18:35 +02:00
}
// reset slow backend
sb.m.Lock()
sb.sleep = 0
sb.m.Unlock()
debug.Log("normal lock period has expired")
select {
case <-wrappedCtx.Done():
// don't call t.Fatal to allow the lock to be properly cleaned up
t.Error("lock refresh failed", time.Now())
2024-02-24 17:07:14 +01:00
case <-time.After(3 * li.refreshabilityTimeout):
2023-07-15 18:18:35 +02:00
// expected lock refresh to work
}
2024-02-24 16:26:29 +01:00
// Unlock should not crash
lock.Unlock()
2023-07-15 18:18:35 +02:00
}
func TestLockWaitTimeout(t *testing.T) {
t.Parallel()
repo, _ := openLockTestRepo(t, nil)
2024-02-24 16:26:29 +01:00
elock, _, err := Lock(context.TODO(), repo, true, 0, func(msg string) {}, func(format string, args ...interface{}) {})
2025-01-05 12:53:20 +01:00
rtest.OK(t, err)
defer elock.Unlock()
retryLock := 200 * time.Millisecond
start := time.Now()
_, _, err = Lock(context.TODO(), repo, false, retryLock, func(msg string) {}, func(format string, args ...interface{}) {})
duration := time.Since(start)
2025-01-05 12:53:20 +01:00
rtest.Assert(t, err != nil,
"create normal lock with exclusively locked repo didn't return an error")
2025-01-05 12:53:20 +01:00
rtest.Assert(t, strings.Contains(err.Error(), "repository is already locked exclusively"),
"create normal lock with exclusively locked repo didn't return the correct error")
2025-01-05 12:53:20 +01:00
rtest.Assert(t, retryLock <= duration && duration < retryLock*3/2,
"create normal lock with exclusively locked repo didn't wait for the specified timeout")
}
2023-07-09 15:15:43 +02:00
func TestLockWaitCancel(t *testing.T) {
t.Parallel()
repo, _ := openLockTestRepo(t, nil)
2024-02-24 16:26:29 +01:00
elock, _, err := Lock(context.TODO(), repo, true, 0, func(msg string) {}, func(format string, args ...interface{}) {})
2025-01-05 12:53:20 +01:00
rtest.OK(t, err)
defer elock.Unlock()
retryLock := 200 * time.Millisecond
cancelAfter := 40 * time.Millisecond
start := time.Now()
ctx, cancel := context.WithCancel(context.TODO())
time.AfterFunc(cancelAfter, cancel)
_, _, err = Lock(ctx, repo, false, retryLock, func(msg string) {}, func(format string, args ...interface{}) {})
duration := time.Since(start)
2025-01-05 12:53:20 +01:00
rtest.Assert(t, err != nil,
"create normal lock with exclusively locked repo didn't return an error")
2025-01-05 12:53:20 +01:00
rtest.Assert(t, strings.Contains(err.Error(), "context canceled"),
"create normal lock with exclusively locked repo didn't return the correct error")
2025-01-05 12:53:20 +01:00
rtest.Assert(t, cancelAfter <= duration && duration < retryLock-10*time.Millisecond,
"create normal lock with exclusively locked repo didn't return in time, duration %v", duration)
}
func TestLockWaitSuccess(t *testing.T) {
t.Parallel()
repo, _ := openLockTestRepo(t, nil)
2024-02-24 16:26:29 +01:00
elock, _, err := Lock(context.TODO(), repo, true, 0, func(msg string) {}, func(format string, args ...interface{}) {})
2025-01-05 12:53:20 +01:00
rtest.OK(t, err)
retryLock := 200 * time.Millisecond
unlockAfter := 40 * time.Millisecond
time.AfterFunc(unlockAfter, func() {
elock.Unlock()
})
2024-02-24 16:26:29 +01:00
lock, _, err := Lock(context.TODO(), repo, false, retryLock, func(msg string) {}, func(format string, args ...interface{}) {})
2025-01-05 12:53:20 +01:00
rtest.OK(t, err)
lock.Unlock()
}