Skip to content

Retry interrupted SSH uploads using a fresh connection - #880

Open
Masterwow3 wants to merge 1 commit into
offen:mainfrom
Masterwow3:fix/ssh-upload-retries
Open

Masterwow3 wants to merge 1 commit into
offen:mainfrom
Masterwow3:fix/ssh-upload-retries

Conversation

@Masterwow3

Copy link
Copy Markdown
Contributor

An SSH/SFTP connection loss during upload currently fails the entire backup run, even when the archive has already been created and the containers have restarted. The keepalive added in #863 helps prevent idle disconnects but does not reconnect after a transport failure.

This change retries the existing archive over a fresh SSH/SFTP connection. Each attempt starts at byte zero and truncates the destination file. It does not recreate the archive or run container lifecycle hooks again.

  • Add SSH_UPLOAD_RETRIES (default: 2; 0 disables retries) and SSH_UPLOAD_RETRY_DELAY (default: 5s).
  • Retry connection errors only. Local source errors, permission errors, and other permanent SFTP errors still fail immediately.
  • Close the failed session and its keepalive before reconnecting. Cleanup and pruning use the current session; a previous session's close error cannot turn a recovered upload into a failed run.
  • Log upload success only after the remote file has closed successfully and the transferred byte count matches the source size.

Validation:

  • go test ./...
  • go vet ./...
  • GOOS=linux GOARCH=amd64 go build ./...
  • In-process SSH/SFTP regression test that forcibly closes the TCP connection during upload, verifies a new connection, compares the complete remote archive byte for byte, and checks pruning and cleanup on the replacement session.
  • Unit coverage for retry limits, failed reconnects, disabled retries, retry delays, permanent errors, remote close failures, size mismatches, and configuration defaults/validation.

The Docker Compose integration suite was not run because no Docker daemon was available in the development environment. The SSH/SFTP regression test runs as part of the Go test suite without Docker.

Scope: this change does not retry initial backend setup or pruning, retain temporary archives after retries are exhausted, or publish remote files by atomic rename. An exhausted upload can still leave a partial remote file, as before. Existing SSH connection/handshake timeout behavior is unchanged.

@m90 m90 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this. I think this is a nice feature to have, but considering stability, I'd prefer starting with a default value of 0 and letting users opt into this behavior. See the comment inline. Also, there is a linting error in the test.

Let me know if there are any questions.

Comment thread cmd/backup/config.go
SSHIdentityFile string `split_words:"true" default:"/root/.ssh/id_rsa"`
SSHIdentityPassphrase string `split_words:"true"`
SSHRemotePath string `split_words:"true"`
SSHUploadRetries WholeNumber `split_words:"true" default:"2"`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considering the current behavior is not retrying, I would feel more comfortable with setting a default of 0 and allow users that need/want this to opt in by setting a value themselves.

// NewStorageBackend creates and initializes a new SSH storage backend.
func NewStorageBackend(opts Config, logFunc storage.Log) (storage.Backend, func() error, error) {
if opts.UploadRetries < 0 || opts.UploadRetryDelay < 0 {
return nil, noop, errwrap.Wrap(nil, "SSH upload retries and retry delay must not be negative")

@m90 m90 Oct 9, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this case is really needed as the WholeNumber type used in the config will bail on anything negative.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants