all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Jakob Klocker <j.klocker@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH pve-manager 2/3] fix #6464: vzdump: add retry strategies for failed guest backups
Date: Mon,  2 Nov 2026 12:58:50 +0100	[thread overview]
Message-ID: <20261102115851.171429-3-j.klocker@proxmox.com> (raw)
In-Reply-To: <20261102115851.171429-1-j.klocker@proxmox.com>

Add the `error-behavior` and `max-retries` options to automatically
retry failed guest backups, which helps with transient failures.

Supported retry strategies:
- `ignore`: Preserves current behavior (no retries, default).
- `retry-immediately`: Retries the guest inline after
  failure.
- `retry`: Defers failed guests until all other guests have been
processed, then retries them in the order they failed.

A guest is retried at most `max-retries` times. Between attempts, the
delay starts at 30 seconds and dobules with each retry, capped at 30
minutes as suggested on bugzilla [0]. If a guest still fails after the
last retry, it is counted as an error and the job finishes with errors.
If a retry succeeds, the guest is reported as successful in the
notifications and no error is counted.

The task log is opened in append mode for retries, so the log contains
the complete transcript of all attempts.

[0]: https://bugzilla.proxmox.com/show_bug.cgi?id=6464#c5
Link: https://bugzilla.proxmox.com/show_bug.cgi?id=6464

Signed-off-by: Jakob Klocker <j.klocker@proxmox.com>
---
 PVE/VZDump.pm | 131 +++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 129 insertions(+), 2 deletions(-)

diff --git a/PVE/VZDump.pm b/PVE/VZDump.pm
index defe9e0a..6199b7ff 100644
--- a/PVE/VZDump.pm
+++ b/PVE/VZDump.pm
@@ -10,6 +10,7 @@ use File::Path;
 use IO::File;
 use IO::Select;
 use IPC::Open3;
+use List::Util qw(min);
 use POSIX qw(strftime);
 use Time::Local;
 
@@ -1047,7 +1048,8 @@ sub exec_backup_task {
         -d $task->{tmpdir}
             || die "unable to create temporary directory '$task->{tmpdir}'";
 
-        $logfd = IO::File->new(">$tmplog")
+        my $logmode = $task->{retries} ? '>>' : '>';
+        $logfd = IO::File->new("${logmode}${tmplog}")
             || die "unable to create log file '$tmplog'";
 
         $task->{dumpdir} = $opts->{dumpdir};
@@ -1434,14 +1436,139 @@ sub exec_backup {
 
     my $starttime = time();
     my $errcount = 0;
+
+    my $error_behavior = $opts->{'error-behavior'} // 'ignore';
+    my $max_retries = $opts->{'max-retries'} // 5;
+    my $min_between_retry = 30;
+    my $max_wait = 60 * 30;
+
+    my $get_backoff = sub {
+        my ($retry) = @_;
+        return min($min_between_retry * 2**($retry - 1), $max_wait);
+    };
+
     eval {
 
         $self->{'backup-provider'}->job_init($starttime) if $self->{'backup-provider'};
         $self->run_hook_script('job-start', undef, $job_start_fd);
 
+        my $failed_tasks = {};
+
         foreach my $task (@$tasklist) {
+            my $vmid = $task->{vmid};
+            my $initial_task = {%$task};
+
+            $task->{retries} = 0;
             $self->exec_backup_task($task);
-            $errcount += 1 if $task->{state} ne 'ok';
+
+            next if $task->{state} eq 'ok';
+
+            if ($error_behavior eq 'retry-immediately') {
+                for my $retry (1 .. $max_retries) {
+                    my $backoff = $get_backoff->($retry);
+
+                    debugmsg(
+                        'info',
+                        "Retrying backup for guest $vmid (attempt $retry/$max_retries) after ${backoff}s delay...",
+                        undef,
+                        1,
+                    );
+                    sleep($backoff);
+
+                    %$task = (%$initial_task, retries => $retry);
+                    $self->exec_backup_task($task);
+
+                    last if $task->{state} eq 'ok';
+                }
+
+            } elsif ($error_behavior eq 'retry') {
+                $failed_tasks->{$vmid} = {
+                    task => $task,
+                    initial_task => $initial_task,
+                    fail_time => time(),
+                    retries => 0,
+                };
+                next;
+            }
+
+            if ($task->{state} ne 'ok') {
+                debugmsg('err', "Guest $vmid failed after $max_retries retries.", undef, 1)
+                    if $error_behavior eq 'retry-immediately';
+                $errcount++;
+            }
+        }
+
+        if ($error_behavior eq 'retry') {
+            while (keys %$failed_tasks > 0) {
+                my $now = time();
+                my $min_sleep = $max_wait;
+                my $ready_to_run = [];
+
+                my @sorted_vmids = sort {
+                    $failed_tasks->{$a}->{fail_time} <=> $failed_tasks->{$b}->{fail_time}
+                        || $a <=> $b
+                } keys %$failed_tasks;
+
+                foreach my $vmid (@sorted_vmids) {
+                    my $fail_info = $failed_tasks->{$vmid};
+
+                    if ($fail_info->{retries} >= $max_retries) {
+                        debugmsg(
+                            'err',
+                            "Guest $vmid failed after $max_retries retries.",
+                            undef,
+                            1,
+                        );
+                        $errcount++;
+                        delete $failed_tasks->{$vmid};
+                        next;
+                    }
+
+                    my $delay = $get_backoff->($fail_info->{retries} + 1);
+                    my $target_time = $fail_info->{fail_time} + $delay;
+                    my $wait_time = $target_time - $now;
+
+                    if ($wait_time > 0) {
+                        $min_sleep = $wait_time if $wait_time < $min_sleep;
+                    } else {
+                        push @$ready_to_run, $vmid;
+                    }
+                }
+
+                if (scalar(@$ready_to_run) == 0 && keys %$failed_tasks > 0) {
+                    debugmsg(
+                        'info',
+                        "Waiting $min_sleep seconds before next backup retry...",
+                        undef,
+                        1,
+                    );
+                    sleep($min_sleep);
+                    next;
+                }
+
+                foreach my $vmid (@$ready_to_run) {
+                    my $fail_info = $failed_tasks->{$vmid};
+
+                    $fail_info->{retries}++;
+                    debugmsg(
+                        'info',
+                        "Retrying backup for guest $vmid (attempt $fail_info->{retries}/$max_retries)",
+                        undef,
+                        1,
+                    );
+
+                    my $task = $fail_info->{task};
+                    %$task =
+                        (%{ $fail_info->{initial_task} }, retries => $fail_info->{retries});
+                    $self->exec_backup_task($task);
+
+                    if ($task->{state} eq 'ok') {
+                        delete $failed_tasks->{$vmid};
+                    } else {
+                        $fail_info->{fail_time} = time();
+                    }
+                }
+            }
         }
 
         $self->{'backup-provider'}->job_cleanup() if $self->{'backup-provider'};
-- 
2.47.3





  parent reply	other threads:[~2026-11-02 12:00 UTC|newest]

Thread overview: 4+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-02 11:58 [PATCH guest-common/manager 0/3] fix #6464: backup: add backup retry strategies Jakob Klocker
2026-11-02 11:58 ` [PATCH pve-guest-common 1/3] vzdump: add error-behavior and max-retries options Jakob Klocker
2026-11-02 11:58 ` Jakob Klocker [this message]
2026-11-02 11:58 ` [PATCH pve-manager 3/3] ui: backup: add error behavior and retry options Jakob Klocker

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261102115851.171429-3-j.klocker@proxmox.com \
    --to=j.klocker@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal