mirror of
https://github.com/hanzoai/datastore.git
synced 2026-09-26 13:55:07 +00:00
804 lines
26 KiB
Python
804 lines
26 KiB
Python
import argparse
|
|
import atexit
|
|
import logging
|
|
import mmap
|
|
import os
|
|
import pathlib
|
|
import random
|
|
import re
|
|
import subprocess
|
|
import tempfile
|
|
import time
|
|
import signal
|
|
import sys
|
|
|
|
logging.basicConfig(
|
|
level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
)
|
|
from integration.helpers.cluster import ZOOKEEPER_CONTAINERS
|
|
from sparkserver import (
|
|
get_unique_free_ports,
|
|
create_spark_http_server,
|
|
close_spark_http_server,
|
|
)
|
|
|
|
# Needs to get free ports before importing ClickHouseCluster
|
|
os.environ["WORKER_FREE_PORTS"] = " ".join([str(p) for p in get_unique_free_ports(50)])
|
|
|
|
from environment import set_environment_variables
|
|
from integration.helpers.cluster import ClickHouseCluster, ClickHouseInstance
|
|
from integration.helpers.postgres_utility import get_postgres_conn
|
|
from integration.helpers.s3_tools import (
|
|
AzureUploader,
|
|
LocalUploader,
|
|
S3Uploader,
|
|
LocalDownloader,
|
|
prepare_s3_bucket,
|
|
)
|
|
from integration.helpers.config_cluster import minio_access_key, minio_secret_key
|
|
from generators import Generator, BuzzHouseGenerator
|
|
from leaks import ElOracloDeLeaks
|
|
from oracles import ElOraculoDeTablas
|
|
from properties import (
|
|
modify_server_settings,
|
|
modify_user_settings,
|
|
modify_keeper_settings,
|
|
)
|
|
|
|
|
|
def ordered_pair(value):
|
|
try:
|
|
x, y = map(int, value.split(","))
|
|
if x > y:
|
|
raise argparse.ArgumentTypeError(
|
|
f"First value must be less than second (got {x} > {y})"
|
|
)
|
|
return (x, y)
|
|
except ValueError:
|
|
raise argparse.ArgumentTypeError(
|
|
"Must be two comma-separated integers (e.g., '1,10')"
|
|
)
|
|
|
|
|
|
def list_of_values(arg):
|
|
return arg.split(",")
|
|
|
|
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument(
|
|
"--server-settings-prob",
|
|
type=int,
|
|
default=80,
|
|
choices=range(0, 101),
|
|
help="Probability to set server properties",
|
|
)
|
|
parser.add_argument(
|
|
"--add-disk-settings-prob",
|
|
type=int,
|
|
default=80,
|
|
choices=range(0, 101),
|
|
help="Probability to set random disks",
|
|
)
|
|
parser.add_argument(
|
|
"--number-disks",
|
|
type=ordered_pair,
|
|
default=(1, 5),
|
|
help="Number of disks to generate. Two ordered integers separated by comma (e.g., 1,3)",
|
|
)
|
|
parser.add_argument(
|
|
"--add-policy-settings-prob",
|
|
type=int,
|
|
default=70,
|
|
choices=range(0, 101),
|
|
help="Probability to set random storage policies",
|
|
)
|
|
parser.add_argument(
|
|
"--add-remote-server-settings-prob",
|
|
type=int,
|
|
default=80,
|
|
choices=range(0, 101),
|
|
help="Probability to set random servers",
|
|
)
|
|
parser.add_argument(
|
|
"--number-servers",
|
|
type=ordered_pair,
|
|
default=(1, 3),
|
|
help="Number of servers to generate. Two ordered integers separated by comma (e.g., 1,3)",
|
|
)
|
|
parser.add_argument(
|
|
"--add-filesystem-caches-prob",
|
|
type=int,
|
|
default=80,
|
|
choices=range(0, 101),
|
|
help="Probability to add filesystem caches",
|
|
)
|
|
parser.add_argument(
|
|
"--number-caches",
|
|
type=ordered_pair,
|
|
default=(1, 3),
|
|
help="Number of filesystem caches to generate. Two ordered integers separated by comma (e.g., 1,3)",
|
|
)
|
|
parser.add_argument(
|
|
"--change-server-version-prob",
|
|
type=int,
|
|
default=80,
|
|
choices=range(0, 101),
|
|
help="Probability to change server version after restart",
|
|
)
|
|
parser.add_argument(
|
|
"--client-binary", type=pathlib.Path, required=True, help="Path to client binary"
|
|
)
|
|
parser.add_argument(
|
|
"--server-binaries",
|
|
type=list_of_values,
|
|
required=True,
|
|
help="Path of server binaries to test",
|
|
)
|
|
parser.add_argument(
|
|
"-c", "--client-config", type=pathlib.Path, help="Path to client configuration file"
|
|
)
|
|
parser.add_argument(
|
|
"-g",
|
|
"--generator",
|
|
choices=["buzzhouse"],
|
|
type=str.lower,
|
|
required=True,
|
|
help="What generator to use",
|
|
)
|
|
parser.add_argument(
|
|
"-l",
|
|
"--log-path",
|
|
type=pathlib.Path,
|
|
default=tempfile.NamedTemporaryFile(suffix=".log"),
|
|
help="Log path",
|
|
)
|
|
parser.add_argument(
|
|
"--replica-values",
|
|
type=list_of_values,
|
|
default="1",
|
|
help="Comma separated list for replica values",
|
|
)
|
|
parser.add_argument(
|
|
"--shard-values",
|
|
type=list_of_values,
|
|
default="1",
|
|
help="Comma separated list for shard values",
|
|
)
|
|
parser.add_argument(
|
|
"--server-config", type=pathlib.Path, help="Path to config.xml file"
|
|
)
|
|
parser.add_argument("-s", "--seed", type=int, default=0, help="Server fuzzer seed")
|
|
parser.add_argument(
|
|
"-u", "--user-config", type=pathlib.Path, help="Path to users.xml file"
|
|
)
|
|
parser.add_argument(
|
|
"--kill-server-prob",
|
|
type=int,
|
|
default=50,
|
|
choices=range(0, 101),
|
|
help="Probability to kill the server instead of shutting it down",
|
|
)
|
|
parser.add_argument(
|
|
"--restart-clickhouse-prob",
|
|
type=int,
|
|
default=50,
|
|
choices=range(0, 101),
|
|
help="Probability to restart ClickHouse instead of integration servers",
|
|
)
|
|
parser.add_argument(
|
|
"--time-between-shutdowns",
|
|
type=ordered_pair,
|
|
default=(20, 30),
|
|
help="In seconds. Two ordered integers separated by comma (e.g., 30,60)",
|
|
)
|
|
parser.add_argument(
|
|
"--time-between-integration-shutdowns",
|
|
type=ordered_pair,
|
|
default=(3, 5),
|
|
help="In seconds. Two ordered integers separated by comma (e.g., 3,5)",
|
|
)
|
|
parser.add_argument(
|
|
"--without-minio",
|
|
action="store_false",
|
|
dest="with_minio",
|
|
help="Without MinIO integration",
|
|
)
|
|
parser.add_argument(
|
|
"--with-azurite", action="store_true", help="With Azure integration"
|
|
)
|
|
parser.add_argument(
|
|
"--without-zookeeper",
|
|
action="store_false",
|
|
dest="with_zookeeper",
|
|
help="Without Zookeeper server",
|
|
)
|
|
parser.add_argument(
|
|
"--with-postgresql", action="store_true", help="With PostgreSQL integration"
|
|
)
|
|
parser.add_argument("--with-mysql", action="store_true", help="With MySQL integration")
|
|
parser.add_argument("--with-nginx", action="store_true", help="With Nginx integration")
|
|
parser.add_argument(
|
|
"--with-sqlite", action="store_true", help="With SQLite integration"
|
|
)
|
|
parser.add_argument(
|
|
"--with-mongodb", action="store_true", help="With MongoDB integration"
|
|
)
|
|
parser.add_argument("--with-redis", action="store_true", help="With Redis integration")
|
|
parser.add_argument(
|
|
"--with-arrowflight", action="store_true", help="With Arrow flight support"
|
|
)
|
|
parser.add_argument("--with-kafka", action="store_true", help="With Kafka integration")
|
|
parser.add_argument(
|
|
"--mem-limit", type=str, default="", help="Set a memory limit, e.g. '1g'"
|
|
)
|
|
parser.add_argument(
|
|
"--without-keeper-map-prefix",
|
|
action="store_false",
|
|
dest="add_keeper_map_prefix",
|
|
help="Add 'keeper_map_path_prefix' server setting",
|
|
)
|
|
parser.add_argument(
|
|
"--without-transactions",
|
|
action="store_false",
|
|
dest="add_transactions",
|
|
help="Add 'allow_experimental_transactions' server setting",
|
|
)
|
|
parser.add_argument(
|
|
"--without-log-tables",
|
|
action="store_false",
|
|
dest="add_log_tables",
|
|
help="Add log tables server settings",
|
|
)
|
|
parser.add_argument(
|
|
"--without-distributed-ddl",
|
|
action="store_false",
|
|
dest="add_distributed_ddl",
|
|
help="Add 'distributed_ddl' settings",
|
|
)
|
|
parser.add_argument(
|
|
"--without-distributed-query",
|
|
action="store_false",
|
|
dest="add_distributed_query",
|
|
help="Add 'distributed_query' settings",
|
|
)
|
|
parser.add_argument(
|
|
"--without-shared-catalog",
|
|
action="store_false",
|
|
dest="add_shared_catalog",
|
|
help="Add 'shared_database_catalog' settings",
|
|
)
|
|
parser.add_argument(
|
|
"--without-database-replicated",
|
|
action="store_false",
|
|
dest="add_database_replicated",
|
|
help="Add 'database_replicated' settings",
|
|
)
|
|
parser.add_argument(
|
|
"--compare-table-dump-prob",
|
|
type=int,
|
|
default=50,
|
|
choices=range(0, 101),
|
|
help="Probability to compare contents of a table after a server restart",
|
|
)
|
|
parser.add_argument(
|
|
"--set-locales-prob",
|
|
type=int,
|
|
default=50,
|
|
choices=range(0, 101),
|
|
help="Probability to send a random locale to all instances in a cluster",
|
|
)
|
|
parser.add_argument(
|
|
"--set-timezones-prob",
|
|
type=int,
|
|
default=50,
|
|
choices=range(0, 101),
|
|
help="Probability to send a random timezone to all instances in a cluster",
|
|
)
|
|
parser.add_argument(
|
|
"--keeper-settings-prob",
|
|
type=int,
|
|
default=80,
|
|
choices=range(0, 101),
|
|
help="Probability to set keeper server properties",
|
|
)
|
|
parser.add_argument(
|
|
"--with-spark", action="store_true", help="With Spark support in Dolor HTTP server"
|
|
)
|
|
parser.add_argument(
|
|
"--with-glue", action="store_true", help="With AWS Glue catalog for Spark"
|
|
)
|
|
parser.add_argument(
|
|
"--with-rest", action="store_true", help="With Iceberg REST catalog for Spark"
|
|
)
|
|
parser.add_argument(
|
|
"--with-hms", action="store_true", help="With Hive catalog for Spark"
|
|
)
|
|
parser.add_argument(
|
|
"--with-unity",
|
|
type=pathlib.Path,
|
|
help="With Unity catalog for Spark, path to Unity dir",
|
|
)
|
|
parser.add_argument(
|
|
"--with-leak-detection", action="store_true", help="Check for memory leaks"
|
|
)
|
|
parser.add_argument(
|
|
"--time-between-leak-detections",
|
|
type=ordered_pair,
|
|
default=(20, 30),
|
|
help="In seconds. Two ordered integers separated by comma (e.g., 30,60)",
|
|
)
|
|
parser.add_argument(
|
|
"--set-shared-mergetree-disk",
|
|
action="store_true",
|
|
help="Set shared merge tree disk or policy",
|
|
)
|
|
parser.add_argument(
|
|
"--without-monitoring",
|
|
action="store_false",
|
|
dest="with_monitoring",
|
|
help="Remove periodic monitoring of the cluster",
|
|
)
|
|
parser.add_argument(
|
|
"--time-between-monitoring-runs",
|
|
type=ordered_pair,
|
|
default=(5, 10),
|
|
help="In seconds. Two ordered integers separated by comma (e.g., 30,60)",
|
|
)
|
|
UNSET = object()
|
|
parser.add_argument(
|
|
"--timeout",
|
|
type=int,
|
|
default=UNSET,
|
|
help="Total time to run the test in minutes (the test will stop after this time)",
|
|
)
|
|
parser.add_argument(
|
|
"--tmp-files-dir",
|
|
type=pathlib.Path,
|
|
default=pathlib.Path("/tmp"),
|
|
help="Path to temporary files dir",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
if len(args.replica_values) != len(args.shard_values):
|
|
raise Exception(
|
|
f"The length of replica values {len(args.replica_values)} is not the same as shard values {len(args.shard_values)}"
|
|
)
|
|
|
|
logging.basicConfig(
|
|
filename=args.log_path,
|
|
filemode="w",
|
|
level=logging.INFO,
|
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
force=True,
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Set seed first
|
|
seed = args.seed
|
|
if seed == 0:
|
|
import secrets
|
|
|
|
seed = secrets.randbits(64) # 64 - bit random integer
|
|
random.seed(seed)
|
|
logger.info(f"Using seed: {seed}")
|
|
|
|
sorted_binaries = []
|
|
# Start the cluster, by using one of the server binaries
|
|
if len(args.server_binaries) > 1 and random.randint(1, 100) <= 90:
|
|
# Pick the lowest version most of the times
|
|
def get_clickhouse_version(binary_path):
|
|
result = subprocess.run(
|
|
[binary_path, "--version"], capture_output=True, text=True
|
|
)
|
|
# Output like: "ClickHouse client version 24.3.1.2 (official build)."
|
|
match = re.search(r"version (\d+\.\d+\.\d+\.?\d*)", result.stdout)
|
|
if match:
|
|
return tuple(int(x) for x in match.group(1).split("."))
|
|
raise ValueError(f"Could not parse version from {binary_path}")
|
|
|
|
first_server = args.server_binaries[0]
|
|
lowest_version = get_clickhouse_version(first_server)
|
|
for val in args.server_binaries:
|
|
next_version = get_clickhouse_version(val)
|
|
if next_version < lowest_version:
|
|
first_server = val
|
|
lowest_version = next_version
|
|
else:
|
|
first_server = random.choice(args.server_binaries)
|
|
# Make sure the first server version is always first
|
|
sorted_binaries.append(first_server)
|
|
for val in args.server_binaries:
|
|
if val != first_server:
|
|
sorted_binaries.append(val)
|
|
|
|
# Find if private binary is being used
|
|
is_private_binary = False
|
|
with open(first_server, "rb") as f:
|
|
mm = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
|
|
is_private_binary = mm.find(b"isCoordinatedMergesTasksActivated") > -1
|
|
mm.close()
|
|
|
|
logger.info(f"Private binary {'' if is_private_binary else 'not '}detected")
|
|
keeper_configs: list[str] = modify_keeper_settings(args, is_private_binary)
|
|
|
|
if args.with_minio:
|
|
# Set environment variables before cluster starts
|
|
credentials_file = tempfile.NamedTemporaryFile(dir=args.tmp_files_dir)
|
|
os.environ["AWS_ACCESS_KEY_ID"] = "testing"
|
|
os.environ["AWS_SECRET_ACCESS_KEY"] = "testing"
|
|
os.environ["AWS_SESSION_TOKEN"] = "testing"
|
|
os.environ["AWS_REGION"] = "us-east-1"
|
|
os.environ["AWS_ENDPOINT_URL"] = "http://localhost:3000"
|
|
os.environ["MINIO_ACCESS_KEY"] = minio_access_key
|
|
os.environ["MINIO_SECRET_KEY"] = minio_secret_key
|
|
with open(credentials_file.name, "w+") as file:
|
|
file.write(
|
|
"[default]\naws_access_key_id = testing\naws_secret_access_key = testing\naws_session_token = testing\naws_region = us-east-1\naws_endpoint_url = http://localhost:3000\n"
|
|
)
|
|
os.environ["AWS_CONFIG_FILE"] = credentials_file.name
|
|
os.environ["AWS_SHARED_CREDENTIALS_FILE"] = credentials_file.name
|
|
|
|
cluster = ClickHouseCluster(
|
|
__file__,
|
|
name="dolor",
|
|
custom_keeper_configs=keeper_configs,
|
|
azurite_default_port=10000,
|
|
server_bin_path=first_server,
|
|
client_bin_path=args.client_binary,
|
|
server_binaries=sorted_binaries,
|
|
with_dolor=True,
|
|
)
|
|
|
|
# Set environment variables such as locales and timezones
|
|
test_env_variables = set_environment_variables(logger, args, "cluster")
|
|
|
|
# Use random server settings sometimes
|
|
server_settings = args.server_config
|
|
user_settings = args.user_config
|
|
modified_server_settings = modified_user_settings = False
|
|
generated_clusters = 0
|
|
if server_settings is not None:
|
|
modified_server_settings, server_settings, generated_clusters = (
|
|
modify_server_settings(args, cluster, is_private_binary, server_settings)
|
|
)
|
|
if generated_clusters > 0:
|
|
modified_user_settings, user_settings = modify_user_settings(
|
|
args, user_settings, generated_clusters
|
|
)
|
|
|
|
dolor_main_configs = [
|
|
"../config/server.crt",
|
|
"../config/server.key",
|
|
"../config/server-cert.pem",
|
|
"../config/server-key.pem",
|
|
"../config/ca-cert.pem",
|
|
"../config/dhparam.pem",
|
|
]
|
|
if server_settings is not None:
|
|
dolor_main_configs.append(server_settings)
|
|
|
|
|
|
servers: list[ClickHouseInstance] = []
|
|
for i in range(0, len(args.replica_values)):
|
|
servers.append(
|
|
cluster.add_instance(
|
|
f"node{i}",
|
|
stay_alive=True,
|
|
copy_common_configs=False,
|
|
with_zookeeper=args.with_zookeeper,
|
|
with_minio=args.with_minio,
|
|
with_nginx=args.with_nginx,
|
|
with_azurite=args.with_azurite,
|
|
with_postgres=args.with_postgresql,
|
|
with_mysql8=args.with_mysql,
|
|
with_mongo=args.with_mongodb,
|
|
with_redis=args.with_redis,
|
|
with_iceberg_catalog=args.with_rest,
|
|
with_glue_catalog=args.with_glue,
|
|
with_hms_catalog=args.with_hms,
|
|
with_arrowflight=args.with_arrowflight,
|
|
with_kafka=args.with_kafka,
|
|
mem_limit=None if args.mem_limit == "" else args.mem_limit,
|
|
main_configs=dolor_main_configs,
|
|
user_configs=[user_settings] if user_settings is not None else [],
|
|
env_variables=test_env_variables,
|
|
macros={"replica": args.replica_values[i], "shard": args.shard_values[i]},
|
|
)
|
|
)
|
|
# Copy the binaries into the containers
|
|
server_versions = {}
|
|
for server in servers:
|
|
server_versions[server.name] = first_server
|
|
cluster.start(300)
|
|
logger.info(
|
|
f"Starting cluster with {len(servers)} server(s) and server binary {first_server}"
|
|
)
|
|
for i in range(0, len(args.replica_values)):
|
|
logger.info(
|
|
f"Server node{i} running on host {servers[i].hostname}, with IPv4 {servers[i].ip_address}, port 9000"
|
|
)
|
|
servers[len(servers) - 1].wait_start(8)
|
|
servers[0].give_user_files_permissions()
|
|
|
|
# Uploaders for object storage
|
|
if args.with_minio:
|
|
prepare_s3_bucket(cluster)
|
|
cluster.default_s3_uploader = S3Uploader(cluster.minio_client, cluster.minio_bucket)
|
|
if args.with_azurite:
|
|
cluster.blob_service_client = cluster.blob_service_client
|
|
cluster.container_client = cluster.blob_service_client.create_container(
|
|
cluster.azure_container_name
|
|
)
|
|
cluster.default_azure_uploader = AzureUploader(
|
|
cluster.blob_service_client, cluster.azure_container_name
|
|
)
|
|
cluster.default_local_uploader = LocalUploader(cluster.instances["node0"])
|
|
cluster.default_local_downloader = LocalDownloader(cluster.instances["node0"])
|
|
|
|
if args.with_postgresql:
|
|
postgres_conn = get_postgres_conn(
|
|
ip=cluster.postgres_ip, port=cluster.postgres_port
|
|
)
|
|
cursor = postgres_conn.cursor()
|
|
cursor.execute("CREATE DATABASE test")
|
|
cursor.close()
|
|
postgres_conn.close()
|
|
|
|
# Handler for HTTP server
|
|
catalog_server = create_spark_http_server(cluster, args.with_unity, test_env_variables)
|
|
|
|
# Start the load generator, at the moment only BuzzHouse is available
|
|
generator: Generator = Generator(pathlib.Path(), pathlib.Path(), pathlib.Path(), None)
|
|
if args.generator == "buzzhouse":
|
|
generator = BuzzHouseGenerator(args, cluster, catalog_server, server_settings)
|
|
logger.info("Starting load generator")
|
|
client = generator.run_generator(servers[0], logger, args)
|
|
logger.info("Load generator started with PID %s", client.process.pid)
|
|
everything_cleaned = False
|
|
|
|
|
|
def dolor_cleanup():
|
|
global everything_cleaned
|
|
if not everything_cleaned:
|
|
if client.process.poll() is None:
|
|
client.process.kill()
|
|
client.process.wait()
|
|
logger.info(f"Load generator exited with code: {client.process.returncode}")
|
|
|
|
try:
|
|
cluster.shutdown(kill=True, ignore_fatal=False)
|
|
except:
|
|
pass
|
|
close_spark_http_server(catalog_server)
|
|
if modified_server_settings:
|
|
try:
|
|
os.unlink(server_settings)
|
|
except FileNotFoundError:
|
|
pass
|
|
if modified_user_settings:
|
|
try:
|
|
os.unlink(user_settings)
|
|
except FileNotFoundError:
|
|
pass
|
|
try:
|
|
os.unlink(generator.temp.name)
|
|
except FileNotFoundError:
|
|
pass
|
|
if args.with_minio:
|
|
try:
|
|
os.unlink(credentials_file.name)
|
|
except FileNotFoundError:
|
|
pass
|
|
for entry in keeper_configs:
|
|
try:
|
|
os.unlink(entry)
|
|
except FileNotFoundError:
|
|
pass
|
|
everything_cleaned = True
|
|
|
|
|
|
def my_signal_handler(sig, frame):
|
|
dolor_cleanup()
|
|
sys.exit(0)
|
|
|
|
|
|
signal.signal(signal.SIGINT, my_signal_handler)
|
|
atexit.register(dolor_cleanup)
|
|
time.sleep(3)
|
|
|
|
integrations = []
|
|
if args.with_zookeeper:
|
|
integrations.extend(["zookeeper", "zookeeper"]) # Increased probability
|
|
if args.with_minio:
|
|
integrations.append("minio")
|
|
if args.with_nginx:
|
|
integrations.append("nginx")
|
|
if args.with_azurite:
|
|
integrations.append("azurite")
|
|
if args.with_postgresql:
|
|
integrations.append("postgres")
|
|
if args.with_mysql:
|
|
integrations.append("mysql8")
|
|
if args.with_mongodb:
|
|
integrations.append("mongo")
|
|
if args.with_redis:
|
|
integrations.append("redis")
|
|
if args.with_kafka:
|
|
integrations.append("kafka")
|
|
if args.with_arrowflight:
|
|
integrations.append("arrowflight")
|
|
|
|
# This is the main loop, run while client and server are running
|
|
all_running = True
|
|
good_exit = True
|
|
tables_oracle: ElOraculoDeTablas = ElOraculoDeTablas()
|
|
# Shutdown info
|
|
lower_bound, upper_bound = args.time_between_shutdowns
|
|
integration_lower_bound, integration_upper_bound = (
|
|
args.time_between_integration_shutdowns
|
|
)
|
|
monitoring_lower_bound, monitoring_upper_bound = args.time_between_monitoring_runs
|
|
# Leak detection
|
|
leak_detector: ElOracloDeLeaks = ElOracloDeLeaks()
|
|
leak_lower_bound, leak_upper_bound = args.time_between_leak_detections
|
|
if args.with_leak_detection:
|
|
leak_detector.reset_and_capture_baseline(cluster)
|
|
# Test timeout if set
|
|
test_limit = None if args.timeout is UNSET else time.time() + args.timeout * 60
|
|
reached_limit = False
|
|
|
|
|
|
def explain_returncode(rc: int) -> str:
|
|
if rc == 0:
|
|
return "successfully (0)."
|
|
if rc < 0:
|
|
sig = -rc
|
|
try:
|
|
name = signal.Signals(sig).name
|
|
except ValueError:
|
|
name = f"SIG{sig}"
|
|
try:
|
|
reason = signal.strsignal(sig) # Py3.8+ (wording varies by platform)
|
|
except Exception:
|
|
reason = ""
|
|
extra = f" - {reason}" if reason else ""
|
|
return f"terminated by signal {sig} ({name}){extra}."
|
|
return f"with status {rc}."
|
|
|
|
|
|
while all_running and (not reached_limit):
|
|
start = time.time()
|
|
finish = start + random.randint(lower_bound, upper_bound)
|
|
next_leak_detection = start + random.randint(leak_lower_bound, leak_upper_bound)
|
|
next_monitoring = start + random.randint(
|
|
monitoring_lower_bound, monitoring_upper_bound
|
|
)
|
|
|
|
while all_running and (not reached_limit) and start < finish:
|
|
if client.process.poll() is not None:
|
|
logger.info(
|
|
f"Load generator finished {explain_returncode(client.process.returncode)}"
|
|
)
|
|
all_running = False
|
|
good_exit = good_exit and generator.validate_exit_code(
|
|
client.process.returncode
|
|
)
|
|
for server in servers:
|
|
pid = server.get_process_pid("clickhouse")
|
|
if pid is None:
|
|
logger.info(f"The server {server.name} is not running")
|
|
all_running = good_exit = False
|
|
reached_limit = test_limit is not None and time.time() >= test_limit
|
|
if reached_limit:
|
|
logger.info("Test timeout reached, stopping the load generator and exiting")
|
|
if all_running and (not reached_limit):
|
|
if args.with_leak_detection and next_leak_detection < time.time():
|
|
leak_detector.run_next_leak_detection(cluster, client)
|
|
next_leak_detection += random.randint(
|
|
leak_lower_bound, leak_upper_bound
|
|
)
|
|
if args.with_monitoring and next_monitoring < time.time():
|
|
tables_oracle.run_health_check(cluster, servers, logger)
|
|
next_monitoring += random.randint(
|
|
monitoring_lower_bound, monitoring_upper_bound
|
|
)
|
|
interval = 1
|
|
time.sleep(interval)
|
|
start += interval
|
|
|
|
if (not all_running) or reached_limit:
|
|
break
|
|
|
|
dump_table = (
|
|
tables_oracle.collect_table_hash_before_shutdown(cluster, logger)
|
|
if random.randint(1, 100) <= args.compare_table_dump_prob
|
|
else None
|
|
)
|
|
kill_server = random.randint(1, 100) <= args.kill_server_prob
|
|
# Pick one of the servers to restart
|
|
# Restart ClickHouse
|
|
if random.randint(1, 100) <= args.restart_clickhouse_prob:
|
|
next_pick = random.choice(servers)
|
|
logger.info(
|
|
f"Restarting the server {next_pick.name} with {'kill' if kill_server else 'manual shutdown'}"
|
|
)
|
|
|
|
try:
|
|
next_pick.stop_clickhouse(stop_wait_sec=10, kill=kill_server)
|
|
except Exception as ex:
|
|
logger.error(f"Failed to stop ClickHouse: {ex}")
|
|
logger.info(f"The server {next_pick.name} is not running")
|
|
all_running = good_exit = False
|
|
time.sleep(1)
|
|
# Replace server binary, using a new temporary symlink
|
|
if (
|
|
all_running
|
|
and len(sorted_binaries) > 1
|
|
and random.randint(1, 100) <= args.change_server_version_prob
|
|
):
|
|
if len(servers) == 1 and len(sorted_binaries) == 2:
|
|
# Pick the other server version
|
|
next_server = sorted_binaries[
|
|
0 if server_versions[next_pick.name] == sorted_binaries[1] else 1
|
|
]
|
|
else:
|
|
next_server = random.choice(sorted_binaries)
|
|
logger.info(f"Picked the server binary {next_server} for restart")
|
|
# Update symlink in the container
|
|
next_pick.exec_in_container(
|
|
[
|
|
"ln",
|
|
"-sf",
|
|
f"/usr/bin/clickhouse{sorted_binaries.index(next_server)}",
|
|
"/usr/bin/clickhouse",
|
|
],
|
|
user="root",
|
|
)
|
|
server_versions[next_pick.name] = next_server
|
|
if all_running:
|
|
time.sleep(3) # Let the keeper session expire
|
|
try:
|
|
next_pick.start_clickhouse(start_wait_sec=10, retry_start=False)
|
|
except Exception as ex:
|
|
logger.error(f"Failed to start ClickHouse: {ex}")
|
|
logger.info(f"The server {next_pick.name} is not running")
|
|
all_running = good_exit = False
|
|
if all_running and args.with_leak_detection and next_pick.name == "node0":
|
|
# Has to reset leak detector
|
|
leak_detector.reset_and_capture_baseline(cluster)
|
|
elif len(integrations) > 0:
|
|
# Restart any other integration
|
|
next_pick = random.choice(integrations)
|
|
choosen_instances = []
|
|
available_options = {
|
|
"zookeeper": list(ZOOKEEPER_CONTAINERS),
|
|
"minio": ["minio1"],
|
|
"nginx": ["nginx"],
|
|
"azurite": ["azurite1"],
|
|
"postgres": ["postgres1"],
|
|
"mysql8": ["mysql80"],
|
|
"mongo": ["mongo1", "mongo_no_cred", "mongo_secure"],
|
|
"redis": ["redis1"],
|
|
"kafka": ["kafka1"],
|
|
"arrowflight": ["flight_server"],
|
|
}
|
|
|
|
restart_choices = list(available_options[next_pick])
|
|
random.shuffle(restart_choices)
|
|
for i in range(0, random.randint(1, len(restart_choices))):
|
|
choosen_instances.append(restart_choices[i])
|
|
logger.info(
|
|
f"Restarting {next_pick} instances {', '.join(choosen_instances)} with {'kill' if kill_server else 'manual shutdown'}"
|
|
)
|
|
|
|
cluster.process_integration_nodes(
|
|
next_pick, choosen_instances, "kill" if kill_server else "stop"
|
|
)
|
|
time.sleep(random.randint(integration_lower_bound, integration_upper_bound))
|
|
cluster.process_integration_nodes(next_pick, choosen_instances, "start")
|
|
if all_running:
|
|
tables_oracle.collect_table_hash_after_shutdown(cluster, logger, dump_table)
|
|
|
|
sys.exit(0 if good_exit else 1)
|