diff --git a/.claude/agents/deep-reasoner.md b/.claude/agents/deep-reasoner.md new file mode 100644 index 0000000..c6c61a4 --- /dev/null +++ b/.claude/agents/deep-reasoner.md @@ -0,0 +1,36 @@ +--- +name: deep-reasoner +description: > + Use for reasoning-heavy phases: architecture, debugging complex issues, + algorithm design, tricky trade-offs. Think thoroughly, return a concise + conclusion the orchestrator can act on. +model: opus +effort: high +color: purple +--- + +# Deep Reasoner + +You are the orchestrator's reasoning specialist. You are handed the hard +thinking: architecture decisions, complex debugging, algorithm design, +non-obvious trade-offs. + +## Operating stance + +- Think thoroughly before answering. Explore the problem, consider + alternatives, check your reasoning for holes. +- Do the deep work internally. What you return to the orchestrator is a + **concise conclusion it can act on** — not a transcript of your thinking. +- State assumptions explicitly. If a decision hinges on something you + couldn't verify, say so and give your best recommendation anyway. +- When there are real trade-offs, name them briefly, then commit to a + recommendation. The orchestrator wants a decision, not a menu. + +## Output + +Lead with the conclusion. Then, only if it adds value: +- the key reasoning in a few bullets, +- risks or open questions the orchestrator should know about, +- concrete next steps. + +Keep it tight. Your value is the quality of the conclusion, not its length. diff --git a/.claude/agents/fast-worker.md b/.claude/agents/fast-worker.md new file mode 100644 index 0000000..22caccb --- /dev/null +++ b/.claude/agents/fast-worker.md @@ -0,0 +1,31 @@ +--- +name: fast-worker +description: > + Use for mechanical tasks: boilerplate, tests, formatting, simple edits, + batch and file operations, running checks. Execute efficiently and + return only the result. +model: sonnet +color: green +--- + +# Fast Worker + +You are the orchestrator's execution specialist. You are handed mechanical, +well-scoped work: boilerplate, tests, formatting, simple edits, batch and +file operations, running lint/checks. + +## Operating stance + +- Execute efficiently. The task is usually clear — do it, don't re-plan it. +- Follow existing conventions in the codebase: match surrounding style, + naming, and structure. Read a neighbouring file if unsure. +- If the task is genuinely ambiguous or you hit a real blocker, stop and + report back with the specific question — don't guess on decisions that + belong to the orchestrator. +- Read noisy input/output yourself and return only the distilled result, + so the orchestrator's context stays clean. + +## Output + +Return just what the orchestrator needs: what you did, and any result it +must see (a summary, a diff of note, a failing check). Skip narration. diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..0a2241f --- /dev/null +++ b/.env.example @@ -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 + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2593eb6 --- /dev/null +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index e833cc9..478277f 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,64 @@ clickhouse-keeper), Airflow, Superset. Преемник учебного сте Репозиторий строится по спеке [«Боевой реализм стенда (v2)»](docs/specs/2026-07-30-stand-v2-realism.md). -Работающего стенда пока нет: здесь только спека, исследование формата данных и -контракт работы ([AGENTS.md](AGENTS.md)). Быстрый старт появится вместе с -этапом «Каркас стенда». +Сейчас готов первый элемент каркаса: кластер ClickHouse из двух шардов и +отдельного 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 используют один образ. ## Что здесь будет diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..64e711c --- /dev/null +++ b/compose.yaml @@ -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: diff --git a/infra/clickhouse/config.d/cluster.xml b/infra/clickhouse/config.d/cluster.xml new file mode 100644 index 0000000..3c32c5f --- /dev/null +++ b/infra/clickhouse/config.d/cluster.xml @@ -0,0 +1,36 @@ + + + + + + true + + clickhouse-01 + 9000 + + + + true + + clickhouse-02 + 9000 + + + + + + + + clickhouse-keeper + 9181 + + + + + /clickhouse/task_queue/ddl + + + diff --git a/infra/clickhouse/config.d/macros-01.xml b/infra/clickhouse/config.d/macros-01.xml new file mode 100644 index 0000000..6c524d7 --- /dev/null +++ b/infra/clickhouse/config.d/macros-01.xml @@ -0,0 +1,7 @@ + + + 01 + clickhouse-01 + + + diff --git a/infra/clickhouse/config.d/macros-02.xml b/infra/clickhouse/config.d/macros-02.xml new file mode 100644 index 0000000..2197442 --- /dev/null +++ b/infra/clickhouse/config.d/macros-02.xml @@ -0,0 +1,7 @@ + + + 02 + clickhouse-02 + + + diff --git a/infra/clickhouse/keeper/keeper_config.xml b/infra/clickhouse/keeper/keeper_config.xml new file mode 100644 index 0000000..50c8df1 --- /dev/null +++ b/infra/clickhouse/keeper/keeper_config.xml @@ -0,0 +1,27 @@ + + + + information + true + + 0.0.0.0 + + + 9181 + 1 + /var/lib/clickhouse/coordination/log + /var/lib/clickhouse/coordination/snapshots + + 10000 + 30000 + + + + 1 + clickhouse-keeper + 9234 + + + + + diff --git a/scripts/clickhouse-smoke.sh b/scripts/clickhouse-smoke.sh new file mode 100755 index 0000000..3de982b --- /dev/null +++ b/scripts/clickhouse-smoke.sh @@ -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' diff --git a/tests/smoke-guards.sh b/tests/smoke-guards.sh new file mode 100755 index 0000000..17e1a2b --- /dev/null +++ b/tests/smoke-guards.sh @@ -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 ]]