repository: move lock file handling from restic package

This commit is contained in:
Michael Eischer
2026-06-13 21:31:23 +02:00
parent 7015da44ad
commit b892b1a150
10 changed files with 83 additions and 90 deletions
+10 -10
View File
@@ -14,7 +14,7 @@ import (
)
type lockContext struct {
lock *restic.Lock
lock *Lock
cancel context.CancelFunc
refreshWG sync.WaitGroup
}
@@ -34,7 +34,7 @@ var lockerInst = &locker{
refreshInterval: defaultRefreshInterval,
// consider a lock refresh failed a bit before the lock actually becomes stale
// the difference allows to compensate for a small time drift between clients.
refreshabilityTimeout: restic.StaleLockTimeout - defaultRefreshInterval*3/2,
refreshabilityTimeout: StaleLockTimeout - defaultRefreshInterval*3/2,
}
func LockRepo(ctx context.Context, repo *Repository, exclusive bool, retryLock time.Duration, printRetry func(msg string), logger func(format string, args ...interface{})) (Unlocker, context.Context, error) {
@@ -43,8 +43,8 @@ func LockRepo(ctx context.Context, repo *Repository, exclusive bool, retryLock t
// Lock wraps the ctx such that it is cancelled when the repository is unlocked
// cancelling the original context also stops the lock refresh
func (l *locker) Lock(ctx context.Context, r *Repository, exclusive bool, retryLock time.Duration, printRetry func(msg string), logger func(format string, args ...interface{})) (Unlocker, context.Context, error) {
var lock *restic.Lock
func (l *locker) Lock(ctx context.Context, r *Repository, exclusive bool, retryLock time.Duration, printRetry func(msg string), logger func(format string, args ...interface{})) (*unlocker, context.Context, error) {
var lock *Lock
var err error
retrySleep := minDuration(l.retrySleepStart, retryLock)
@@ -55,8 +55,8 @@ func (l *locker) Lock(ctx context.Context, r *Repository, exclusive bool, retryL
retryLoop:
for {
lock, err = restic.NewLock(ctx, repo, exclusive)
if err != nil && restic.IsAlreadyLocked(err) {
lock, err = NewLock(ctx, repo, exclusive)
if err != nil && IsAlreadyLocked(err) {
if !retryMessagePrinted {
printRetry(fmt.Sprintf("repo already locked, waiting up to %s for the lock\n", retryLock))
@@ -72,7 +72,7 @@ retryLoop:
case <-retryTimeout:
debug.Log("repo already locked, timeout expired")
// Last lock attempt
lock, err = restic.NewLock(ctx, repo, exclusive)
lock, err = NewLock(ctx, repo, exclusive)
break retryLoop
case <-retrySleepCh:
retrySleep = minDuration(retrySleep*2, l.retrySleepMax)
@@ -82,7 +82,7 @@ retryLoop:
break retryLoop
}
}
if restic.IsInvalidLock(err) {
if IsInvalidLock(err) {
return nil, ctx, errors.Fatalf("%v\n\nthe `unlock --remove-all` command can be used to remove invalid locks. Make sure that no other restic process is accessing the repository when running the command", err)
}
if err != nil {
@@ -242,7 +242,7 @@ func (l *locker) monitorLockRefresh(ctx context.Context, lockInfo *lockContext,
}
}
func tryRefreshStaleLock(ctx context.Context, be backend.Backend, lock *restic.Lock, cancel context.CancelFunc, logger func(format string, args ...interface{})) bool {
func tryRefreshStaleLock(ctx context.Context, be backend.Backend, lock *Lock, cancel context.CancelFunc, logger func(format string, args ...interface{})) bool {
freeze := backend.AsBackend[backend.FreezeBackend](be)
if freeze != nil {
debug.Log("freezing backend")
@@ -277,7 +277,7 @@ func (l *unlocker) Unlock() {
// RemoveStaleLocks deletes all locks detected as stale from the repository.
func RemoveStaleLocks(ctx context.Context, repo *Repository) (uint, error) {
var processed uint
err := restic.ForAllLocks(ctx, repo, nil, func(id restic.ID, lock *restic.Lock, err error) error {
err := ForAllLocks(ctx, repo, nil, func(id restic.ID, lock *Lock, err error) error {
if err != nil {
// ignore locks that cannot be loaded
debug.Log("ignore lock %v: %v", id, err)
+460
View File
@@ -0,0 +1,460 @@
package repository
import (
"context"
"fmt"
"os"
"os/signal"
"os/user"
"sync"
"syscall"
"testing"
"time"
"github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/restic"
)
// UnlockCancelDelay bounds the duration how long lock cleanup operations will wait
// if the passed in context was canceled.
const UnlockCancelDelay = 1 * time.Minute
// Lock represents a process locking the repository for an operation.
//
// There are two types of locks: exclusive and non-exclusive. There may be many
// different non-exclusive locks, but at most one exclusive lock, which can
// only be acquired while no non-exclusive lock is held.
//
// A lock must be refreshed regularly to not be considered stale, this must be
// triggered by regularly calling Refresh.
type Lock struct {
lock sync.Mutex
Time time.Time `json:"time"`
Exclusive bool `json:"exclusive"`
Hostname string `json:"hostname"`
Username string `json:"username"`
PID int `json:"pid"`
UID uint32 `json:"uid,omitempty"`
GID uint32 `json:"gid,omitempty"`
repo restic.Unpacked[restic.FileType]
lockID *restic.ID
}
// alreadyLockedError is returned when NewLock or NewExclusiveLock are unable to
// acquire the desired lock.
type alreadyLockedError struct {
otherLock *Lock
}
func (e *alreadyLockedError) Error() string {
s := ""
if e.otherLock.Exclusive {
s = "exclusively "
}
return fmt.Sprintf("repository is already locked %sby %v", s, e.otherLock)
}
// IsAlreadyLocked returns true iff err indicates that a repository is
// already locked.
func IsAlreadyLocked(err error) bool {
var e *alreadyLockedError
return errors.As(err, &e)
}
// invalidLockError is returned when NewLock or NewExclusiveLock fail due
// to an invalid lock.
type invalidLockError struct {
err error
}
func (e *invalidLockError) Error() string {
return fmt.Sprintf("invalid lock file: %v", e.err)
}
func (e *invalidLockError) Unwrap() error {
return e.err
}
// IsInvalidLock returns true iff err indicates that locking failed due to
// an invalid lock.
func IsInvalidLock(err error) bool {
var e *invalidLockError
return errors.As(err, &e)
}
var ErrRemovedLock = errors.New("lock file was removed in the meantime")
var waitBeforeLockCheck = 200 * time.Millisecond
// delay increases by factor 2 on each retry
var initialWaitBetweenLockRetries = 5 * time.Second
// TestSetLockTimeout can be used to reduce the lock wait timeout for tests.
func TestSetLockTimeout(t testing.TB, d time.Duration) {
t.Logf("setting lock timeout to %v", d)
waitBeforeLockCheck = d
initialWaitBetweenLockRetries = d
}
// NewLock returns a new lock for the repository. If an
// exclusive lock is already held by another process, it returns an error
// that satisfies IsAlreadyLocked. If the new lock is exclude, then other
// non-exclusive locks also result in an IsAlreadyLocked error.
func NewLock(ctx context.Context, repo restic.Unpacked[restic.FileType], exclusive bool) (*Lock, error) {
lock := &Lock{
Time: time.Now(),
PID: os.Getpid(),
Exclusive: exclusive,
repo: repo,
}
hn, err := os.Hostname()
if err == nil {
lock.Hostname = hn
}
if err = lock.fillUserInfo(); err != nil {
return nil, err
}
if err = lock.checkForOtherLocks(ctx); err != nil {
return nil, err
}
lockID, err := lock.createLock(ctx)
if err != nil {
return nil, err
}
lock.lockID = &lockID
time.Sleep(waitBeforeLockCheck)
if err = lock.checkForOtherLocks(ctx); err != nil {
_ = lock.Unlock(ctx)
return nil, err
}
return lock, nil
}
func (l *Lock) fillUserInfo() error {
usr, err := user.Current()
if err != nil {
return nil
}
l.Username = usr.Username
l.UID, l.GID, err = restic.UidGidInt(usr)
return err
}
// checkForOtherLocks looks for other locks that currently exist in the repository.
//
// If an exclusive lock is to be created, checkForOtherLocks returns an error
// if there are any other locks, regardless if exclusive or not. If a
// non-exclusive lock is to be created, an error is only returned when an
// exclusive lock is found.
func (l *Lock) checkForOtherLocks(ctx context.Context) error {
var err error
checkedIDs := restic.NewIDSet()
if l.lockID != nil {
checkedIDs.Insert(*l.lockID)
}
delay := initialWaitBetweenLockRetries
// retry locking a few times
for i := 0; i < 4; i++ {
if i != 0 {
// sleep between retries to give backend some time to settle
if err := cancelableDelay(ctx, delay); err != nil {
return err
}
delay *= 2
}
// Store updates in new IDSet to prevent data races
var m sync.Mutex
newCheckedIDs := checkedIDs.Clone()
err = ForAllLocks(ctx, l.repo, checkedIDs, func(id restic.ID, lock *Lock, err error) error {
if err != nil {
// if we cannot load a lock then it is unclear whether it can be ignored
// it could either be invalid or just unreadable due to network/permission problems
debug.Log("ignore lock %v: %v", id, err)
return err
}
if l.Exclusive {
return &alreadyLockedError{otherLock: lock}
}
if !l.Exclusive && lock.Exclusive {
return &alreadyLockedError{otherLock: lock}
}
// valid locks will remain valid
m.Lock()
newCheckedIDs.Insert(id)
m.Unlock()
return nil
})
checkedIDs = newCheckedIDs
// no lock detected
if err == nil {
return nil
}
// lock conflicts are permanent
if _, ok := err.(*alreadyLockedError); ok {
return err
}
}
if errors.Is(err, restic.ErrInvalidData) {
return &invalidLockError{err}
}
return err
}
func cancelableDelay(ctx context.Context, delay time.Duration) error {
// delay next try a bit
timer := time.NewTimer(delay)
select {
case <-ctx.Done():
timer.Stop()
return ctx.Err()
case <-timer.C:
}
return nil
}
// createLock acquires the lock by creating a file in the repository.
func (l *Lock) createLock(ctx context.Context) (restic.ID, error) {
id, err := restic.SaveJSONUnpacked(ctx, l.repo, restic.LockFile, l)
if err != nil {
return restic.ID{}, err
}
return id, nil
}
// Unlock removes the lock from the repository.
func (l *Lock) Unlock(ctx context.Context) error {
if l == nil || l.lockID == nil {
return nil
}
ctx, cancel := delayedCancelContext(ctx, UnlockCancelDelay)
defer cancel()
return l.repo.RemoveUnpacked(ctx, restic.LockFile, *l.lockID)
}
var StaleLockTimeout = 30 * time.Minute
// Stale returns true if the lock is stale. A lock is stale if the timestamp is
// older than 30 minutes or if it was created on the current machine and the
// process isn't alive any more.
func (l *Lock) Stale() bool {
l.lock.Lock()
defer l.lock.Unlock()
debug.Log("testing if lock %v for process %d is stale", l.lockID, l.PID)
if time.Since(l.Time) > StaleLockTimeout {
debug.Log("lock is stale, timestamp is too old: %v\n", l.Time)
return true
}
hn, err := os.Hostname()
if err != nil {
debug.Log("unable to find current hostname: %v", err)
// since we cannot find the current hostname, assume that the lock is
// not stale.
return false
}
if hn != l.Hostname {
// lock was created on a different host, assume the lock is not stale.
return false
}
// check if we can reach the process retaining the lock
exists := l.processExists()
if !exists {
debug.Log("could not reach process, %d, lock is probably stale\n", l.PID)
return true
}
debug.Log("lock not stale\n")
return false
}
func delayedCancelContext(parentCtx context.Context, delay time.Duration) (context.Context, context.CancelFunc) {
ctx, cancel := context.WithCancel(context.Background())
go func() {
select {
case <-parentCtx.Done():
case <-ctx.Done():
return
}
time.Sleep(delay)
cancel()
}()
return ctx, cancel
}
// Refresh refreshes the lock by creating a new file in the backend with a new
// timestamp. Afterwards the old lock is removed.
func (l *Lock) Refresh(ctx context.Context) error {
debug.Log("refreshing lock %v", l.lockID)
l.lock.Lock()
l.Time = time.Now()
l.lock.Unlock()
id, err := l.createLock(ctx)
if err != nil {
return err
}
l.lock.Lock()
defer l.lock.Unlock()
debug.Log("new lock ID %v", id)
oldLockID := l.lockID
l.lockID = &id
ctx, cancel := delayedCancelContext(ctx, UnlockCancelDelay)
defer cancel()
return l.repo.RemoveUnpacked(ctx, restic.LockFile, *oldLockID)
}
// RefreshStaleLock is an extended variant of Refresh that can also refresh stale lock files.
func (l *Lock) RefreshStaleLock(ctx context.Context) error {
debug.Log("refreshing stale lock %v", l.lockID)
// refreshing a stale lock is possible if it still exists and continues to do
// so until after creating a new lock. The initial check avoids creating a new
// lock file if this lock was already removed in the meantime.
exists, err := l.checkExistence(ctx)
if err != nil {
return err
} else if !exists {
return ErrRemovedLock
}
l.lock.Lock()
l.Time = time.Now()
l.lock.Unlock()
id, err := l.createLock(ctx)
if err != nil {
return err
}
time.Sleep(waitBeforeLockCheck)
exists, err = l.checkExistence(ctx)
ctx, cancel := delayedCancelContext(ctx, UnlockCancelDelay)
defer cancel()
if err != nil {
// cleanup replacement lock
_ = l.repo.RemoveUnpacked(ctx, restic.LockFile, id)
return err
}
if !exists {
// cleanup replacement lock
_ = l.repo.RemoveUnpacked(ctx, restic.LockFile, id)
return ErrRemovedLock
}
l.lock.Lock()
defer l.lock.Unlock()
debug.Log("new lock ID %v", id)
oldLockID := l.lockID
l.lockID = &id
return l.repo.RemoveUnpacked(ctx, restic.LockFile, *oldLockID)
}
func (l *Lock) checkExistence(ctx context.Context) (bool, error) {
l.lock.Lock()
defer l.lock.Unlock()
exists := false
err := l.repo.List(ctx, restic.LockFile, func(id restic.ID, _ int64) error {
if id.Equal(*l.lockID) {
exists = true
}
return nil
})
return exists, err
}
func (l *Lock) String() string {
l.lock.Lock()
defer l.lock.Unlock()
text := fmt.Sprintf("PID %d on %s by %s (UID %d, GID %d)\nlock was created at %s (%s ago)\nstorage ID %v",
l.PID, l.Hostname, l.Username, l.UID, l.GID,
l.Time.Format("2006-01-02 15:04:05"), time.Since(l.Time),
l.lockID.Str())
return text
}
// listen for incoming SIGHUP and ignore
var ignoreSIGHUP sync.Once
func init() {
ignoreSIGHUP.Do(func() {
go func() {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGHUP)
for s := range c {
debug.Log("Signal received: %v\n", s)
}
}()
})
}
// LoadLock loads and unserializes a lock from a repository.
func LoadLock(ctx context.Context, repo restic.LoaderUnpacked, id restic.ID) (*Lock, error) {
lock := &Lock{}
if err := restic.LoadJSONUnpacked(ctx, repo, restic.LockFile, id, lock); err != nil {
return nil, err
}
lock.lockID = &id
return lock, nil
}
// ForAllLocks reads all locks in parallel and calls the given callback.
// It is guaranteed that the function is not run concurrently. If the
// callback returns an error, this function is cancelled and also returns that error.
// If a lock ID is passed via excludeID, it will be ignored.
func ForAllLocks(ctx context.Context, repo restic.ListerLoaderUnpacked, excludeIDs restic.IDSet, fn func(restic.ID, *Lock, error) error) error {
var m sync.Mutex
// For locks decoding is nearly for free, thus just assume were only limited by IO
return restic.ParallelList(ctx, repo, restic.LockFile, repo.Connections(), func(ctx context.Context, id restic.ID, size int64) error {
if excludeIDs.Has(id) {
return nil
}
if size == 0 {
// Ignore empty lock files as some backends do not guarantee atomic uploads.
// These may leave empty files behind if an upload was interrupted between
// creating the file and writing its data.
return nil
}
lock, err := LoadLock(ctx, repo, id)
m.Lock()
defer m.Unlock()
return fn(id, lock, err)
})
}
+246
View File
@@ -0,0 +1,246 @@
package repository
import (
"context"
"fmt"
"io"
"os"
"testing"
"time"
"github.com/restic/restic/internal/backend"
"github.com/restic/restic/internal/backend/mem"
"github.com/restic/restic/internal/restic"
rtest "github.com/restic/restic/internal/test"
)
func TestLockFile(t *testing.T) {
repo := TestRepository(t)
TestSetLockTimeout(t, 5*time.Millisecond)
lock, err := NewLock(context.TODO(), &internalRepository{repo}, false)
rtest.OK(t, err)
rtest.OK(t, lock.Unlock(context.TODO()))
}
func TestDoubleUnlock(t *testing.T) {
repo := TestRepository(t)
TestSetLockTimeout(t, 5*time.Millisecond)
lock, err := NewLock(context.TODO(), &internalRepository{repo}, false)
rtest.OK(t, err)
rtest.OK(t, lock.Unlock(context.TODO()))
err = lock.Unlock(context.TODO())
rtest.Assert(t, err != nil,
"double unlock didn't return an error, got %v", err)
}
func TestMultipleLock(t *testing.T) {
repo := TestRepository(t)
TestSetLockTimeout(t, 5*time.Millisecond)
lock1, err := NewLock(context.TODO(), &internalRepository{repo}, false)
rtest.OK(t, err)
lock2, err := NewLock(context.TODO(), &internalRepository{repo}, false)
rtest.OK(t, err)
rtest.OK(t, lock1.Unlock(context.TODO()))
rtest.OK(t, lock2.Unlock(context.TODO()))
}
type failLockLoadingBackend struct {
backend.Backend
}
func (be *failLockLoadingBackend) Load(ctx context.Context, h backend.Handle, length int, offset int64, fn func(rd io.Reader) error) error {
if h.Type == restic.LockFile {
return fmt.Errorf("error loading lock")
}
return be.Backend.Load(ctx, h, length, offset, fn)
}
func TestMultipleLockFailure(t *testing.T) {
be := &failLockLoadingBackend{Backend: mem.New()}
repo, _ := TestRepositoryWithBackend(t, be, 0, Options{})
TestSetLockTimeout(t, 5*time.Millisecond)
lock1, err := NewLock(context.TODO(), &internalRepository{repo}, false)
rtest.OK(t, err)
_, err = NewLock(context.TODO(), &internalRepository{repo}, false)
rtest.Assert(t, err != nil, "unreadable lock file did not result in an error")
rtest.OK(t, lock1.Unlock(context.TODO()))
}
func TestLockExclusive(t *testing.T) {
repo := TestRepository(t)
elock, err := NewLock(context.TODO(), &internalRepository{repo}, true)
rtest.OK(t, err)
rtest.OK(t, elock.Unlock(context.TODO()))
}
func TestLockOnExclusiveLockedRepo(t *testing.T) {
repo := TestRepository(t)
TestSetLockTimeout(t, 5*time.Millisecond)
elock, err := NewLock(context.TODO(), &internalRepository{repo}, true)
rtest.OK(t, err)
lock, err := NewLock(context.TODO(), &internalRepository{repo}, false)
rtest.Assert(t, err != nil,
"create normal lock with exclusively locked repo didn't return an error")
rtest.Assert(t, IsAlreadyLocked(err),
"create normal lock with exclusively locked repo didn't return the correct error")
rtest.OK(t, lock.Unlock(context.TODO()))
rtest.OK(t, elock.Unlock(context.TODO()))
}
func TestExclusiveLockOnLockedRepo(t *testing.T) {
repo := TestRepository(t)
TestSetLockTimeout(t, 5*time.Millisecond)
elock, err := NewLock(context.TODO(), &internalRepository{repo}, false)
rtest.OK(t, err)
lock, err := NewLock(context.TODO(), &internalRepository{repo}, true)
rtest.Assert(t, err != nil,
"create normal lock with exclusively locked repo didn't return an error")
rtest.Assert(t, IsAlreadyLocked(err),
"create normal lock with exclusively locked repo didn't return the correct error")
rtest.OK(t, lock.Unlock(context.TODO()))
rtest.OK(t, elock.Unlock(context.TODO()))
}
var staleLockTests = []struct {
timestamp time.Time
stale bool
staleOnOtherHost bool
pid int
}{
{
timestamp: time.Now(),
stale: false,
staleOnOtherHost: false,
pid: os.Getpid(),
},
{
timestamp: time.Now().Add(-time.Hour),
stale: true,
staleOnOtherHost: true,
pid: os.Getpid(),
},
{
timestamp: time.Now().Add(3 * time.Minute),
stale: false,
staleOnOtherHost: false,
pid: os.Getpid(),
},
{
timestamp: time.Now(),
stale: true,
staleOnOtherHost: false,
pid: os.Getpid() + 500000,
},
}
func TestLockStale(t *testing.T) {
hostname, err := os.Hostname()
rtest.OK(t, err)
otherHostname := "other-" + hostname
for i, test := range staleLockTests {
lock := Lock{
Time: test.timestamp,
PID: test.pid,
Hostname: hostname,
}
rtest.Assert(t, lock.Stale() == test.stale,
"TestStaleLock: test %d failed: expected stale: %v, got %v",
i, test.stale, !test.stale)
lock.Hostname = otherHostname
rtest.Assert(t, lock.Stale() == test.staleOnOtherHost,
"TestStaleLock: test %d failed: expected staleOnOtherHost: %v, got %v",
i, test.staleOnOtherHost, !test.staleOnOtherHost)
}
}
func checkSingleLock(t *testing.T, repo restic.Lister) restic.ID {
t.Helper()
var lockID *restic.ID
err := repo.List(context.TODO(), restic.LockFile, func(id restic.ID, size int64) error {
if lockID != nil {
t.Error("more than one lock found")
}
lockID = &id
return nil
})
if err != nil {
t.Fatal(err)
}
if lockID == nil {
t.Fatal("no lock found")
}
return *lockID
}
func testLockRefresh(t *testing.T, refresh func(lock *Lock) error) {
repo := TestRepository(t)
TestSetLockTimeout(t, 5*time.Millisecond)
lock, err := NewLock(context.TODO(), &internalRepository{repo}, false)
rtest.OK(t, err)
time0 := lock.Time
lockID := checkSingleLock(t, repo)
time.Sleep(time.Millisecond)
rtest.OK(t, refresh(lock))
lockID2 := checkSingleLock(t, repo)
rtest.Assert(t, !lockID.Equal(lockID2),
"expected a new ID after lock refresh, got the same")
lock2, err := LoadLock(context.TODO(), repo, lockID2)
rtest.OK(t, err)
rtest.Assert(t, lock2.Time.After(time0),
"expected a later timestamp after lock refresh")
rtest.OK(t, lock.Unlock(context.TODO()))
}
func TestLockRefresh(t *testing.T) {
testLockRefresh(t, func(lock *Lock) error {
return lock.Refresh(context.TODO())
})
}
func TestLockRefreshStale(t *testing.T) {
testLockRefresh(t, func(lock *Lock) error {
return lock.RefreshStaleLock(context.TODO())
})
}
func TestLockRefreshStaleMissing(t *testing.T) {
repo, _, be := TestRepositoryWithVersion(t, 0)
TestSetLockTimeout(t, 5*time.Millisecond)
lock, err := NewLock(context.TODO(), &internalRepository{repo}, false)
rtest.OK(t, err)
lockID := checkSingleLock(t, repo)
// refresh must fail if lock was removed
rtest.OK(t, be.Remove(context.TODO(), backend.Handle{Type: restic.LockFile, Name: lockID.String()}))
time.Sleep(time.Millisecond)
err = lock.RefreshStaleLock(context.TODO())
rtest.Assert(t, err == ErrRemovedLock, "unexpected error, expected %v, got %v", ErrRemovedLock, err)
}
+32
View File
@@ -0,0 +1,32 @@
//go:build !windows
package repository
import (
"os"
"syscall"
"github.com/restic/restic/internal/debug"
)
// checkProcess will check if the process retaining the lock
// exists and responds to SIGHUP signal.
// Returns true if the process exists and responds.
func (l *Lock) processExists() bool {
proc, err := os.FindProcess(l.PID)
if err != nil {
debug.Log("error searching for process %d: %v\n", l.PID, err)
return false
}
defer func() {
_ = proc.Release()
}()
debug.Log("sending SIGHUP to process %d\n", l.PID)
err = proc.Signal(syscall.SIGHUP)
if err != nil {
debug.Log("signal error: %v, lock is probably stale\n", err)
return false
}
return true
}
+22
View File
@@ -0,0 +1,22 @@
package repository
import (
"os"
"github.com/restic/restic/internal/debug"
)
// checkProcess will check if the process retaining the lock exists.
// Returns true if the process exists.
func (l *Lock) processExists() bool {
proc, err := os.FindProcess(l.PID)
if err != nil {
debug.Log("error searching for process %d: %v\n", l.PID, err)
return false
}
err = proc.Release()
if err != nil {
debug.Log("error releasing process %d: %v\n", l.PID, err)
}
return true
}
+4 -4
View File
@@ -38,7 +38,7 @@ func checkedLockRepo(ctx context.Context, t *testing.T, repo *Repository, locker
lock, wrappedCtx, err := lockerInst.Lock(ctx, repo, false, retryLock, func(msg string) {}, func(format string, args ...interface{}) {})
rtest.OK(t, err)
rtest.OK(t, wrappedCtx.Err())
if lock.(*unlocker).info.lock.Stale() {
if lock.info.lock.Stale() {
t.Fatal("lock returned stale lock")
}
return lock, wrappedCtx
@@ -83,7 +83,7 @@ func TestLockConflict(t *testing.T) {
if err == nil {
t.Fatal("second lock should have failed")
}
rtest.Assert(t, restic.IsAlreadyLocked(err), "unexpected error %v", err)
rtest.Assert(t, IsAlreadyLocked(err), "unexpected error %v", err)
}
type writeOnceBackend struct {
@@ -309,8 +309,8 @@ func createFakeLock(repo *Repository, t time.Time, pid int) (restic.ID, error) {
return restic.ID{}, err
}
newLock := &restic.Lock{Time: t, PID: pid, Hostname: hostname}
return restic.SaveJSONUnpacked(context.TODO(), &internalRepository{repo}, restic.LockFile, &newLock)
newLock := &Lock{Time: t, PID: pid, Hostname: hostname}
return restic.SaveJSONUnpacked(context.TODO(), &internalRepository{repo}, restic.LockFile, newLock)
}
func lockExists(repo restic.Lister, t testing.TB, lockID restic.ID) bool {
-5
View File
@@ -158,11 +158,6 @@ func BenchmarkAllVersions(b *testing.B, bench VersionedBenchmark) {
}
}
func TestNewLock(_ *testing.T, repo *Repository, exclusive bool) (*restic.Lock, error) {
// TODO get rid of this test helper
return restic.NewLock(context.TODO(), &internalRepository{repo}, exclusive)
}
// TestCheckRepo runs the checker on repo.
func TestCheckRepo(t testing.TB, repo *Repository) {
chkr := newChecker(repo)