forked from TrueCloudLab/restic
cd4cc1daec
locks: fix testing stale locks created on other hosts
308 lines
7.4 KiB
Go
308 lines
7.4 KiB
Go
package restic
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"os/user"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/restic/restic/backend"
|
|
"github.com/restic/restic/debug"
|
|
"github.com/restic/restic/repository"
|
|
)
|
|
|
|
// 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 {
|
|
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 *repository.Repository
|
|
lockID *backend.ID
|
|
}
|
|
|
|
// ErrAlreadyLocked is returned when NewLock or NewExclusiveLock are unable to
|
|
// acquire the desired lock.
|
|
type ErrAlreadyLocked struct {
|
|
otherLock *Lock
|
|
}
|
|
|
|
func (e ErrAlreadyLocked) Error() string {
|
|
return fmt.Sprintf("repository is already locked by %v", e.otherLock)
|
|
}
|
|
|
|
// IsAlreadyLocked returns true iff err is an instance of ErrAlreadyLocked.
|
|
func IsAlreadyLocked(err error) bool {
|
|
if _, ok := err.(ErrAlreadyLocked); ok {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// NewLock returns a new, non-exclusive lock for the repository. If an
|
|
// exclusive lock is already held by another process, ErrAlreadyLocked is
|
|
// returned.
|
|
func NewLock(repo *repository.Repository) (*Lock, error) {
|
|
return newLock(repo, false)
|
|
}
|
|
|
|
// NewExclusiveLock returns a new, exclusive lock for the repository. If
|
|
// another lock (normal and exclusive) is already held by another process,
|
|
// ErrAlreadyLocked is returned.
|
|
func NewExclusiveLock(repo *repository.Repository) (*Lock, error) {
|
|
return newLock(repo, true)
|
|
}
|
|
|
|
const waitBeforeLockCheck = 200 * time.Millisecond
|
|
|
|
func newLock(repo *repository.Repository, excl bool) (*Lock, error) {
|
|
lock := &Lock{
|
|
Time: time.Now(),
|
|
PID: os.Getpid(),
|
|
Exclusive: excl,
|
|
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(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
lockID, err := lock.createLock()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
lock.lockID = &lockID
|
|
|
|
time.Sleep(waitBeforeLockCheck)
|
|
|
|
if err = lock.checkForOtherLocks(); err != nil {
|
|
lock.Unlock()
|
|
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 = 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() error {
|
|
return eachLock(l.repo, func(id backend.ID, lock *Lock, err error) error {
|
|
if l.lockID != nil && id.Equal(*l.lockID) {
|
|
return nil
|
|
}
|
|
|
|
// ignore locks that cannot be loaded
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
if l.Exclusive {
|
|
return ErrAlreadyLocked{otherLock: lock}
|
|
}
|
|
|
|
if !l.Exclusive && lock.Exclusive {
|
|
return ErrAlreadyLocked{otherLock: lock}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func eachLock(repo *repository.Repository, f func(backend.ID, *Lock, error) error) error {
|
|
done := make(chan struct{})
|
|
defer close(done)
|
|
|
|
for id := range repo.List(backend.Lock, done) {
|
|
lock, err := LoadLock(repo, id)
|
|
err = f(id, lock, err)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// createLock acquires the lock by creating a file in the repository.
|
|
func (l *Lock) createLock() (backend.ID, error) {
|
|
id, err := l.repo.SaveJSONUnpacked(backend.Lock, l)
|
|
if err != nil {
|
|
return backend.ID{}, err
|
|
}
|
|
|
|
return id, nil
|
|
}
|
|
|
|
// Unlock removes the lock from the repository.
|
|
func (l *Lock) Unlock() error {
|
|
if l == nil || l.lockID == nil {
|
|
return nil
|
|
}
|
|
|
|
return l.repo.Backend().Remove(backend.Lock, l.lockID.String())
|
|
}
|
|
|
|
var staleTimeout = 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 {
|
|
debug.Log("Lock.Stale", "testing if lock %v for process %d is stale", l.lockID.Str(), l.PID)
|
|
if time.Now().Sub(l.Time) > staleTimeout {
|
|
debug.Log("Lock.Stale", "lock is stale, timestamp is too old: %v\n", l.Time)
|
|
return true
|
|
}
|
|
|
|
hn, err := os.Hostname()
|
|
if err != nil {
|
|
debug.Log("Lock.Stale", "unable to find current hostnanme: %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
|
|
}
|
|
|
|
proc, err := os.FindProcess(l.PID)
|
|
defer proc.Release()
|
|
if err != nil {
|
|
debug.Log("Lock.Stale", "error searching for process %d: %v\n", l.PID, err)
|
|
return true
|
|
}
|
|
|
|
debug.Log("Lock.Stale", "sending SIGHUP to process %d\n", l.PID)
|
|
err = proc.Signal(syscall.SIGHUP)
|
|
if err != nil {
|
|
debug.Log("Lock.Stale", "signal error: %v, lock is probably stale\n", err)
|
|
return true
|
|
}
|
|
|
|
debug.Log("Lock.Stale", "lock not stale\n")
|
|
return false
|
|
}
|
|
|
|
// 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() error {
|
|
debug.Log("Lock.Refresh", "refreshing lock %v", l.lockID.Str())
|
|
id, err := l.createLock()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = l.repo.Backend().Remove(backend.Lock, l.lockID.String())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
debug.Log("Lock.Refresh", "new lock ID %v", id.Str())
|
|
l.lockID = &id
|
|
|
|
return nil
|
|
}
|
|
|
|
func (l Lock) String() string {
|
|
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())
|
|
|
|
if l.Stale() {
|
|
text += " (stale)"
|
|
}
|
|
|
|
return text
|
|
}
|
|
|
|
// listen for incoming SIGHUP and ignore
|
|
var ignoreSIGHUP sync.Once
|
|
|
|
func init() {
|
|
ignoreSIGHUP.Do(func() {
|
|
go func() {
|
|
c := make(chan os.Signal)
|
|
signal.Notify(c, syscall.SIGHUP)
|
|
for s := range c {
|
|
debug.Log("lock.ignoreSIGHUP", "Signal received: %v\n", s)
|
|
}
|
|
}()
|
|
})
|
|
}
|
|
|
|
// LoadLock loads and unserializes a lock from a repository.
|
|
func LoadLock(repo *repository.Repository, id backend.ID) (*Lock, error) {
|
|
lock := &Lock{}
|
|
if err := repo.LoadJSONUnpacked(backend.Lock, id, lock); err != nil {
|
|
return nil, err
|
|
}
|
|
lock.lockID = &id
|
|
|
|
return lock, nil
|
|
}
|
|
|
|
// RemoveStaleLocks deletes all locks detected as stale from the repository.
|
|
func RemoveStaleLocks(repo *repository.Repository) error {
|
|
return eachLock(repo, func(id backend.ID, lock *Lock, err error) error {
|
|
// ignore locks that cannot be loaded
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
if lock.Stale() {
|
|
return repo.Backend().Remove(backend.Lock, id.String())
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// RemoveAllLocks removes all locks forcefully.
|
|
func RemoveAllLocks(repo *repository.Repository) error {
|
|
return eachLock(repo, func(id backend.ID, lock *Lock, err error) error {
|
|
return repo.Backend().Remove(backend.Lock, id.String())
|
|
})
|
|
}
|