From ec6111d2604a15661054e6de7c453849cf904cf5 Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Wed, 11 Feb 2026 09:48:56 +0300 Subject: [PATCH] fix(superset): stabilize bootstrap after clean reset - make superset-init run via dedicated init service\n- tolerate missing dm views during early metadata refresh\n- add clickhouse dependency for init service\n- document clean-reset behavior and re-init flow --- Makefile | 4 ++- README.md | 5 ++-- docker-compose.yml | 2 ++ docs/OPERATIONS.md | 2 ++ superset/init_superset.py | 54 ++++++++++++++++++++++++++++++++++----- 5 files changed, 58 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index db375a0..2470dec 100644 --- a/Makefile +++ b/Makefile @@ -69,7 +69,9 @@ recover-monitoring: # Инициализация Superset (подключение к ClickHouse + датасеты) superset-init: - $(COMPOSE) exec -T superset bash -c "python /app/superset_init/init_superset.py" + $(COMPOSE) up -d postgres-metadata clickhouse + $(COMPOSE) up --abort-on-container-exit --exit-code-from superset-init superset-init + $(COMPOSE) up -d --no-deps superset # Создание дашборда с чартами superset-dashboard: diff --git a/README.md b/README.md index cd30275..18b30fc 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ docker compose exec -T clickhouse clickhouse-client --user=default --password=12 | Prometheus | http://localhost:9090 | Метрики | — | | Grafana | http://localhost:3000 | Визуализация метрик | admin/admin | -Superset: после `make up` готовый дашборд доступен по адресу http://localhost:8088/superset/dashboard/1/ +Superset: UI доступен после `make up` по адресу http://localhost:8088, дашборд — http://localhost:8088/superset/dashboard/1/ --- @@ -280,7 +280,7 @@ make superset-dashboard ``` ┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ -│ Superset UI │────▶│ PostgreSQL │────▶│ ClickHouse │ +│ Superset UI │────▶│ PostgreSQL │───▶│ ClickHouse │ │ (localhost) │ │ (metadata) │ │ (данные) │ │ :8088 │ │ dashboards, │ │ dm.v_* VIEW │ └─────────────────┘ │ datasets, charts│ └─────────────────┘ @@ -324,6 +324,7 @@ make superset-dashboard - Airflow (ClickHouseOperator) ходит в ClickHouse по native TCP (порт `9000` внутри сети Docker). - Superset (clickhouse-connect) ходит по HTTP (порт `8123` внутри сети Docker). - **Superset**: дашборд не появился сразу — подождите 30-60 секунд после `make up`, затем проверьте `curl http://localhost:8088/health`. +- **Superset**: после `make clean` витрины `dm.*` ещё не созданы, поэтому чарты могут быть пустыми до запуска `ddl_init -> kafka_load -> etl_pipeline`; после этого выполните `make superset-init`. - **Superset**: при полном сбросе (`docker compose down -v`) метаданные Superset пропадут т.к. используется общая PostgreSQL. Для чистого перезапуска Superset удалите только БД `superset` в PostgreSQL и перезапустите контейнеры. --- diff --git a/docker-compose.yml b/docker-compose.yml index 7054a79..223b9b6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -180,6 +180,8 @@ services: depends_on: postgres-metadata: condition: service_healthy + clickhouse: + condition: service_started prometheus: image: prom/prometheus:v2.53.4 diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index 7ee8cc6..ffb992e 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -12,6 +12,7 @@ - `make ddl` (применяет SQL из `sql/ddl/00_databases.sql` и `sql/ddl/*/*.sql` в ClickHouse) - `make data` (пересоздаёт топики и заливает небольшой срез данных в Kafka; полный режим — `FULL=1 make data`) - `make transform` (запускает batch-процесс ODS -> DDS -> DM) +- `make superset-init` (повторная инициализация Superset: подключение к ClickHouse, датасеты, дашборд) - `docker compose ps` - `docker compose logs -f --tail=200 ` - `docker compose down` (сохраняет named volumes, включая `clickhouse-data`) @@ -302,4 +303,5 @@ curl -s -X POST -u admin:admin http://localhost:3000/api/admin/provisioning/aler docker compose up -d --force-recreate superset-init superset ``` - После `docker compose down -v` нужно повторно прогнать: `ddl_init` -> `kafka_load` -> `etl_pipeline`. +- После `make clean`/`down -v` Superset стартует, но витрины `dm.*` ещё пустые или отсутствуют до прогона ETL; после `ddl_init` -> `kafka_load` -> `etl_pipeline` выполнить `make superset-init`. - Для демо по умолчанию использовать малый срез данных; полный прогон делать осознанно. diff --git a/superset/init_superset.py b/superset/init_superset.py index 679eb8b..378ec9e 100644 --- a/superset/init_superset.py +++ b/superset/init_superset.py @@ -29,6 +29,7 @@ import os import logging import subprocess from urllib.parse import quote_plus +from sqlalchemy.exc import NoSuchTableError # Настройка логирования logging.basicConfig( @@ -47,6 +48,27 @@ CLICKHOUSE_PORT = os.getenv('CLICKHOUSE_HTTP_PORT', '8123') CLICKHOUSE_DATABASE = os.getenv('CLICKHOUSE_DATABASE', 'default') +def is_expected_missing_table_error(exc: Exception, schema_name: str, table_name: str) -> bool: + """Проверяет, что ошибка связана с отсутствием таблицы/вьюхи в ClickHouse.""" + if isinstance(exc, NoSuchTableError): + return True + + text = str(exc).strip().lower() + full_table_name = f"{schema_name}.{table_name}".lower() + known_markers = ( + "doesn't exist", + "does not exist", + "unknown table", + "no such table", + "code: 60", + ) + + if text == full_table_name: + return True + + return full_table_name in text and any(marker in text for marker in known_markers) + + def build_clickhouse_uri() -> str: """Собирает URI подключения к ClickHouse для Superset.""" user = quote_plus(CLICKHOUSE_USER) @@ -143,6 +165,17 @@ def import_datasets(): "from superset.extensions import db", "from superset.models.core import Database", "from superset.connectors.sqla.models import SqlaTable", + "from sqlalchemy.exc import NoSuchTableError", + "", + "def is_expected_missing_table_error(exc, schema_name, table_name):", + " text = str(exc).strip().lower()", + " full_table_name = f'{schema_name}.{table_name}'.lower()", + " known_markers = ('doesn\\'t exist', 'does not exist', 'unknown table', 'no such table', 'code: 60')", + " if isinstance(exc, NoSuchTableError):", + " return True", + " if text == full_table_name:", + " return True", + " return full_table_name in text and any(marker in text for marker in known_markers)", "", "# Получаем базу данных", "database = db.session.query(Database).filter_by(database_name='clickhouse_dwh').first()", @@ -179,9 +212,13 @@ def import_datasets(): " else:", f" print('Dataset {table_name} already exists')", " except Exception as e:", - f" print(f'ERROR: failed to refresh {table_name}: {{e}}')", - " db.session.rollback()", - " errors += 1", + f" if is_expected_missing_table_error(e, {schema_name!r}, {table_name!r}):", + f" print('WARNING: metadata not refreshed for {table_name}: source table/view not found yet')", + " db.session.rollback()", + " else:", + f" print(f'ERROR: failed to refresh {table_name}: {{e}}')", + " db.session.rollback()", + " errors += 1", "else:", " try:", ( @@ -200,9 +237,14 @@ def import_datasets(): f" print('Created dataset: {table_name}')", " imported += 1", " except Exception as e:", - f" print(f'ERROR: failed to create {table_name}: {{e}}')", - " db.session.rollback()", - " errors += 1", + f" if is_expected_missing_table_error(e, {schema_name!r}, {table_name!r}):", + " db.session.commit()", + f" print('WARNING: created dataset {table_name} without metadata: source table/view not found yet')", + " imported += 1", + " else:", + f" print(f'ERROR: failed to create {table_name}: {{e}}')", + " db.session.rollback()", + " errors += 1", ]) script_lines.extend([