Missing throw in generateUUIDForTable lets RESTORE silently reuse backup UUIDs
I maintainer di solito rispondono entro 1 giorno
Valutazione
- Difficoltà
- 2/5
- Tempo stimato
- 1-3 ore
- Idoneità per principianti
- 73/100
- Tipo di issue
- Bug
- Chiarezza
- Specificata chiaramente
- Stato di attività
- Attiva
- Stack tecnologico
- clickhouse, cpp, postgresql
- Ambito
- databases, distributed-systems
Direzione di ricerca
Inizia da src/Backups/RestoreCoordinationOnCluster.cpp:424 in generateUUIDForTable, poi leggi le chiamate da RestorerFromBackup::createTable e createDatabase. Esegui la riproduzione oppure aggiungi il caso di regressione descritto, eliminando il nodo Keeper table_uuids durante RESTORE. Il lavoro è completato quando RESTORE fallisce con un errore di coordinamento dovuto al nodo mancante, invece di riuscire con l’UUID del backup.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Describe what's wrong
RESTORE ... ON CLUSTERreports RESTORED and reuses the backup's table UUID instead of failing when<restore>/table_uuidsis missing in Keeper.- Trigger is the ZooKeeper node
table_uuidsvanishing mid-restore, sotryCreatereturns ZNONODE. - Expected: the restore fails with a coordination error about the missing node; actual: it succeeds and silently keeps the old UUID.
- Once this happens, every later table and database on that host takes the same silent path, since the node is never recreated.
Root cause: In RestoreCoordinationOnCluster.cpp:424 inside generateUUIDForTable, the KeeperException built for ZNONODE is never thrown, so the lambda returns normally, retryLoop treats it as success, and the backup UUID is kept instead of regenerated.
Analysis details (evidence, affected locations, impact)
Why we believe this is a bug: RestorerFromBackup::createTable (RestorerFromBackup.cpp:899) and createDatabase (:588) call generateUUIDForTable. There tryCreate returns ZNONODE, line 424 builds the exception without throw, the lambda returns normally, retryLoop counts that as success, and neither copyToQuery branch runs.
Affected locations:
src/Backups/RestoreCoordinationOnCluster.cpp:424— generateUUIDForTable: KeeperException constructed for ZNONODE but not thrown
Impact: - The host silently skips the cross-host UUID agreement. The header contract (RestoreCoordinationOnCluster.h:75-76) needs one UUID per replicated table on every replica for {uuid} ZooKeeper paths to work, so hosts that hit the branch end up on a different ZooKeeper path from hosts that did not.
- Once
table_uuidsis gone, every later table and database on that host takes the silent branch, becausecreateRootNodes(line 73) creates the node once and nothing re-creates it. The line #126973 (where this was found; not confirmed as the cause) fixed is less exposed: therecreateAncestorsre-creates the parent right beforetryCreate. - Trigger: the node has to vanish mid-restore. The only in-tree remover is
BackupCoordinationCleaner::cleanup, gated onallHostsFinished, so in practice this means manual deletion or Keeper data loss.
Does it reproduce on most recent release?
Confirmed on master; not checked on a release.
How to reproduce
Reproducing requires continuously deleting the table_uuids Keeper node during the RESTORE, since the in-tree cleaner normally only removes it after all hosts finish.
import threading
import uuid
import pytest
from kazoo.exceptions import NoNodeError, NotEmptyError
from helpers.cluster import ClickHouseCluster
cluster = ClickHouseCluster(__file__)
node = cluster.add_instance(
"node",
main_configs=["configs/cluster.xml"],
external_dirs=["/backups/"],
with_zookeeper=True,
)
@pytest.fixture(scope="module", autouse=True)
def started_cluster():
try:
cluster.start()
yield cluster
finally:
cluster.shutdown()
def test_restore_fails_if_table_uuids_node_is_missing():
node.query("CREATE TABLE t (x UInt32) ENGINE = MergeTree ORDER BY x")
node.query("INSERT INTO t VALUES (1), (2), (3)")
node.query("BACKUP TABLE t TO Disk('backups', 'b1')")
node.query("DROP TABLE t SYNC")
restore_uuid = str(uuid.uuid4())
path = f"/clickhouse/backups/restore-{restore_uuid}/table_uuids"
zk = cluster.get_kazoo_client("zoo1")
stop = threading.Event()
def remove_table_uuids_node():
while not stop.is_set():
try:
zk.delete(path)
except (NoNodeError, NotEmptyError):
pass
thread = threading.Thread(target=remove_table_uuids_node)
thread.start()
try:
error = node.query_and_get_error(
f"RESTORE TABLE t ON CLUSTER 'cluster1' FROM Disk('backups', 'b1') "
f"SETTINGS restore_uuid = '{restore_uuid}'"
)
finally:
stop.set()
thread.join()
assert "No node" in error
Files next to test.py:
configs/cluster.xml:
<clickhouse>
<remote_servers>
<cluster1>
<shard>
<replica>
<host>node</host>
<port>9000</port>
</replica>
</shard>
</cluster1>
</remote_servers>
<storage_configuration>
<disks>
<backups>
<type>local</type>
<path>/backups/</path>
</backups>
</disks>
</storage_configuration>
<backups>
<allowed_disk>backups</allowed_disk>
</backups>
</clickhouse>
The same steps without the integration-test harness, using the official Docker images:
#!/usr/bin/env bash
set -u
IMAGE=${IMAGE:-clickhouse/clickhouse-server:head}
NAME=ch126973
DIR=$(mktemp -d)
cat > "$DIR/repro.xml" <<'EOF'
<clickhouse>
<keeper_server>
<tcp_port>9181</tcp_port>
<server_id>1</server_id>
<log_storage_path>/var/lib/clickhouse/coordination/log</log_storage_path>
<snapshot_storage_path>/var/lib/clickhouse/coordination/snapshots</snapshot_storage_path>
<raft_configuration>
<server><id>1</id><hostname>localhost</hostname><port>9234</port></server>
</raft_configuration>
</keeper_server>
<zookeeper><node><host>localhost</host><port>9181</port></node></zookeeper>
<remote_servers>
<c1><shard><replica><host>localhost</host><port>9000</port></replica></shard></c1>
</remote_servers>
<storage_configuration>
<disks><backups><type>local</type><path>/backups/</path></backups></disks>
</storage_configuration>
<backups><allowed_disk>backups</allowed_disk></backups>
</clickhouse>
EOF
docker rm -f $NAME >/dev/null 2>&1
docker run -d --name $NAME -v "$DIR/repro.xml:/etc/clickhouse-server/config.d/repro.xml" "$IMAGE" >/dev/null
Q() { docker exec $NAME clickhouse-client -q "$1"; }
for i in $(seq 60); do Q "SELECT 1" >/dev/null 2>&1 && break; sleep 1; done
Q "SELECT version()"
Q "CREATE TABLE t (x UInt32) ENGINE = MergeTree ORDER BY x"
Q "INSERT INTO t VALUES (1), (2), (3)"
echo "original uuid: $(Q "SELECT uuid FROM system.tables WHERE database = 'default' AND name = 't'")"
Q "BACKUP TABLE t TO Disk('backups', 'b1')" >/dev/null
Q "DROP TABLE t SYNC"
# Control: a normal RESTORE ON CLUSTER gives the table a new UUID.
Q "RESTORE TABLE t ON CLUSTER c1 FROM Disk('backups', 'b1')" >/dev/null
echo "control uuid: $(Q "SELECT uuid FROM system.tables WHERE database = 'default' AND name = 't'")"
Q "DROP TABLE t SYNC"
# Same RESTORE while the coordination node <restore>/table_uuids keeps disappearing.
R=$(cat /proc/sys/kernel/random/uuid)
docker exec -d $NAME bash -c "timeout 20 bash -c \"yes 'rm /clickhouse/backups/restore-$R/table_uuids' | clickhouse keeper-client -p 9181 --tests-mode\" >/dev/null 2>&1"
sleep 1
Q "RESTORE TABLE t ON CLUSTER c1 FROM Disk('backups', 'b1') SETTINGS restore_uuid = '$R'"
echo "restored uuid: $(Q "SELECT uuid FROM system.tables WHERE database = 'default' AND name = 't'")"
Q "SELECT count() FROM t"
Q "SELECT name, value, last_error_message FROM system.errors WHERE name = 'KEEPER_EXCEPTION' AND last_error_message LIKE '%table_uuids%'"
docker rm -f $NAME >/dev/null
rm -rf "$DIR"
Expected behavior
RESTORE should fail with a coordination error like 'No node, path /clickhouse/backups/restore-/table_uuids/...'.
Expected output of the integration test above:
RESTORE fails with `Coordination error: No node, path /clickhouse/backups/restore-<uuid>/table_uuids/...` and the test passes. Without the deleter, the restored table gets a new UUID (control above: d04b4410-... != c8aeceab-...).
Error message and/or stacktrace
RESTORE reports RESTORED and the table ends up with the original backup UUID instead of a new one, with only a KEEPER_EXCEPTION row in system.errors as a trace.
Actual output of the integration test above:
Integration test, run 1:
E helpers.client.QueryRuntimeException: Client expected to be failed but succeeded! stdout: 29e48b75-b345-443e-9295-759094eca395 RESTORED
Integration test, run 2:
E helpers.client.QueryRuntimeException: Client expected to be failed but succeeded! stdout: 89ea8732-6be6-4f65-bc2a-b79f59cd0b5f RESTORED
Standalone script on clickhouse/clickhouse-server:head (26.11.1.353):
ori
Suggested fix
throw zkutil::KeeperException::fromPath(res, path); at RestoreCoordinationOnCluster.cpp:424. No trade-off: ZNONODE is not retried by retryLoop, so the RESTORE fails the same way the KeeperMap path now does.
Additional context
Open risks:
- The proof test relies on a background thread deleting
table_uuidsbetween the internal host'sstartupand its firstgenerateUUIDForTable. It was deterministic in 4 of 4 local runs, but it is timing-based.
Found during automated review of PR #126973; this finding was not executed, so nobody is tagged. The cited lines predate that PR: per git blame they were introduced by #109327 (Fix EmbeddedRocksDB backup to include table data). Severity P2 · Finding h_pr126973_001
- Lingua principale
- C++
- Stelle
- 50.5k
- Fork
- 9.1k
- Merge medio
- 20h 1m
- PR unite (30g)
- 625
Preparare l'ambiente
- Nessun Dockerfile né file Docker Compose
- Ha un modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di ClickHouse/ClickHouse
-
ai_p2 comp-datalake
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
ClickHouse/ClickHouse#127437 ·
I maintainer di solito rispondono entro 1 giorno
-
ai_p2 comp-dictionary
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
ClickHouse/ClickHouse#127261 ·
I maintainer di solito rispondono entro 1 giorno
-
countSubstrings*(col, const, start_pos) undercounts after a straddling matchForse già presa @fallintoplace l’ha presa 1 giorno fa. Apertaai_p3 comp-regular-function
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
ClickHouse/ClickHouse#127188 ·
I maintainer di solito rispondono entro 1 giorno
-
ai_p3 comp-ci-infrastructure
Difficoltà 2/5 1-3 ore Idoneità per principianti 86/100
ClickHouse/ClickHouse#126502 ·
I maintainer di solito rispondono entro 1 giorno
-
ai_p3 comp-data-types
Difficoltà 2/5 1-3 ore Idoneità per principianti 74/100
ClickHouse/ClickHouse#126316 ·
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di ClickHouse/ClickHouse
Issue simili
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
-
macOS: a double-click on a notarized altairsim is refused, and the docs give only the offline caseForse già presa @anhaoXie l’ha presa oggi. Apertadocumentation
Difficoltà 2/5 1-3 ore Idoneità per principianti 88/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
mutationpp/Mutationpp#337 ·
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
bulletphysics/bullet3#4819 ·
-
[YSQL] Local-node initdb reads postgres.bki, but YB clusters bootstrap only from yb_postgres.bkiApertaarea/ysql kind/enhancement priority/low status/awaiting-triage
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
yugabyte/yugabyte-db#35763 ·
I maintainer di solito rispondono entro 1 giorno