github.com/mckael/restic@v0.8.3/internal/restic/lock.go (about)

     1  package restic
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"os/signal"
     8  	"os/user"
     9  	"sync"
    10  	"syscall"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/restic/restic/internal/errors"
    15  
    16  	"github.com/restic/restic/internal/debug"
    17  )
    18  
    19  // Lock represents a process locking the repository for an operation.
    20  //
    21  // There are two types of locks: exclusive and non-exclusive. There may be many
    22  // different non-exclusive locks, but at most one exclusive lock, which can
    23  // only be acquired while no non-exclusive lock is held.
    24  //
    25  // A lock must be refreshed regularly to not be considered stale, this must be
    26  // triggered by regularly calling Refresh.
    27  type Lock struct {
    28  	Time      time.Time `json:"time"`
    29  	Exclusive bool      `json:"exclusive"`
    30  	Hostname  string    `json:"hostname"`
    31  	Username  string    `json:"username"`
    32  	PID       int       `json:"pid"`
    33  	UID       uint32    `json:"uid,omitempty"`
    34  	GID       uint32    `json:"gid,omitempty"`
    35  
    36  	repo   Repository
    37  	lockID *ID
    38  }
    39  
    40  // ErrAlreadyLocked is returned when NewLock or NewExclusiveLock are unable to
    41  // acquire the desired lock.
    42  type ErrAlreadyLocked struct {
    43  	otherLock *Lock
    44  }
    45  
    46  func (e ErrAlreadyLocked) Error() string {
    47  	s := ""
    48  	if e.otherLock.Exclusive {
    49  		s = "exclusively "
    50  	}
    51  	return fmt.Sprintf("repository is already locked %sby %v", s, e.otherLock)
    52  }
    53  
    54  // IsAlreadyLocked returns true iff err is an instance of ErrAlreadyLocked.
    55  func IsAlreadyLocked(err error) bool {
    56  	if _, ok := errors.Cause(err).(ErrAlreadyLocked); ok {
    57  		return true
    58  	}
    59  
    60  	return false
    61  }
    62  
    63  // NewLock returns a new, non-exclusive lock for the repository. If an
    64  // exclusive lock is already held by another process, ErrAlreadyLocked is
    65  // returned.
    66  func NewLock(ctx context.Context, repo Repository) (*Lock, error) {
    67  	return newLock(ctx, repo, false)
    68  }
    69  
    70  // NewExclusiveLock returns a new, exclusive lock for the repository. If
    71  // another lock (normal and exclusive) is already held by another process,
    72  // ErrAlreadyLocked is returned.
    73  func NewExclusiveLock(ctx context.Context, repo Repository) (*Lock, error) {
    74  	return newLock(ctx, repo, true)
    75  }
    76  
    77  var waitBeforeLockCheck = 200 * time.Millisecond
    78  
    79  // TestSetLockTimeout can be used to reduce the lock wait timeout for tests.
    80  func TestSetLockTimeout(t testing.TB, d time.Duration) {
    81  	t.Logf("setting lock timeout to %v", d)
    82  	waitBeforeLockCheck = d
    83  }
    84  
    85  func newLock(ctx context.Context, repo Repository, excl bool) (*Lock, error) {
    86  	lock := &Lock{
    87  		Time:      time.Now(),
    88  		PID:       os.Getpid(),
    89  		Exclusive: excl,
    90  		repo:      repo,
    91  	}
    92  
    93  	hn, err := os.Hostname()
    94  	if err == nil {
    95  		lock.Hostname = hn
    96  	}
    97  
    98  	if err = lock.fillUserInfo(); err != nil {
    99  		return nil, err
   100  	}
   101  
   102  	if err = lock.checkForOtherLocks(ctx); err != nil {
   103  		return nil, err
   104  	}
   105  
   106  	lockID, err := lock.createLock(ctx)
   107  	if err != nil {
   108  		return nil, err
   109  	}
   110  
   111  	lock.lockID = &lockID
   112  
   113  	time.Sleep(waitBeforeLockCheck)
   114  
   115  	if err = lock.checkForOtherLocks(ctx); err != nil {
   116  		_ = lock.Unlock()
   117  		return nil, err
   118  	}
   119  
   120  	return lock, nil
   121  }
   122  
   123  func (l *Lock) fillUserInfo() error {
   124  	usr, err := user.Current()
   125  	if err != nil {
   126  		return nil
   127  	}
   128  	l.Username = usr.Username
   129  
   130  	l.UID, l.GID, err = uidGidInt(*usr)
   131  	return err
   132  }
   133  
   134  // checkForOtherLocks looks for other locks that currently exist in the repository.
   135  //
   136  // If an exclusive lock is to be created, checkForOtherLocks returns an error
   137  // if there are any other locks, regardless if exclusive or not. If a
   138  // non-exclusive lock is to be created, an error is only returned when an
   139  // exclusive lock is found.
   140  func (l *Lock) checkForOtherLocks(ctx context.Context) error {
   141  	return eachLock(ctx, l.repo, func(id ID, lock *Lock, err error) error {
   142  		if l.lockID != nil && id.Equal(*l.lockID) {
   143  			return nil
   144  		}
   145  
   146  		// ignore locks that cannot be loaded
   147  		if err != nil {
   148  			return nil
   149  		}
   150  
   151  		if l.Exclusive {
   152  			return ErrAlreadyLocked{otherLock: lock}
   153  		}
   154  
   155  		if !l.Exclusive && lock.Exclusive {
   156  			return ErrAlreadyLocked{otherLock: lock}
   157  		}
   158  
   159  		return nil
   160  	})
   161  }
   162  
   163  func eachLock(ctx context.Context, repo Repository, f func(ID, *Lock, error) error) error {
   164  	return repo.List(ctx, LockFile, func(id ID, size int64) error {
   165  		lock, err := LoadLock(ctx, repo, id)
   166  		if err != nil {
   167  			return err
   168  		}
   169  
   170  		return f(id, lock, err)
   171  	})
   172  }
   173  
   174  // createLock acquires the lock by creating a file in the repository.
   175  func (l *Lock) createLock(ctx context.Context) (ID, error) {
   176  	id, err := l.repo.SaveJSONUnpacked(ctx, LockFile, l)
   177  	if err != nil {
   178  		return ID{}, err
   179  	}
   180  
   181  	return id, nil
   182  }
   183  
   184  // Unlock removes the lock from the repository.
   185  func (l *Lock) Unlock() error {
   186  	if l == nil || l.lockID == nil {
   187  		return nil
   188  	}
   189  
   190  	return l.repo.Backend().Remove(context.TODO(), Handle{Type: LockFile, Name: l.lockID.String()})
   191  }
   192  
   193  var staleTimeout = 30 * time.Minute
   194  
   195  // Stale returns true if the lock is stale. A lock is stale if the timestamp is
   196  // older than 30 minutes or if it was created on the current machine and the
   197  // process isn't alive any more.
   198  func (l *Lock) Stale() bool {
   199  	debug.Log("testing if lock %v for process %d is stale", l, l.PID)
   200  	if time.Since(l.Time) > staleTimeout {
   201  		debug.Log("lock is stale, timestamp is too old: %v\n", l.Time)
   202  		return true
   203  	}
   204  
   205  	hn, err := os.Hostname()
   206  	if err != nil {
   207  		debug.Log("unable to find current hostname: %v", err)
   208  		// since we cannot find the current hostname, assume that the lock is
   209  		// not stale.
   210  		return false
   211  	}
   212  
   213  	if hn != l.Hostname {
   214  		// lock was created on a different host, assume the lock is not stale.
   215  		return false
   216  	}
   217  
   218  	// check if we can reach the process retaining the lock
   219  	exists := l.processExists()
   220  	if !exists {
   221  		debug.Log("could not reach process, %d, lock is probably stale\n", l.PID)
   222  		return true
   223  	}
   224  
   225  	debug.Log("lock not stale\n")
   226  	return false
   227  }
   228  
   229  // Refresh refreshes the lock by creating a new file in the backend with a new
   230  // timestamp. Afterwards the old lock is removed.
   231  func (l *Lock) Refresh(ctx context.Context) error {
   232  	debug.Log("refreshing lock %v", l.lockID)
   233  	id, err := l.createLock(ctx)
   234  	if err != nil {
   235  		return err
   236  	}
   237  
   238  	err = l.repo.Backend().Remove(context.TODO(), Handle{Type: LockFile, Name: l.lockID.String()})
   239  	if err != nil {
   240  		return err
   241  	}
   242  
   243  	debug.Log("new lock ID %v", id)
   244  	l.lockID = &id
   245  
   246  	return nil
   247  }
   248  
   249  func (l Lock) String() string {
   250  	text := fmt.Sprintf("PID %d on %s by %s (UID %d, GID %d)\nlock was created at %s (%s ago)\nstorage ID %v",
   251  		l.PID, l.Hostname, l.Username, l.UID, l.GID,
   252  		l.Time.Format("2006-01-02 15:04:05"), time.Since(l.Time),
   253  		l.lockID.Str())
   254  
   255  	return text
   256  }
   257  
   258  // listen for incoming SIGHUP and ignore
   259  var ignoreSIGHUP sync.Once
   260  
   261  func init() {
   262  	ignoreSIGHUP.Do(func() {
   263  		go func() {
   264  			c := make(chan os.Signal)
   265  			signal.Notify(c, syscall.SIGHUP)
   266  			for s := range c {
   267  				debug.Log("Signal received: %v\n", s)
   268  			}
   269  		}()
   270  	})
   271  }
   272  
   273  // LoadLock loads and unserializes a lock from a repository.
   274  func LoadLock(ctx context.Context, repo Repository, id ID) (*Lock, error) {
   275  	lock := &Lock{}
   276  	if err := repo.LoadJSONUnpacked(ctx, LockFile, id, lock); err != nil {
   277  		return nil, err
   278  	}
   279  	lock.lockID = &id
   280  
   281  	return lock, nil
   282  }
   283  
   284  // RemoveStaleLocks deletes all locks detected as stale from the repository.
   285  func RemoveStaleLocks(ctx context.Context, repo Repository) error {
   286  	return eachLock(ctx, repo, func(id ID, lock *Lock, err error) error {
   287  		// ignore locks that cannot be loaded
   288  		if err != nil {
   289  			return nil
   290  		}
   291  
   292  		if lock.Stale() {
   293  			return repo.Backend().Remove(context.TODO(), Handle{Type: LockFile, Name: id.String()})
   294  		}
   295  
   296  		return nil
   297  	})
   298  }
   299  
   300  // RemoveAllLocks removes all locks forcefully.
   301  func RemoveAllLocks(ctx context.Context, repo Repository) error {
   302  	return eachLock(ctx, repo, func(id ID, lock *Lock, err error) error {
   303  		return repo.Backend().Remove(context.TODO(), Handle{Type: LockFile, Name: id.String()})
   304  	})
   305  }