Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a global option --retry-lock #4107

Merged
merged 3 commits into from
Apr 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions changelog/unreleased/issue-719
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Enhancement: Add --retry-lock option

This option allows to specify a duration for which restic will wait if there
already exists a conflicting lock within the repository.

https://github.com/restic/restic/issues/719
https://github.com/restic/restic/pull/2214
https://github.com/restic/restic/pull/4107
2 changes: 1 addition & 1 deletion cmd/restic/cmd_backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
if !gopts.JSON {
progressPrinter.V("lock repository")
}
lock, ctx, err := lockRepo(ctx, repo)
lock, ctx, err := lockRepo(ctx, repo, gopts.RetryLock, gopts.JSON)
defer unlockRepo(lock)
if err != nil {
return err
Expand Down
2 changes: 1 addition & 1 deletion cmd/restic/cmd_cat.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error {

if !gopts.NoLock {
var lock *restic.Lock
lock, ctx, err = lockRepo(ctx, repo)
lock, ctx, err = lockRepo(ctx, repo, gopts.RetryLock, gopts.JSON)
defer unlockRepo(lock)
if err != nil {
return err
Expand Down
2 changes: 1 addition & 1 deletion cmd/restic/cmd_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args
if !gopts.NoLock {
Verbosef("create exclusive lock for repository\n")
var lock *restic.Lock
lock, ctx, err = lockRepoExclusive(ctx, repo)
lock, ctx, err = lockRepoExclusive(ctx, repo, gopts.RetryLock, gopts.JSON)
defer unlockRepo(lock)
if err != nil {
return err
Expand Down
4 changes: 2 additions & 2 deletions cmd/restic/cmd_copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,14 @@ func runCopy(ctx context.Context, opts CopyOptions, gopts GlobalOptions, args []

if !gopts.NoLock {
var srcLock *restic.Lock
srcLock, ctx, err = lockRepo(ctx, srcRepo)
srcLock, ctx, err = lockRepo(ctx, srcRepo, gopts.RetryLock, gopts.JSON)
defer unlockRepo(srcLock)
if err != nil {
return err
}
}

dstLock, ctx, err := lockRepo(ctx, dstRepo)
dstLock, ctx, err := lockRepo(ctx, dstRepo, gopts.RetryLock, gopts.JSON)
defer unlockRepo(dstLock)
if err != nil {
return err
Expand Down
4 changes: 2 additions & 2 deletions cmd/restic/cmd_debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ func runDebugDump(ctx context.Context, gopts GlobalOptions, args []string) error

if !gopts.NoLock {
var lock *restic.Lock
lock, ctx, err = lockRepo(ctx, repo)
lock, ctx, err = lockRepo(ctx, repo, gopts.RetryLock, gopts.JSON)
defer unlockRepo(lock)
if err != nil {
return err
Expand Down Expand Up @@ -462,7 +462,7 @@ func runDebugExamine(ctx context.Context, gopts GlobalOptions, args []string) er

if !gopts.NoLock {
var lock *restic.Lock
lock, ctx, err = lockRepo(ctx, repo)
lock, ctx, err = lockRepo(ctx, repo, gopts.RetryLock, gopts.JSON)
defer unlockRepo(lock)
if err != nil {
return err
Expand Down
2 changes: 1 addition & 1 deletion cmd/restic/cmd_diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args []

if !gopts.NoLock {
var lock *restic.Lock
lock, ctx, err = lockRepo(ctx, repo)
lock, ctx, err = lockRepo(ctx, repo, gopts.RetryLock, gopts.JSON)
defer unlockRepo(lock)
if err != nil {
return err
Expand Down
2 changes: 1 addition & 1 deletion cmd/restic/cmd_dump.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ func runDump(ctx context.Context, opts DumpOptions, gopts GlobalOptions, args []

if !gopts.NoLock {
var lock *restic.Lock
lock, ctx, err = lockRepo(ctx, repo)
lock, ctx, err = lockRepo(ctx, repo, gopts.RetryLock, gopts.JSON)
defer unlockRepo(lock)
if err != nil {
return err
Expand Down
2 changes: 1 addition & 1 deletion cmd/restic/cmd_find.go
Original file line number Diff line number Diff line change
Expand Up @@ -575,7 +575,7 @@ func runFind(ctx context.Context, opts FindOptions, gopts GlobalOptions, args []

if !gopts.NoLock {
var lock *restic.Lock
lock, ctx, err = lockRepo(ctx, repo)
lock, ctx, err = lockRepo(ctx, repo, gopts.RetryLock, gopts.JSON)
defer unlockRepo(lock)
if err != nil {
return err
Expand Down
2 changes: 1 addition & 1 deletion cmd/restic/cmd_forget.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ func runForget(ctx context.Context, opts ForgetOptions, gopts GlobalOptions, arg

if !opts.DryRun || !gopts.NoLock {
var lock *restic.Lock
lock, ctx, err = lockRepoExclusive(ctx, repo)
lock, ctx, err = lockRepoExclusive(ctx, repo, gopts.RetryLock, gopts.JSON)
defer unlockRepo(lock)
if err != nil {
return err
Expand Down
8 changes: 4 additions & 4 deletions cmd/restic/cmd_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,23 +212,23 @@ func runKey(ctx context.Context, gopts GlobalOptions, args []string) error {

switch args[0] {
case "list":
lock, ctx, err := lockRepo(ctx, repo)
lock, ctx, err := lockRepo(ctx, repo, gopts.RetryLock, gopts.JSON)
defer unlockRepo(lock)
if err != nil {
return err
}

return listKeys(ctx, repo, gopts)
case "add":
lock, ctx, err := lockRepo(ctx, repo)
lock, ctx, err := lockRepo(ctx, repo, gopts.RetryLock, gopts.JSON)
defer unlockRepo(lock)
if err != nil {
return err
}

return addKey(ctx, repo, gopts)
case "remove":
lock, ctx, err := lockRepoExclusive(ctx, repo)
lock, ctx, err := lockRepoExclusive(ctx, repo, gopts.RetryLock, gopts.JSON)
defer unlockRepo(lock)
if err != nil {
return err
Expand All @@ -241,7 +241,7 @@ func runKey(ctx context.Context, gopts GlobalOptions, args []string) error {

return deleteKey(ctx, repo, id)
case "passwd":
lock, ctx, err := lockRepoExclusive(ctx, repo)
lock, ctx, err := lockRepoExclusive(ctx, repo, gopts.RetryLock, gopts.JSON)
defer unlockRepo(lock)
if err != nil {
return err
Expand Down
8 changes: 4 additions & 4 deletions cmd/restic/cmd_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,19 @@ func init() {
cmdRoot.AddCommand(cmdList)
}

func runList(ctx context.Context, cmd *cobra.Command, opts GlobalOptions, args []string) error {
func runList(ctx context.Context, cmd *cobra.Command, gopts GlobalOptions, args []string) error {
if len(args) != 1 {
return errors.Fatal("type not specified, usage: " + cmd.Use)
}

repo, err := OpenRepository(ctx, opts)
repo, err := OpenRepository(ctx, gopts)
if err != nil {
return err
}

if !opts.NoLock && args[0] != "locks" {
if !gopts.NoLock && args[0] != "locks" {
var lock *restic.Lock
lock, ctx, err = lockRepo(ctx, repo)
lock, ctx, err = lockRepo(ctx, repo, gopts.RetryLock, gopts.JSON)
defer unlockRepo(lock)
if err != nil {
return err
Expand Down
2 changes: 1 addition & 1 deletion cmd/restic/cmd_migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ func runMigrate(ctx context.Context, opts MigrateOptions, gopts GlobalOptions, a
return err
}

lock, ctx, err := lockRepoExclusive(ctx, repo)
lock, ctx, err := lockRepoExclusive(ctx, repo, gopts.RetryLock, gopts.JSON)
defer unlockRepo(lock)
if err != nil {
return err
Expand Down
2 changes: 1 addition & 1 deletion cmd/restic/cmd_mount.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ func runMount(ctx context.Context, opts MountOptions, gopts GlobalOptions, args

if !gopts.NoLock {
var lock *restic.Lock
lock, ctx, err = lockRepo(ctx, repo)
lock, ctx, err = lockRepo(ctx, repo, gopts.RetryLock, gopts.JSON)
defer unlockRepo(lock)
if err != nil {
return err
Expand Down
2 changes: 1 addition & 1 deletion cmd/restic/cmd_prune.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ func runPrune(ctx context.Context, opts PruneOptions, gopts GlobalOptions) error
opts.unsafeRecovery = true
}

lock, ctx, err := lockRepoExclusive(ctx, repo)
lock, ctx, err := lockRepoExclusive(ctx, repo, gopts.RetryLock, gopts.JSON)
defer unlockRepo(lock)
if err != nil {
return err
Expand Down
2 changes: 1 addition & 1 deletion cmd/restic/cmd_rebuild_index.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func runRebuildIndex(ctx context.Context, opts RebuildIndexOptions, gopts Global
return err
}

lock, ctx, err := lockRepoExclusive(ctx, repo)
lock, ctx, err := lockRepoExclusive(ctx, repo, gopts.RetryLock, gopts.JSON)
defer unlockRepo(lock)
if err != nil {
return err
Expand Down
2 changes: 1 addition & 1 deletion cmd/restic/cmd_recover.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func runRecover(ctx context.Context, gopts GlobalOptions) error {
return err
}

lock, ctx, err := lockRepo(ctx, repo)
lock, ctx, err := lockRepo(ctx, repo, gopts.RetryLock, gopts.JSON)
defer unlockRepo(lock)
if err != nil {
return err
Expand Down
2 changes: 1 addition & 1 deletion cmd/restic/cmd_restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions,

if !gopts.NoLock {
var lock *restic.Lock
lock, ctx, err = lockRepo(ctx, repo)
lock, ctx, err = lockRepo(ctx, repo, gopts.RetryLock, gopts.JSON)
defer unlockRepo(lock)
if err != nil {
return err
Expand Down
4 changes: 2 additions & 2 deletions cmd/restic/cmd_rewrite.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,9 @@ func runRewrite(ctx context.Context, opts RewriteOptions, gopts GlobalOptions, a
var err error
if opts.Forget {
Verbosef("create exclusive lock for repository\n")
lock, ctx, err = lockRepoExclusive(ctx, repo)
lock, ctx, err = lockRepoExclusive(ctx, repo, gopts.RetryLock, gopts.JSON)
} else {
lock, ctx, err = lockRepo(ctx, repo)
lock, ctx, err = lockRepo(ctx, repo, gopts.RetryLock, gopts.JSON)
}
defer unlockRepo(lock)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion cmd/restic/cmd_snapshots.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func runSnapshots(ctx context.Context, opts SnapshotOptions, gopts GlobalOptions

if !gopts.NoLock {
var lock *restic.Lock
lock, ctx, err = lockRepo(ctx, repo)
lock, ctx, err = lockRepo(ctx, repo, gopts.RetryLock, gopts.JSON)
defer unlockRepo(lock)
if err != nil {
return err
Expand Down
2 changes: 1 addition & 1 deletion cmd/restic/cmd_stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func runStats(ctx context.Context, gopts GlobalOptions, args []string) error {

if !gopts.NoLock {
var lock *restic.Lock
lock, ctx, err = lockRepo(ctx, repo)
lock, ctx, err = lockRepo(ctx, repo, gopts.RetryLock, gopts.JSON)
defer unlockRepo(lock)
if err != nil {
return err
Expand Down
2 changes: 1 addition & 1 deletion cmd/restic/cmd_tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func runTag(ctx context.Context, opts TagOptions, gopts GlobalOptions, args []st
if !gopts.NoLock {
Verbosef("create exclusive lock for repository\n")
var lock *restic.Lock
lock, ctx, err = lockRepoExclusive(ctx, repo)
lock, ctx, err = lockRepoExclusive(ctx, repo, gopts.RetryLock, gopts.JSON)
defer unlockRepo(lock)
if err != nil {
return err
Expand Down
2 changes: 2 additions & 0 deletions cmd/restic/global.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ type GlobalOptions struct {
Quiet bool
Verbose int
NoLock bool
RetryLock time.Duration
JSON bool
CacheDir string
NoCache bool
Expand Down Expand Up @@ -115,6 +116,7 @@ func init() {
// use empty paremeter name as `-v, --verbose n` instead of the correct `--verbose=n` is confusing
f.CountVarP(&globalOptions.Verbose, "verbose", "v", "be verbose (specify multiple times or a level using --verbose=n``, max level/times is 2)")
f.BoolVar(&globalOptions.NoLock, "no-lock", false, "do not lock the repository, this allows some operations on read-only repositories")
f.DurationVar(&globalOptions.RetryLock, "retry-lock", 0, "retry to lock the repository if it is already locked, takes a value like 5m or 2h (default: no retries)")
f.BoolVarP(&globalOptions.JSON, "json", "", false, "set output mode to JSON for commands that support it")
f.StringVar(&globalOptions.CacheDir, "cache-dir", "", "set the cache `directory`. (default: use system default cache directory)")
f.BoolVar(&globalOptions.NoCache, "no-cache", false, "do not use a local cache")
Expand Down
61 changes: 55 additions & 6 deletions cmd/restic/lock.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,29 @@ var globalLocks struct {
sync.Once
}

func lockRepo(ctx context.Context, repo restic.Repository) (*restic.Lock, context.Context, error) {
return lockRepository(ctx, repo, false)
func lockRepo(ctx context.Context, repo restic.Repository, retryLock time.Duration, json bool) (*restic.Lock, context.Context, error) {
return lockRepository(ctx, repo, false, retryLock, json)
}

func lockRepoExclusive(ctx context.Context, repo restic.Repository) (*restic.Lock, context.Context, error) {
return lockRepository(ctx, repo, true)
func lockRepoExclusive(ctx context.Context, repo restic.Repository, retryLock time.Duration, json bool) (*restic.Lock, context.Context, error) {
return lockRepository(ctx, repo, true, retryLock, json)
}

var (
retrySleepStart = 5 * time.Second
retrySleepMax = 60 * time.Second
)

func minDuration(a, b time.Duration) time.Duration {
if a <= b {
return a
}
return b
}

// lockRepository wraps the ctx such that it is cancelled when the repository is unlocked
// cancelling the original context also stops the lock refresh
func lockRepository(ctx context.Context, repo restic.Repository, exclusive bool) (*restic.Lock, context.Context, error) {
func lockRepository(ctx context.Context, repo restic.Repository, exclusive bool, retryLock time.Duration, json bool) (*restic.Lock, context.Context, error) {
// make sure that a repository is unlocked properly and after cancel() was
// called by the cleanup handler in global.go
globalLocks.Do(func() {
Expand All @@ -43,7 +55,44 @@ func lockRepository(ctx context.Context, repo restic.Repository, exclusive bool)
lockFn = restic.NewExclusiveLock
}

lock, err := lockFn(ctx, repo)
var lock *restic.Lock
var err error

retrySleep := minDuration(retrySleepStart, retryLock)
retryMessagePrinted := false
retryTimeout := time.After(retryLock)

retryLoop:
for {
lock, err = lockFn(ctx, repo)
if err != nil && restic.IsAlreadyLocked(err) {

if !retryMessagePrinted {
if !json {
Verbosef("repo already locked, waiting up to %s for the lock\n", retryLock)
}
retryMessagePrinted = true
}

debug.Log("repo already locked, retrying in %v", retrySleep)
retrySleepCh := time.After(retrySleep)

select {
case <-ctx.Done():
return nil, ctx, ctx.Err()
case <-retryTimeout:
debug.Log("repo already locked, timeout expired")
// Last lock attempt
lock, err = lockFn(ctx, repo)
break retryLoop
case <-retrySleepCh:
retrySleep = minDuration(retrySleep*2, retrySleepMax)
}
} else {
// anything else, either a successful lock or another error
break retryLoop
}
}
if restic.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)
}
Expand Down