Skip to content

Always set a terminal status when a backup or restore fails - #110990

Open
Onyx2406 wants to merge 4 commits into
ClickHouse:masterfrom
Onyx2406:fix-backup-stuck-creating-status
Open

Always set a terminal status when a backup or restore fails#110990
Onyx2406 wants to merge 4 commits into
ClickHouse:masterfrom
Onyx2406:fix-backup-stuck-creating-status

Conversation

@Onyx2406

Copy link
Copy Markdown
Contributor

Closes: #92649

An aborted backup could stay in CREATING_BACKUP status in system.backups forever (with BACKUP ... ASYNC clients waiting for the status never waking up), even though the backup had already failed — e.g. when the failure was MEMORY_LIMIT_EXCEEDED and the server was under memory pressure.

Root cause

When a backup fails, BackupStarter::onException runs the cleanup (marking the backup corrupted, notifying coordination, removing files) before writing the terminal status. Any of those steps can throw a second exception — under memory pressure even an allocation in the cleanup fails — and that exception escaped onException before setStatusSafe(..., BACKUP_FAILED) ran. In the async path the exception disappeared into a discarded future, so nothing was recorded anywhere: no terminal status, no system.backup_log row, and BackupsWorker::wait blocked forever.

Fix

Wrap the cleanup of both BackupStarter::onException and RestoreStarter::onException in try/catch (the cleanup failure is logged), so the terminal status is always written afterwards; std::current_exception at that point again refers to the original error, so the recorded status/exception are unchanged. Also widen the async runner's catch (const std::exception &) to catch (...), matching the sync path.

A new backup_cleanup_error failpoint injects a cleanup failure deterministically; the stateless test 04618_backup_failed_status_on_cleanup_error uses it to verify the status still reaches BACKUP_FAILED.

Verified by code-path analysis plus the included test; no local build was run, so correctness relies on CI.

Changelog category (leave one):

  • Bug Fix (user-visible misbehavior in an official stable release)

Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):

Fixed a failed BACKUP or RESTORE staying in the CREATING_BACKUP/RESTORING status forever when the cleanup after the failure threw a second exception; the terminal status is now always set.

Onyx2406 added 3 commits July 19, 2026 19:34
If the cleanup after a failed backup threw a second exception (e.g. under memory pressure), the exception escaped before the terminal status was written, leaving the operation CREATING_BACKUP in system.backups forever and blocking clients waiting for it. Wrap the cleanup so the terminal status is always set, and catch all exceptions in the async runner.
Restore two em dashes corrupted by an editor round-trip, move the fault injection after setIsCorrupted so the BackupImpl destructor check is not tripped in the test, and make the test no-parallel because the failpoint is server-global.
The `try`/`catch` around the cleanup was not enough to guarantee a
terminal status: the log message formatting before it, the formatting in
the `catch` handler and the allocations inside `setStatus` (exception
message with a stack trace, backup log entry) could all throw
`MEMORY_LIMIT_EXCEEDED` again and escape `onException`, reproducing the
stuck-status hang the previous commits fixed. `LockMemoryExceptionInThread`
suppresses memory limit exceptions for the whole scope, the established
idiom for such cleanup paths.
@jkartseva jkartseva self-assigned this Jul 20, 2026
@jkartseva jkartseva added the can be tested Allows running workflows for external contributors label Jul 27, 2026
@clickhouse-gh

clickhouse-gh Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Workflow [PR], commit [808e092]

Summary:

job_name test_name status info comment
Style check FAIL
functional_tests_check FAIL cidb
catch_all FAIL cidb
Finish Workflow FAIL
python3 ./ci/jobs/scripts/workflow_hooks/new_tests_check.py FAIL
Code Review DROPPED
Fast test (arm_darwin) DROPPED
Build (amd_debug) DROPPED
Build (amd_asan_ubsan) DROPPED
Build (amd_tsan) DROPPED
Build (amd_msan) DROPPED
Build (amd_binary) DROPPED
Build (arm_debug) DROPPED

@clickhouse-gh clickhouse-gh Bot added the pr-bugfix Pull request with bugfix, not backported by default label Jul 27, 2026
(is_internal_backup ? "internal backup" : "backup"), backup_name_for_logging));
}

backups_worker.setStatusSafe(backup_id, getBackupStatusFromCurrentException());

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.

setStatusSafe name is misleading: it can throw since it allocates
I makes sense to add a try-catch

  void BackupsWorker::setStatusSafe(const String & id, BackupStatus status) noexcept
  {
      try
      {
          setStatus(id, status, /* throw_if_error = */ false);
      }
      catch (...)
      {
          tryLogCurrentException(log, fmt::format("Failed to set status for {}", id));
      }
  }


$CLICKHOUSE_CLIENT -q "SYSTEM ENABLE FAILPOINT backup_cleanup_error"

$CLICKHOUSE_CLIENT -q "BACKUP TABLE ${CLICKHOUSE_DATABASE}.table_does_not_exist TO Disk('backups', '${backup_name}')" 2>/dev/null || echo 'backup failed'

@jkartseva jkartseva Jul 27, 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.

Please add a test for the RESTORE path.

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

Labels

can be tested Allows running workflows for external contributors pr-bugfix Pull request with bugfix, not backported by default

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Aborted backup still showing with status CREATING_BACKUP in system.backups

2 participants