Files
zooqueen 0f1e1b5143 merge: ClickHouse 26.6.1.1 into datastore — preserve debrand + ZAP + gRPC/gcloud removal
Merge upstream/changelog-26.6 (9593 commits) into our debranded fork.
- 190 conflicts resolved: 24 delete-side kept-removed (grpc/arrowflight/gcloud), submodules+version→26.6,
  contrib/CMakeLists keeps zap + new libpng, drops capnproto (zap provides kj), Server.cpp/CMakeLists
  re-apply the ZAP wiring + gRPC/arrowflight/gcloud removal.
- Full-tree debrand re-swept over the new 26.6 code (uniform Datastore naming; Azure fork Clickhouse*
  symbols preserved). 0 clickhouse in product src/programs/CMakeLists.
- Version now 26.6.1.1. gRPC/google-cloud/chdig stay removed; ZAP remains the transport.
2026-06-23 12:23:48 -07:00

201 lines
8.4 KiB
Python

import os
import re
import time
def generate_keeper_configs(configs_dir, clusters):
"""Generate Keeper XML config files for the given cluster definitions.
clusters is a list of (filenames, hosts, chunk_size[, use_s3_disk_primary]) tuples where:
- filenames: output XML file names, one per server
- hosts: hostname for each server (same length as filenames)
- chunk_size: snapshot_transfer_chunk_size value, or None to omit
- use_s3_disk_primary: (optional, default False) use an S3 plain disk as the *primary*
snapshot disk so that RemoteSnapshotLoader is exercised during
chunked transfer. Each server gets its own prefix inside the
"root" bucket (pre-created by the test framework).
"""
def make_config(server_id, hosts, chunk_size, use_s3_disk_primary):
if use_s3_disk_primary:
# Each server gets its own S3 path so snapshot objects don't collide across clusters.
endpoint = f"http://minio1:9001/root/keeper-snapshots/{hosts[server_id - 1]}/"
storage_block = (
"\n<storage_configuration>"
"\n <disks>"
"\n <keeper_snap_s3>"
"\n <type>s3_plain</type>"
f"\n <endpoint>{endpoint}</endpoint>"
"\n <access_key_id>minio</access_key_id>"
"\n <secret_access_key>Datastore_Minio_P@ssw0rd</secret_access_key>"
"\n </keeper_snap_s3>"
"\n </disks>"
"\n</storage_configuration>"
)
snapshot_disk_line = "\n <snapshot_storage_disk>keeper_snap_s3</snapshot_storage_disk>"
else:
storage_block = ""
snapshot_disk_line = ""
chunk_line = (
f"\n <snapshot_transfer_chunk_size>{chunk_size}</snapshot_transfer_chunk_size>"
if chunk_size else ""
)
# Assign decreasing priorities: first node is most likely to become leader.
base_prios = [70, 20, 10, 5, 3]
prios = base_prios[:len(hosts)]
servers = []
for i, (host, prio) in enumerate(zip(hosts, prios), start=1):
follower = "\n <start_as_follower>true</start_as_follower>" if i > 1 else ""
servers.append(
f" <server>\n"
f" <id>{i}</id>\n"
f" <hostname>{host}</hostname>\n"
f" <port>9234</port>\n"
f" <can_become_leader>true</can_become_leader>{follower}\n"
f" <priority>{prio}</priority>\n"
f" </server>"
)
return (
"<datastore>\n"
+ (f"{storage_block}\n" if storage_block else "")
+ f" <keeper_server>{snapshot_disk_line}\n"
f" <tcp_port>9181</tcp_port>\n"
f" <server_id>{server_id}</server_id>\n"
f"\n"
f" <coordination_settings>\n"
f" <operation_timeout_ms>5000</operation_timeout_ms>\n"
f" <session_timeout_ms>10000</session_timeout_ms>\n"
f" <raft_logs_level>trace</raft_logs_level>\n"
f" <snapshot_distance>50</snapshot_distance>\n"
f" <stale_log_gap>10</stale_log_gap>\n"
f" <reserved_log_items>1</reserved_log_items>{chunk_line}\n"
f" </coordination_settings>\n"
f"\n"
f" <raft_configuration>\n"
+ "\n".join(servers) + "\n"
" </raft_configuration>\n"
" </keeper_server>\n"
"</datastore>\n"
)
os.makedirs(configs_dir, exist_ok=True)
# Always write the small remote read-buffer config so that ReadBufferFromS3::nextImpl
# is called multiple times per readStrict, making failpoints reachable and stressing
# RemoteSnapshotLoader under realistic multi-chunk I/O conditions.
small_buf_path = os.path.join(configs_dir, "small_remote_buf_user.xml")
with open(small_buf_path, "w") as f:
f.write(
"<datastore>\n<profiles>\n <default>\n"
" <max_read_buffer_size_remote_fs>1024</max_read_buffer_size_remote_fs>\n"
" </default>\n</profiles>\n</datastore>\n"
)
for cluster_def in clusters:
filenames, hosts, chunk_size = cluster_def[:3]
use_s3_disk_primary = cluster_def[3] if len(cluster_def) > 3 else False
for server_id, filename in enumerate(filenames, start=1):
path = os.path.join(configs_dir, filename)
with open(path, "w") as f:
f.write(make_config(server_id, hosts, chunk_size, use_s3_disk_primary))
def stop_zk(zk):
try:
if zk:
zk.stop()
zk.close()
except Exception:
pass
def fill_test_tree(zk, base, count=300):
import os as _os
zk.ensure_path(base)
for i in range(count):
zk.create(f"{base}/{i}", _os.urandom(1024)) # random to resist ZSTD compression
for i in range(0, count, 10):
zk.delete(f"{base}/{i}")
def cleanup_test_tree(cluster, leader_node, base):
import helpers.keeper_utils as keeper_utils
zk = None
try:
zk = keeper_utils.get_fake_zk(cluster, leader_node.name)
if zk.exists(base):
zk.delete(base, recursive=True)
except Exception:
pass
finally:
stop_zk(zk)
def verify_test_tree(leader_zk, lagging_zk, base, count=300):
leader_zk.sync(base)
lagging_zk.sync(base)
for i in range(count):
if i % 10 != 0:
assert lagging_zk.get(f"{base}/{i}")[0] == leader_zk.get(f"{base}/{i}")[0]
else:
assert lagging_zk.exists(f"{base}/{i}") is None
def get_kill_timestamp(node):
return node.query("SELECT now64(6)").strip()
def _query_text_log(node, after_time, pattern, timeout=15):
deadline = time.time() + timeout
while True:
try:
node.query("SYSTEM FLUSH LOGS")
result = node.query(
f"SELECT message FROM system.text_log "
f"WHERE event_time_microseconds > '{after_time}' "
f"AND message LIKE '{pattern}' "
f"ORDER BY event_time_microseconds"
).strip()
if result:
return [line for line in result.splitlines() if line]
except Exception:
pass
if time.time() >= deadline:
return []
time.sleep(1)
def get_received_snapshot_info(node, after_time, timeout=15):
lines = _query_text_log(node, after_time, "Saved snapshot % chunks, % bytes)", timeout)
if not lines:
return None
m = re.search(r"Saved snapshot (\d+) \((\d+) chunks, (\d+) bytes\)", lines[-1])
if not m:
return None
return int(m.group(1)), int(m.group(2)), int(m.group(3))
def get_snapshot_log_lines_for_idx(node, snapshot_log_idx, after_time, timeout=15):
return _query_text_log(
node, after_time, f"Saving snapshot {snapshot_log_idx} obj_id %", timeout
)
def assert_receiving_snapshot_logged(node_lagging, after_time, disk_type):
"""Assert that the follower logged receiving a snapshot to the expected disk type ("local" or "remote")."""
pattern = f"Receiving snapshot % to {disk_type} disk"
lines = _query_text_log(node_lagging, after_time, pattern, timeout=15)
assert lines, f"Expected '{pattern}' in system.text_log on {node_lagging.name}"
def assert_obj_ids(node_lagging, snapshot_log_idx, expected, after_time):
lines = get_snapshot_log_lines_for_idx(node_lagging, snapshot_log_idx, after_time)
assert lines, "No 'Saving snapshot' log lines appeared during recovery"
all_ids = [int(m.group(1)) for line in lines if (m := re.search(r"obj_id (\d+)", line))]
duplicates = len(all_ids) - len(set(all_ids))
# NuRaft may re-send a chunk when a heartbeat fires before the first ACK returns;
# tolerate at most len(expected)//2 duplicates to catch systematic bugs.
max_allowed = len(expected) // 2
assert set(all_ids) == set(expected), f"Expected obj_ids={set(expected)}, got: {sorted(set(all_ids))}"
assert duplicates <= max_allowed, \
f"Too many duplicate chunks: {duplicates} (max {max_allowed}), obj_ids={all_ids}"