Files
Alexey MilovidovandClaude Opus 4.8 ac81781fb3 Strengthen the regression test for #83187
Cover all backup destinations (File, local disk, s3_plain, object storage), and
after restarting with the backup deleted, assert that the `Backup` database is
still attached but loaded without tables, and that the original database is
unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-29 10:54:01 +00:00

215 lines
6.9 KiB
Python

import pytest
from helpers.cluster import ClickHouseCluster
from helpers.s3_tools import list_s3_objects
cluster = ClickHouseCluster(__file__)
instance = cluster.add_instance(
"instance",
main_configs=["configs/backups.xml"],
stay_alive=True,
with_minio=True,
)
@pytest.fixture(scope="module", autouse=True)
def start_cluster():
try:
cluster.start()
yield cluster
finally:
cluster.shutdown()
def cleanup_backup_files(instance):
instance.exec_in_container(["bash", "-c", "rm -rf /backups/"])
instance.exec_in_container(["bash", "-c", "rm -rf /local_plain/"])
minio = cluster.minio_client
s3_objects = list_s3_objects(minio, cluster.minio_bucket, prefix="")
for s3_object in s3_objects:
minio.remove_object(cluster.minio_bucket, s3_object)
@pytest.mark.parametrize(
"backup_destination",
[
"File('test_database_backup_file')",
"Disk('backup_disk_local', 'test_database_backup')",
"Disk('backup_disk_s3_plain', 'test_database_backup')",
"Disk('backup_disk_object_storage_local_plain', 'test_database_backup')",
],
)
def test_database_backup_database(backup_destination):
cleanup_backup_files(instance)
instance.query(
f"""
DROP DATABASE IF EXISTS test_database;
DROP DATABASE IF EXISTS test_database_backup;
CREATE DATABASE test_database;
CREATE TABLE test_database.test_table_1 (id UInt64, value String) ENGINE=MergeTree ORDER BY id;
INSERT INTO test_database.test_table_1 VALUES (0, 'test_database.test_table_1');
CREATE TABLE test_database.test_table_2 (id UInt64, value String) ENGINE=MergeTree ORDER BY id;
INSERT INTO test_database.test_table_2 VALUES (0, 'test_database.test_table_2');
CREATE TABLE test_database.test_table_3 (id UInt64, value String) ENGINE=MergeTree ORDER BY id;
INSERT INTO test_database.test_table_3 VALUES (0, 'test_database.test_table_3');
BACKUP DATABASE test_database TO {backup_destination};
CREATE DATABASE test_database_backup ENGINE = Backup('test_database', {backup_destination});
"""
)
assert (
instance.query("SELECT id, value FROM test_database_backup.test_table_1")
== "0\ttest_database.test_table_1\n"
)
assert (
instance.query("SELECT id, value FROM test_database_backup.test_table_2")
== "0\ttest_database.test_table_2\n"
)
assert (
instance.query("SELECT id, value FROM test_database_backup.test_table_3")
== "0\ttest_database.test_table_3\n"
)
instance.restart_clickhouse()
assert (
instance.query("SELECT id, value FROM test_database_backup.test_table_1")
== "0\ttest_database.test_table_1\n"
)
assert (
instance.query("SELECT id, value FROM test_database_backup.test_table_2")
== "0\ttest_database.test_table_2\n"
)
assert (
instance.query("SELECT id, value FROM test_database_backup.test_table_3")
== "0\ttest_database.test_table_3\n"
)
instance.query("DROP DATABASE test_database_backup")
instance.query("DROP DATABASE test_database")
cleanup_backup_files(instance)
@pytest.mark.parametrize(
"backup_destination",
[
"File('test_table_backup_file')",
"Disk('backup_disk_local', 'test_table_backup')",
"Disk('backup_disk_s3_plain', 'test_table_backup')",
"Disk('backup_disk_object_storage_local_plain', 'test_table_backup')",
],
)
def test_database_backup_table(backup_destination):
cleanup_backup_files(instance)
instance.query(
f"""
DROP DATABASE IF EXISTS test_database;
DROP DATABASE IF EXISTS test_table_backup;
CREATE DATABASE test_database;
CREATE TABLE test_database.test_table (id UInt64, value String) ENGINE=MergeTree ORDER BY id;
INSERT INTO test_database.test_table VALUES (0, 'test_database.test_table');
BACKUP TABLE test_database.test_table TO {backup_destination};
CREATE DATABASE test_table_backup ENGINE = Backup('test_database', {backup_destination});
"""
)
assert (
instance.query("SELECT id, value FROM test_table_backup.test_table")
== "0\ttest_database.test_table\n"
)
instance.restart_clickhouse()
assert (
instance.query("SELECT id, value FROM test_table_backup.test_table")
== "0\ttest_database.test_table\n"
)
instance.query("DROP DATABASE test_table_backup")
instance.query("DROP DATABASE test_database")
cleanup_backup_files(instance)
@pytest.mark.parametrize(
"backup_destination",
[
"File('test_database_backup_file')",
"Disk('backup_disk_local', 'test_database_backup')",
"Disk('backup_disk_s3_plain', 'test_database_backup')",
"Disk('backup_disk_object_storage_local_plain', 'test_database_backup')",
],
)
def test_database_backup_unavailable_but_server_starts(backup_destination):
# Regression test for https://github.com/ClickHouse/ClickHouse/issues/83187
# When a Backup database refers to a backup that became unavailable (e.g. the backup
# files were deleted or the underlying storage is inaccessible), the server must still
# start. The Backup database is loaded without any tables.
cleanup_backup_files(instance)
instance.query(
f"""
DROP DATABASE IF EXISTS test_database SYNC;
DROP DATABASE IF EXISTS test_database_backup SYNC;
CREATE DATABASE test_database;
CREATE TABLE test_database.test_table (id UInt64, value String) ENGINE=MergeTree ORDER BY id;
INSERT INTO test_database.test_table VALUES (0, 'test_database.test_table');
BACKUP DATABASE test_database TO {backup_destination};
CREATE DATABASE test_database_backup ENGINE = Backup('test_database', {backup_destination});
"""
)
assert (
instance.query("SELECT id, value FROM test_database_backup.test_table")
== "0\ttest_database.test_table\n"
)
# Make the backup unavailable and restart the server.
cleanup_backup_files(instance)
instance.restart_clickhouse()
# The server must start despite the unavailable backup.
assert instance.query("SELECT 1") == "1\n"
# The Backup database is still attached, but loaded without any tables.
assert (
instance.query(
"SELECT name FROM system.databases WHERE name = 'test_database_backup'"
)
== "test_database_backup\n"
)
assert (
instance.query(
"SELECT count() FROM system.tables WHERE database = 'test_database_backup'"
)
== "0\n"
)
# The original (non-backup) database is unaffected by the unavailable backup.
assert (
instance.query("SELECT id, value FROM test_database.test_table")
== "0\ttest_database.test_table\n"
)
instance.query("DROP DATABASE IF EXISTS test_database_backup SYNC")
instance.query("DROP DATABASE IF EXISTS test_database SYNC")
cleanup_backup_files(instance)