feat(infra): поднят кластер ClickHouse из двух шардов и keeper

- Зачем:
  - этап 1 спеки требует стенд, поднимаемый одной командой; кластер —
    единственный режим, выключателя «без кластера» нет (issue #12).
- Что:
  - compose.yaml: две ноды ClickHouse и отдельный clickhouse-keeper на
    зафиксированном LTS-образе 26.3.17.56, порты только на 127.0.0.1.
  - infra/clickhouse: общее описание кластера, подключение к keeper и
    отдельные макросы shard и replica для каждой ноды.
  - scripts/clickhouse-smoke.sh: восемь проверок ON CLUSTER от описания
    кластера до удаления временных таблиц, вывод по-русски.
  - tests/smoke-guards.sh: три проверки самой smoke-команды —
    ограниченная аварийная очистка, обработка прерывания, окружение keeper.
  - README.md: быстрый старт, роли нод, обоснование выбора версии.
- Проверка:
  - make up && make smoke && make smoke-guards && make clean
This commit is contained in:
2026-07-30 18:53:14 +03:00
parent 4bb0bdfd1a
commit 2f9bb8700e
10 changed files with 555 additions and 3 deletions
+7
View File
@@ -0,0 +1,7 @@
COMPOSE_PROJECT_NAME=clickstream-data-platform
CLICKHOUSE_IMAGE=clickhouse/clickhouse-server:26.3.17.56
CLICKHOUSE_01_HTTP_PORT=28123
CLICKHOUSE_01_TCP_PORT=29000
CLICKHOUSE_02_HTTP_PORT=28124
CLICKHOUSE_02_TCP_PORT=29001
+24
View File
@@ -0,0 +1,24 @@
COMPOSE := docker compose
.PHONY: up down clean ps logs smoke smoke-guards
up:
$(COMPOSE) up --detach --wait --wait-timeout 180
down:
$(COMPOSE) down --remove-orphans
clean:
$(COMPOSE) down --volumes --remove-orphans
ps:
$(COMPOSE) ps
logs:
$(COMPOSE) logs --follow
smoke:
./scripts/clickhouse-smoke.sh
smoke-guards:
./tests/smoke-guards.sh
+58 -3
View File
@@ -8,9 +8,64 @@ clickhouse-keeper), Airflow, Superset. Преемник учебного сте
Репозиторий строится по спеке Репозиторий строится по спеке
[«Боевой реализм стенда (v2)»](docs/specs/2026-07-30-stand-v2-realism.md). [«Боевой реализм стенда (v2)»](docs/specs/2026-07-30-stand-v2-realism.md).
Работающего стенда пока нет: здесь только спека, исследование формата данных и Сейчас готов первый элемент каркаса: кластер ClickHouse из двух шардов и
контракт работы ([AGENTS.md](AGENTS.md)). Быстрый старт появится вместе с отдельного clickhouse-keeper.
этапом «Каркас стенда».
## Быстрый старт
Нужны Docker с Compose и свободные порты `28123`, `28124`, `29000`, `29001`.
Настройки портов и образа лежат в `.env` и не попадают в git:
```bash
cp .env.example .env
make up
make smoke
```
`make up` поднимает единственный режим стенда: два шарда ClickHouse по одной
реплике и отдельный keeper. Команда ждёт здорового состояния всех трёх
контейнеров. Нода `clickhouse-01` служит инициатором DDL и позже будет точкой
подключения Airflow. К `clickhouse-02` позже подключится Superset.
`make smoke` одной командой проверяет описание кластера, макросы нод, связь с
keeper, создание `ReplicatedMergeTree` и `Distributed` через `ON CLUSTER`,
путь в keeper из макроса, вставку через первую ноду и чтение со второй,
очередь распределённых DDL и удаление временных таблиц. Скрипт печатает каждый
шаг и зелёный результат по-русски.
`make smoke-guards` проверяет саму smoke-команду: аварийную очистку, обработку
прерывания, подсказку о незапущенном стенде и окружение keeper.
Остановить контейнеры можно командой `make down`. Для холодного старта с
удалением данных используйте `make clean`.
HTTP-интерфейсы доступны только с локальной машины:
- нода 1 — `http://127.0.0.1:28123`;
- нода 2 — `http://127.0.0.1:28124`.
У локального учебного кластера нет пароля: ноды используют общего пользователя
`default` для запросов `Distributed`. Порты поэтому привязаны к `127.0.0.1` и
не открыты во внешнюю сеть.
В бою перед репликами ClickHouse обычно был бы балансировщик. Здесь в каждом
шарде одна реплика, поэтому балансировать нечего. Балансировщик и топология
2×2 намеренно не входят в стенд.
Конфигурация сверена 30 июля 2026 года с официальной документацией ClickHouse:
[настройками сервера](https://clickhouse.com/docs/operations/server-configuration-parameters/settings),
[Keeper](https://clickhouse.com/docs/guides/oss/deployment-and-scaling/keeper/),
[ReplicatedMergeTree](https://clickhouse.com/docs/engines/table-engines/mergetree-family/replication),
[ON CLUSTER](https://clickhouse.com/docs/sql-reference/distributed-ddl) и
[Distributed](https://clickhouse.com/docs/engines/table-engines/special/distributed).
Описание кластера задаётся через `remote_servers`, макросы — через `macros`,
подключение к keeper — через `zookeeper`; путь `ReplicatedMergeTree` содержит
`{shard}` и `{replica}`, а `Distributed` получает имя кластера, базу, локальную
таблицу и ключ шардирования. Макросы выбраны, чтобы один DDL через `ON CLUSTER`
создавал отдельный путь каждого шарда без вписанных вручную значений. Для
образа зафиксирован точный текущий
[LTS-выпуск 26.3.17.56](https://github.com/ClickHouse/ClickHouse/releases/tag/v26.3.17.56-lts);
серверы и keeper используют один образ.
## Что здесь будет ## Что здесь будет
+72
View File
@@ -0,0 +1,72 @@
name: ${COMPOSE_PROJECT_NAME:-clickstream-data-platform}
x-clickhouse-nofile: &clickhouse-nofile
nofile:
soft: 262144
hard: 262144
x-clickhouse-common: &clickhouse-common
image: ${CLICKHOUSE_IMAGE:-clickhouse/clickhouse-server:26.3.17.56}
restart: unless-stopped
environment:
# Стенд локальный: нодам нужен общий пользователь без пароля для Distributed.
CLICKHOUSE_SKIP_USER_SETUP: "1"
depends_on:
clickhouse-keeper:
condition: service_healthy
ulimits: *clickhouse-nofile
mem_limit: 1g
healthcheck:
test: ["CMD-SHELL", "clickhouse-client --host 127.0.0.1 --query 'SELECT 1' >/dev/null 2>&1"]
interval: 5s
timeout: 3s
retries: 30
start_period: 10s
services:
clickhouse-keeper:
image: ${CLICKHOUSE_IMAGE:-clickhouse/clickhouse-server:26.3.17.56}
restart: unless-stopped
user: clickhouse
ulimits: *clickhouse-nofile
entrypoint:
- clickhouse
- keeper
- --config-file=/etc/clickhouse-keeper/keeper_config.xml
volumes:
- ./infra/clickhouse/keeper/keeper_config.xml:/etc/clickhouse-keeper/keeper_config.xml:ro
- clickhouse_keeper_data:/var/lib/clickhouse
mem_limit: 512m
healthcheck:
test: ["CMD-SHELL", "clickhouse keeper-client --host 127.0.0.1 --port 9181 --query ruok | grep -q imok"]
interval: 5s
timeout: 3s
retries: 30
start_period: 10s
# Инициатор DDL и будущее подключение Airflow.
clickhouse-01:
<<: *clickhouse-common
ports:
- "127.0.0.1:${CLICKHOUSE_01_HTTP_PORT:-28123}:8123"
- "127.0.0.1:${CLICKHOUSE_01_TCP_PORT:-29000}:9000"
volumes:
- ./infra/clickhouse/config.d/cluster.xml:/etc/clickhouse-server/config.d/cluster.xml:ro
- ./infra/clickhouse/config.d/macros-01.xml:/etc/clickhouse-server/config.d/macros.xml:ro
- clickhouse_01_data:/var/lib/clickhouse
# Будущее подключение Superset.
clickhouse-02:
<<: *clickhouse-common
ports:
- "127.0.0.1:${CLICKHOUSE_02_HTTP_PORT:-28124}:8123"
- "127.0.0.1:${CLICKHOUSE_02_TCP_PORT:-29001}:9000"
volumes:
- ./infra/clickhouse/config.d/cluster.xml:/etc/clickhouse-server/config.d/cluster.xml:ro
- ./infra/clickhouse/config.d/macros-02.xml:/etc/clickhouse-server/config.d/macros.xml:ro
- clickhouse_02_data:/var/lib/clickhouse
volumes:
clickhouse_keeper_data:
clickhouse_01_data:
clickhouse_02_data:
+36
View File
@@ -0,0 +1,36 @@
<clickhouse>
<!--
Схема проверена по документации ClickHouse 26.3: одинаковый
remote_servers на нодах, координатор в zookeeper, DDL через keeper.
-->
<remote_servers>
<clickstream_cluster>
<shard>
<internal_replication>true</internal_replication>
<replica>
<host>clickhouse-01</host>
<port>9000</port>
</replica>
</shard>
<shard>
<internal_replication>true</internal_replication>
<replica>
<host>clickhouse-02</host>
<port>9000</port>
</replica>
</shard>
</clickstream_cluster>
</remote_servers>
<zookeeper>
<node>
<host>clickhouse-keeper</host>
<port>9181</port>
</node>
</zookeeper>
<distributed_ddl>
<path>/clickhouse/task_queue/ddl</path>
</distributed_ddl>
</clickhouse>
+7
View File
@@ -0,0 +1,7 @@
<clickhouse>
<macros>
<shard>01</shard>
<replica>clickhouse-01</replica>
</macros>
</clickhouse>
+7
View File
@@ -0,0 +1,7 @@
<clickhouse>
<macros>
<shard>02</shard>
<replica>clickhouse-02</replica>
</macros>
</clickhouse>
+27
View File
@@ -0,0 +1,27 @@
<clickhouse>
<!-- Отдельный keeper использует тот же выпуск ClickHouse, что и серверы. -->
<logger>
<level>information</level>
<console>true</console>
</logger>
<listen_host>0.0.0.0</listen_host>
<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>
<coordination_settings>
<operation_timeout_ms>10000</operation_timeout_ms>
<session_timeout_ms>30000</session_timeout_ms>
</coordination_settings>
<raft_configuration>
<server>
<id>1</id>
<hostname>clickhouse-keeper</hostname>
<port>9234</port>
</server>
</raft_configuration>
</keeper_server>
</clickhouse>
+178
View File
@@ -0,0 +1,178 @@
#!/usr/bin/env bash
set -Eeuo pipefail
readonly ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
readonly CLUSTER="clickstream_cluster"
readonly LOCAL_TABLE="smoke_replicated_local"
readonly DISTRIBUTED_TABLE="smoke_distributed"
compose() {
docker compose --project-directory "$ROOT_DIR" "$@"
}
query() {
local service="$1"
local sql="$2"
compose exec -T "$service" clickhouse-client --query "$sql"
}
query_with_timeout() {
local timeout_seconds="$1"
local service="$2"
local sql="$3"
timeout --foreground "${timeout_seconds}s" \
docker compose --project-directory "$ROOT_DIR" exec -T "$service" \
clickhouse-client --query "$sql"
}
fail() {
printf 'ОШИБКА: %s\n' "$1" >&2
exit 1
}
assert_equal() {
local expected="$1"
local actual="$2"
local message="$3"
[[ "$actual" == "$expected" ]] || fail "$message: ожидалось «$expected», получено «$actual»"
}
ensure_stand_running() {
local running_services
local service
running_services="$(compose ps --status running --services 2>/dev/null || true)"
for service in clickhouse-keeper clickhouse-01 clickhouse-02; do
if ! grep -qx "$service" <<<"$running_services"; then
fail "стенд запущен не полностью; выполните make up и дождитесь здорового состояния контейнеров"
fi
done
}
cleanup_tables() {
local cleanup_failed=0
query_with_timeout 5 clickhouse-01 "DROP TABLE IF EXISTS default.${DISTRIBUTED_TABLE} ON CLUSTER ${CLUSTER} SYNC" >/dev/null 2>&1 || cleanup_failed=1
query_with_timeout 5 clickhouse-01 "DROP TABLE IF EXISTS default.${LOCAL_TABLE} ON CLUSTER ${CLUSTER} SYNC" >/dev/null 2>&1 || cleanup_failed=1
return "$cleanup_failed"
}
on_exit() {
local status=$?
trap - EXIT
if [[ "$status" -ne 0 ]]; then
printf 'Сбой проверки: выполняется быстрая очистка временных таблиц...\n' >&2
fi
cleanup_tables || printf 'ПРЕДУПРЕЖДЕНИЕ: очистка не завершена; после восстановления стенда повторите make smoke.\n' >&2
exit "$status"
}
on_signal() {
trap - EXIT INT TERM
printf 'Проверка прервана пользователем: выполняется быстрая очистка временных таблиц...\n' >&2
cleanup_tables || printf 'ПРЕДУПРЕЖДЕНИЕ: очистка не завершена; после восстановления стенда повторите make smoke.\n' >&2
exit 130
}
assert_ddl_queue_completed() {
local service="$1"
local phase="$2"
local queue_state
local total
local unfinished
queue_state="$(query "$service" "SELECT count(), countIf(status NOT IN ('Finished', 'Removing')) FROM system.distributed_ddl_queue FORMAT TSV")"
total="${queue_state%%$'\t'*}"
unfinished="${queue_state##*$'\t'}"
[[ "$total" -gt 0 ]] || fail "на ${service} очередь DDL пуста ${phase}"
assert_equal "0" "$unfinished" "на ${service} есть незавершённые DDL ${phase}"
}
ensure_stand_running
trap on_exit EXIT
trap on_signal INT TERM
printf 'Проверка 1/8: описание кластера одинаково на обеих нодах...\n'
cluster_sql="SELECT cluster, shard_num, replica_num, host_name, port FROM system.clusters WHERE cluster = '${CLUSTER}' ORDER BY shard_num, replica_num FORMAT TSV"
cluster_01="$(query clickhouse-01 "$cluster_sql")"
cluster_02="$(query clickhouse-02 "$cluster_sql")"
expected_cluster=$'clickstream_cluster\t1\t1\tclickhouse-01\t9000\nclickstream_cluster\t2\t1\tclickhouse-02\t9000'
assert_equal "$expected_cluster" "$cluster_01" "неверная топология на первой ноде"
assert_equal "$expected_cluster" "$cluster_02" "неверная топология на второй ноде"
printf 'ЗЕЛЁНО: обе ноды видят ожидаемые два шарда: clickhouse-01 и clickhouse-02.\n'
printf 'Проверка 2/8: у нод разные макросы shard и replica...\n'
macros_sql="SELECT macro, substitution FROM system.macros WHERE macro IN ('shard', 'replica') ORDER BY macro FORMAT TSV"
macros_01="$(query clickhouse-01 "$macros_sql")"
macros_02="$(query clickhouse-02 "$macros_sql")"
assert_equal $'replica\tclickhouse-01\nshard\t01' "$macros_01" "неверные макросы первой ноды"
assert_equal $'replica\tclickhouse-02\nshard\t02' "$macros_02" "неверные макросы второй ноды"
[[ "$macros_01" != "$macros_02" ]] || fail "макросы нод не должны совпадать"
printf 'ЗЕЛЁНО: clickhouse-01=(shard 01, replica clickhouse-01), clickhouse-02=(shard 02, replica clickhouse-02).\n'
printf 'Проверка 3/8: keeper отвечает обеим нодам...\n'
query clickhouse-01 "SELECT name FROM system.zookeeper WHERE path = '/' ORDER BY name FORMAT Null"
query clickhouse-02 "SELECT name FROM system.zookeeper WHERE path = '/' ORDER BY name FORMAT Null"
printf 'ЗЕЛЁНО: system.zookeeper доступна с обеих нод.\n'
cleanup_tables || fail "не удалось очистить объекты предыдущего запуска"
printf 'Проверка 4/8: ReplicatedMergeTree создаётся через ON CLUSTER...\n'
query clickhouse-01 "
CREATE TABLE default.${LOCAL_TABLE} ON CLUSTER ${CLUSTER}
(
ClientID UInt64,
value String
)
ENGINE = ReplicatedMergeTree(
'/clickhouse/tables/{shard}/${LOCAL_TABLE}',
'{replica}'
)
ORDER BY ClientID
" >/dev/null
tables_sql="SELECT name, engine FROM system.tables WHERE database = 'default' AND name = '${LOCAL_TABLE}' FORMAT TSV"
assert_equal $'smoke_replicated_local\tReplicatedMergeTree' "$(query clickhouse-01 "$tables_sql")" "локальная таблица не создана на первой ноде"
assert_equal $'smoke_replicated_local\tReplicatedMergeTree' "$(query clickhouse-02 "$tables_sql")" "локальная таблица не создана на второй ноде"
printf 'ЗЕЛЁНО: ReplicatedMergeTree видна в system.tables на обеих нодах.\n'
printf 'Проверка 5/8: путь в keeper собран из макроса shard...\n'
path_sql="SELECT zookeeper_path, replica_name FROM system.replicas WHERE database = 'default' AND table = '${LOCAL_TABLE}' FORMAT TSV"
assert_equal "/clickhouse/tables/01/${LOCAL_TABLE}"$'\t'"clickhouse-01" "$(query clickhouse-01 "$path_sql")" "неверные путь или имя реплики на первой ноде"
assert_equal "/clickhouse/tables/02/${LOCAL_TABLE}"$'\t'"clickhouse-02" "$(query clickhouse-02 "$path_sql")" "неверные путь или имя реплики на второй ноде"
printf 'ЗЕЛЁНО: пути собраны из shard (/01/ и /02/), имя реплики собрано из макроса replica.\n'
printf 'Проверка 6/8: Distributed создаётся ON CLUSTER и передаёт данные между нодами...\n'
query clickhouse-01 "
CREATE TABLE default.${DISTRIBUTED_TABLE} ON CLUSTER ${CLUSTER}
AS default.${LOCAL_TABLE}
ENGINE = Distributed(
'${CLUSTER}',
'default',
'${LOCAL_TABLE}',
cityHash64(ClientID)
)
" >/dev/null
distributed_sql="SELECT engine FROM system.tables WHERE database = 'default' AND name = '${DISTRIBUTED_TABLE}' FORMAT TSVRaw"
assert_equal "Distributed" "$(query clickhouse-01 "$distributed_sql")" "Distributed-таблица не создана на первой ноде"
assert_equal "Distributed" "$(query clickhouse-02 "$distributed_sql")" "Distributed-таблица не создана на второй ноде"
query clickhouse-01 "INSERT INTO default.${LOCAL_TABLE} VALUES (42, 'из первой ноды')"
read_back="$(query clickhouse-02 "SELECT ClientID, value FROM default.${DISTRIBUTED_TABLE} WHERE ClientID = 42 FORMAT TSV")"
assert_equal $'42\tиз первой ноды' "$read_back" "вторая нода не прочитала вставленную строку"
query clickhouse-01 "INSERT INTO default.${DISTRIBUTED_TABLE} SETTINGS distributed_foreground_insert = 1 SELECT number + 1000, 'через Distributed' FROM numbers(16)"
sharding_sql="SELECT countIf(_shard_num != cityHash64(ClientID) % 2 + 1), uniqExact(_shard_num) FROM default.${DISTRIBUTED_TABLE} WHERE value = 'через Distributed' FORMAT TSV"
assert_equal $'0\t2' "$(query clickhouse-02 "$sharding_sql")" "Distributed использует неверный ключ шардирования"
printf 'ЗЕЛЁНО: локальная строка первой ноды читается со второй; ключ cityHash64(ClientID) разложил строки по двум шардам.\n'
printf 'Проверка 7/8: в очереди распределённых DDL нет незавершённых заданий...\n'
assert_ddl_queue_completed clickhouse-01 'после CREATE'
assert_ddl_queue_completed clickhouse-02 'после CREATE'
printf 'ЗЕЛЁНО: очередь содержит задания CREATE, незавершённых среди них нет.\n'
printf 'Проверка 8/8: временные таблицы удаляются через ON CLUSTER...\n'
query clickhouse-01 "DROP TABLE default.${DISTRIBUTED_TABLE} ON CLUSTER ${CLUSTER} SYNC" >/dev/null
query clickhouse-01 "DROP TABLE default.${LOCAL_TABLE} ON CLUSTER ${CLUSTER} SYNC" >/dev/null
remaining_sql="SELECT count() FROM system.tables WHERE database = 'default' AND name IN ('${LOCAL_TABLE}', '${DISTRIBUTED_TABLE}') FORMAT TSVRaw"
assert_equal "0" "$(query clickhouse-01 "$remaining_sql")" "временные таблицы остались на первой ноде"
assert_equal "0" "$(query clickhouse-02 "$remaining_sql")" "временные таблицы остались на второй ноде"
assert_ddl_queue_completed clickhouse-01 'после DROP'
assert_ddl_queue_completed clickhouse-02 'после DROP'
trap - EXIT INT TERM
printf 'ЗЕЛЁНО: временные таблицы удалены; проверены завершённые задания CREATE и DROP.\n'
printf 'ИТОГ: все 8 проверок кластера ClickHouse прошли.\n'
+139
View File
@@ -0,0 +1,139 @@
#!/usr/bin/env bash
set -uo pipefail
readonly ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
readonly LOCAL_TABLE="smoke_replicated_local"
readonly DISTRIBUTED_TABLE="smoke_distributed"
passed=0
failed=0
compose() {
docker compose --project-directory "$ROOT_DIR" "$@"
}
query() {
local service="$1"
local sql="$2"
compose exec -T "$service" clickhouse-client --query "$sql"
}
pass() {
passed=$((passed + 1))
printf 'ЗЕЛЁНО: %s.\n' "$1"
}
fail() {
failed=$((failed + 1))
printf 'ОШИБКА: %s.\n' "$1" >&2
}
wait_for_local_table() {
local attempt
for attempt in $(seq 1 100); do
if [[ "$(query clickhouse-01 "SELECT count() FROM system.tables WHERE database = 'default' AND name = '${LOCAL_TABLE}' FORMAT TSVRaw" 2>/dev/null || true)" == '1' ]]; then
return 0
fi
sleep 0.05
done
return 1
}
restore_stand() {
make --no-print-directory -C "$ROOT_DIR" up >/dev/null
query clickhouse-01 "DROP TABLE IF EXISTS default.${DISTRIBUTED_TABLE} ON CLUSTER clickstream_cluster SYNC" >/dev/null 2>&1 || true
query clickhouse-01 "DROP TABLE IF EXISTS default.${LOCAL_TABLE} ON CLUSTER clickstream_cluster SYNC" >/dev/null 2>&1 || true
}
check_failure_guards() {
local failure_log
local failure_pid
local failure_status
local failure_elapsed
local interrupt_log
local interrupt_pid
local interrupt_status
local remaining_tables
local started_at
local bounded_failure_ok=0
local interrupt_ok=0
interrupt_status=999
remaining_tables='не проверено'
failure_log="$(mktemp)"
started_at="$(date +%s)"
setsid make --no-print-directory -C "$ROOT_DIR" smoke >"$failure_log" 2>&1 &
failure_pid=$!
if wait_for_local_table; then
compose kill clickhouse-02 >/dev/null
wait "$failure_pid"
failure_status=$?
failure_elapsed=$(( $(date +%s) - started_at ))
if [[ "$failure_status" -eq 2 ]] && [[ "$failure_elapsed" -lt 20 ]]; then
bounded_failure_ok=1
fi
else
kill -TERM -- "-$failure_pid" >/dev/null 2>&1 || true
wait "$failure_pid" >/dev/null 2>&1 || true
fi
rm -f "$failure_log"
restore_stand
interrupt_log="$(mktemp)"
setsid env --default-signal=INT,TERM "$ROOT_DIR/scripts/clickhouse-smoke.sh" >"$interrupt_log" 2>&1 &
interrupt_pid=$!
if wait_for_local_table; then
kill -INT -- "-$interrupt_pid"
wait "$interrupt_pid"
interrupt_status=$?
remaining_tables="$(query clickhouse-01 "SELECT count() FROM clusterAllReplicas('clickstream_cluster', system.tables) WHERE database = 'default' AND name IN ('${LOCAL_TABLE}', '${DISTRIBUTED_TABLE}') FORMAT TSVRaw")"
if [[ "$interrupt_status" -eq 130 ]] && [[ "$remaining_tables" == '0' ]]; then
interrupt_ok=1
fi
else
kill -TERM -- "-$interrupt_pid" >/dev/null 2>&1 || true
wait "$interrupt_pid" >/dev/null 2>&1 || true
fi
rm -f "$interrupt_log"
if [[ "$bounded_failure_ok" -eq 1 ]] && [[ "$interrupt_ok" -eq 1 ]]; then
pass 'аварийная очистка ограничена по времени, SIGINT возвращает 130 и удаляет временные таблицы'
else
fail "нарушена аварийная семантика smoke: bounded=${bounded_failure_ok}, interrupt=${interrupt_ok}, status=${interrupt_status}, tables=${remaining_tables}"
fi
}
check_keeper_runtime() {
local keeper_user
local keeper_nofile
local keeper_owner
keeper_user="$(compose exec -T clickhouse-keeper id -un)"
keeper_nofile="$(compose exec -T clickhouse-keeper awk '$1 == "Max" && $2 == "open" && $3 == "files" {print $4}' /proc/1/limits)"
keeper_owner="$(compose exec -T clickhouse-keeper stat -c '%U:%G' /var/lib/clickhouse/coordination)"
if [[ "$keeper_user" == 'clickhouse' ]] && [[ "$keeper_nofile" -ge 262144 ]] && [[ "$keeper_owner" == 'clickhouse:clickhouse' ]]; then
pass 'keeper работает от clickhouse с nofile 262144 и своим каталогом данных'
else
fail "неверное окружение keeper: user=${keeper_user}, nofile=${keeper_nofile}, owner=${keeper_owner}"
fi
}
check_preflight_hint() {
local output
local status
compose kill clickhouse-02 >/dev/null
output="$("$ROOT_DIR/scripts/clickhouse-smoke.sh" 2>&1)"
status=$?
if [[ "$status" -ne 0 ]] && grep -q 'выполните make up' <<<"$output"; then
pass 'неполный стенд получает русскую подсказку выполнить make up'
else
fail 'нет русской подсказки для незапущенного стенда'
fi
make --no-print-directory -C "$ROOT_DIR" up >/dev/null
}
check_failure_guards
check_keeper_runtime
check_preflight_hint
printf 'ИТОГ: пройдено %d, ошибок %d\n' "$passed" "$failed"
[[ "$failed" -eq 0 ]]