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 }