From be4992f9c6bae7b5844d8ac2fd2e71bf459f5d9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vitor=20Hugo=20Belorio=20Sim=C3=A3o?= Date: Fri, 20 Mar 2026 15:41:49 -0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=B1=20refactor(bootstrap):=20separar?= =?UTF-8?q?=20init=5Fdb=20do=20startup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extrai o bootstrap de banco e seed para uma rotina dedicada, mantendo init_db apenas como alias legado de compatibilidade para evitar quebra nos fluxos existentes. Remove o bootstrap automatico do startup do app HTTP e do container principal, deixando o processo de atendimento responsavel apenas por subir a aplicacao e nao por preparar schema ou popular dados. Alinha compose, exemplos de systemd, documentacao e testes para o novo fluxo explicito de bootstrap, com a suite completa validada em 211 testes. --- DEPLOY_SERVIDOR.md | 13 ++- Dockerfile | 4 +- README.md | 43 +++++++--- app/db/bootstrap.py | 81 +++++++++++++++++++ app/db/init_db.py | 62 ++------------ app/main.py | 9 +-- .../orquestrador-bootstrap.service.example | 15 ++++ deploy/systemd/orquestrador.service.example | 2 +- docker-compose.yml | 66 ++++++++------- tests/test_runtime_bootstrap.py | 54 +++++++++---- 10 files changed, 229 insertions(+), 120 deletions(-) create mode 100644 app/db/bootstrap.py create mode 100644 deploy/systemd/orquestrador-bootstrap.service.example diff --git a/DEPLOY_SERVIDOR.md b/DEPLOY_SERVIDOR.md index 21bfa0a..b1c900c 100644 --- a/DEPLOY_SERVIDOR.md +++ b/DEPLOY_SERVIDOR.md @@ -96,18 +96,24 @@ Antes de ativar o servico, rode uma inicializacao manual para validar banco e se ```bash cd /opt/orquestrador source venv/bin/activate -python -m app.db.init_db +python -m app.db.bootstrap ``` ## 7) Configurar `systemd` -Copie o template: +Copie o template principal: ```bash sudo cp deploy/systemd/orquestrador.service.example /etc/systemd/system/orquestrador.service sudo nano /etc/systemd/system/orquestrador.service ``` +Se quiser um atalho explicito para bootstrap manual via `systemd`, existe tambem o template: + +```bash +sudo cp deploy/systemd/orquestrador-bootstrap.service.example /etc/systemd/system/orquestrador-bootstrap.service +``` + Depois recarregue e inicie: ```bash @@ -143,6 +149,9 @@ cd /opt/orquestrador git pull origin main source venv/bin/activate pip install -r requirements.txt +# rode o bootstrap apenas quando houver mudanca de schema/seed +python -m app.db.bootstrap sudo systemctl restart orquestrador sudo systemctl status orquestrador ``` + diff --git a/Dockerfile b/Dockerfile index 7fbbfe4..2868940 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,5 +29,5 @@ COPY app /app/app ENV PATH=/root/.local/bin:$PATH -# Sobe o bootstrap de banco e inicia o satelite do Telegram. -CMD ["sh", "-c", "python -m app.db.init_db && python -m app.integrations.telegram_satellite_service"] +# Inicia apenas o servico principal; bootstrap de banco e seed sao rotinas explicitas. +CMD ["python", "-m", "app.integrations.telegram_satellite_service"] diff --git a/README.md b/README.md index 3eea69e..3d7e9d9 100644 --- a/README.md +++ b/README.md @@ -156,17 +156,27 @@ O projeto usa duas conexoes MySQL: - banco de tools - banco mock de negocio -O bootstrap atual cria tabelas e executa seed por meio de: -- [app/db/init_db.py](/d:/vitor/Pessoal/PJ/Orquestrador/app/db/init_db.py) +O bootstrap agora e uma rotina dedicada e explicita: +- [app/db/bootstrap.py](/d:/vitor/Pessoal/PJ/Orquestrador/app/db/bootstrap.py) +- [app/db/init_db.py](/d:/vitor/Pessoal/PJ/Orquestrador/app/db/init_db.py) como alias legado de compatibilidade -Esse bootstrap e usado no container e pode ser executado manualmente antes do servico principal. +Importante: +- o container principal nao executa bootstrap automaticamente; +- o app HTTP legado nao executa bootstrap no startup; +- a preparacao de schema e seed deve ser rodada de forma explicita antes do servico principal quando necessario. ## Execucao Local ### Sem Docker 1. Configure as variaveis de ambiente com base em `.env.example`. -2. Inicialize banco e seed: +2. Inicialize banco e seed com a rotina dedicada: + +```bash +python -m app.db.bootstrap +``` + +Alias legado ainda aceito: ```bash python -m app.db.init_db @@ -184,8 +194,15 @@ O compose atual sobe: - `mysql` - `redis` - `telegram` +- `bootstrap` como rotina opcional e dedicada via profile + +Preparar banco e seed: + +```bash +docker compose --profile bootstrap run --rm bootstrap +``` -Subida completa: +Subida completa do atendimento: ```bash docker compose up --build @@ -253,13 +270,19 @@ Observacao: ## Docker -O [Dockerfile](/d:/vitor/Pessoal/PJ/Orquestrador/Dockerfile) hoje sobe o servico principal do projeto: +O [Dockerfile](/d:/vitor/Pessoal/PJ/Orquestrador/Dockerfile) agora sobe apenas o servico principal do projeto: + +```bash +python -m app.integrations.telegram_satellite_service +``` + +O bootstrap fica separado e pode ser executado quando necessario com: ```bash -python -m app.db.init_db && python -m app.integrations.telegram_satellite_service +python -m app.db.bootstrap ``` -Isso deixa o container alinhado com o uso atual do sistema, sem assumir FastAPI como interface principal. +Isso evita que um restart do container recrie schema ou rode seed de forma implicita. ## Testes @@ -285,7 +308,9 @@ DEBUG=false python -m unittest discover -s tests -v Os proximos ganhos mais valiosos para o projeto sao: - persistir trilha de conversa e decisoes -- desacoplar bootstrap de banco do startup da aplicacao +- consolidar observabilidade por turno e por tool com baixo acoplamento operacional - aumentar observabilidade por turno e por tool - reduzir o tamanho do `OrquestradorService` - consolidar documentacao operacional Telegram-first + + diff --git a/app/db/bootstrap.py b/app/db/bootstrap.py new file mode 100644 index 0000000..5b7f89b --- /dev/null +++ b/app/db/bootstrap.py @@ -0,0 +1,81 @@ +""" +Rotina dedicada de bootstrap de banco de dados. +Cria tabelas e executa seed inicial de forma explicita, fora do startup do app. +""" + +from app.core.settings import settings +from app.db.database import Base, engine +from app.db.mock_database import MockBase, mock_engine +from app.db.models import Tool +from app.db.mock_models import ( + ConversationTurn, + Customer, + Order, + RentalContract, + RentalFine, + RentalPayment, + RentalVehicle, + ReviewSchedule, + Vehicle, +) +from app.db.mock_seed import seed_mock_data +from app.db.tool_seed import seed_tools + + +def bootstrap_databases( + *, + run_tools_seed: bool | None = None, + run_mock_seed: bool | None = None, +) -> None: + """Cria tabelas e executa seed inicial em ambos os bancos.""" + print("Inicializando bancos...") + failures: list[str] = [] + + should_seed_tools = settings.auto_seed_tools if run_tools_seed is None else bool(run_tools_seed) + should_seed_mock = ( + settings.auto_seed_mock and settings.mock_seed_enabled + if run_mock_seed is None + else bool(run_mock_seed) + ) + + try: + print("Criando tabelas MySQL (tools)...") + Base.metadata.create_all(bind=engine) + if should_seed_tools: + print("Populando tools iniciais...") + seed_tools() + else: + print("Seed de tools desabilitada por configuracao.") + print("MySQL tools OK.") + except Exception as exc: + print(f"Aviso: falha no MySQL (tools): {exc}") + failures.append(f"tools={exc}") + + try: + print("Criando tabelas MySQL (dados ficticios)...") + MockBase.metadata.create_all(bind=mock_engine) + if should_seed_mock: + print("Populando dados ficticios iniciais...") + seed_mock_data() + else: + print("Seed mock desabilitada por configuracao.") + print("MySQL mock OK.") + except Exception as exc: + print(f"Aviso: falha no MySQL mock: {exc}") + failures.append(f"mock={exc}") + + if failures: + raise RuntimeError( + "Falha ao inicializar bancos do orquestrador: " + " | ".join(failures) + ) + + print("Bancos inicializados com sucesso!") + + +def main() -> None: + """Executa o bootstrap dedicado quando chamado via modulo.""" + bootstrap_databases() + + +if __name__ == "__main__": + main() diff --git a/app/db/init_db.py b/app/db/init_db.py index 6da05b4..ef55d59 100644 --- a/app/db/init_db.py +++ b/app/db/init_db.py @@ -1,64 +1,14 @@ """ -Inicializacao de banco de dados. -Cria tabelas e executa seed inicial em ambos os bancos. +Compatibilidade para bootstrap legado. +Mantem o comando historico `python -m app.db.init_db` delegando para a rotina dedicada. """ -from app.core.settings import settings -from app.db.database import Base, engine -from app.db.mock_database import MockBase, mock_engine -from app.db.models import Tool -from app.db.mock_models import ( - ConversationTurn, - Customer, - Order, - RentalContract, - RentalFine, - RentalPayment, - RentalVehicle, - ReviewSchedule, - Vehicle, -) -from app.db.mock_seed import seed_mock_data -from app.db.tool_seed import seed_tools +from app.db.bootstrap import bootstrap_databases -def init_db(): - """Cria tabelas e executa seed inicial em ambos os bancos.""" - print("Inicializando bancos...") - failures: list[str] = [] - - try: - print("Criando tabelas MySQL (tools)...") - Base.metadata.create_all(bind=engine) - if settings.auto_seed_tools: - print("Populando tools iniciais...") - seed_tools() - else: - print("Seed de tools desabilitada por configuracao.") - print("MySQL tools OK.") - except Exception as exc: - print(f"Aviso: falha no MySQL (tools): {exc}") - failures.append(f"tools={exc}") - - try: - print("Criando tabelas MySQL (dados ficticios)...") - MockBase.metadata.create_all(bind=mock_engine) - if settings.auto_seed_mock and settings.mock_seed_enabled: - print("Populando dados ficticios iniciais...") - seed_mock_data() - else: - print("Seed mock desabilitada por configuracao.") - print("MySQL mock OK.") - except Exception as exc: - print(f"Aviso: falha no MySQL mock: {exc}") - failures.append(f"mock={exc}") - - if failures: - raise RuntimeError( - "Falha ao inicializar bancos do orquestrador: " + " | ".join(failures) - ) - - print("Bancos inicializados com sucesso!") +def init_db() -> None: + """Mantem compatibilidade com o nome legado do bootstrap.""" + bootstrap_databases() if __name__ == "__main__": diff --git a/app/main.py b/app/main.py index 2b31d93..bc79137 100644 --- a/app/main.py +++ b/app/main.py @@ -1,6 +1,5 @@ from fastapi import FastAPI -from app.db.init_db import init_db from app.services.ai.llm_service import LLMService app = FastAPI(title="AI Orquestrador") @@ -9,15 +8,13 @@ app = FastAPI(title="AI Orquestrador") @app.on_event("startup") async def startup_event(): """ - Inicializa o banco de dados e executa seeds automaticamente. + Realiza apenas inicializacao leve do app HTTP legado. + Bootstrap de banco e seed agora sao operacoes explicitas e separadas. """ - print("[Startup] Iniciando bootstrap legado do app HTTP...") - init_db() - try: await LLMService().warmup() print("[Startup] LLM warmup concluido.") except Exception as e: print(f"[Startup] Aviso: falha no warmup do LLM: {e}") - print("[Startup] App HTTP legado inicializado.") + print("[Startup] App HTTP legado inicializado sem bootstrap automatico.") diff --git a/deploy/systemd/orquestrador-bootstrap.service.example b/deploy/systemd/orquestrador-bootstrap.service.example new file mode 100644 index 0000000..3814063 --- /dev/null +++ b/deploy/systemd/orquestrador-bootstrap.service.example @@ -0,0 +1,15 @@ +[Unit] +Description=AI Orquestrador Database Bootstrap +After=network.target + +[Service] +Type=oneshot +User=vitor +Group=vitor +WorkingDirectory=/opt/orquestrador +EnvironmentFile=/opt/orquestrador/.env.prod +Environment=PATH=/opt/orquestrador/venv/bin +ExecStart=/opt/orquestrador/venv/bin/python -m app.db.bootstrap + +[Install] +WantedBy=multi-user.target diff --git a/deploy/systemd/orquestrador.service.example b/deploy/systemd/orquestrador.service.example index b390ca9..9d3e611 100644 --- a/deploy/systemd/orquestrador.service.example +++ b/deploy/systemd/orquestrador.service.example @@ -9,7 +9,7 @@ Group=vitor WorkingDirectory=/opt/orquestrador EnvironmentFile=/opt/orquestrador/.env.prod Environment=PATH=/opt/orquestrador/venv/bin -ExecStart=/bin/sh -c '/opt/orquestrador/venv/bin/python -m app.db.init_db && /opt/orquestrador/venv/bin/python -m app.integrations.telegram_satellite_service' +ExecStart=/opt/orquestrador/venv/bin/python -m app.integrations.telegram_satellite_service Restart=always RestartSec=5 diff --git a/docker-compose.yml b/docker-compose.yml index ef56082..91746ac 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,3 +1,31 @@ +x-orquestrador-env: &orquestrador-env + GOOGLE_PROJECT_ID: ${GOOGLE_PROJECT_ID:-local-dev} + GOOGLE_LOCATION: ${GOOGLE_LOCATION:-us-central1} + VERTEX_MODEL_NAME: ${VERTEX_MODEL_NAME:-gemini-2.5-pro} + ENVIRONMENT: ${ENVIRONMENT:-development} + DEBUG: ${ORQUESTRADOR_DEBUG:-false} + DB_HOST: mysql + DB_PORT: 3306 + DB_USER: root + DB_PASSWORD: root + DB_NAME: orquestrador_mock + MOCK_DB_HOST: mysql + MOCK_DB_PORT: 3306 + MOCK_DB_USER: root + MOCK_DB_PASSWORD: root + MOCK_DB_NAME: orquestrador_mock + AUTO_SEED_TOOLS: ${AUTO_SEED_TOOLS:-true} + AUTO_SEED_MOCK: ${AUTO_SEED_MOCK:-true} + MOCK_SEED_ENABLED: ${MOCK_SEED_ENABLED:-true} + TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:-} + TELEGRAM_POLLING_TIMEOUT: ${TELEGRAM_POLLING_TIMEOUT:-30} + TELEGRAM_REQUEST_TIMEOUT: ${TELEGRAM_REQUEST_TIMEOUT:-45} + CONVERSATION_STATE_BACKEND: ${CONVERSATION_STATE_BACKEND:-redis} + CONVERSATION_STATE_TTL_MINUTES: ${CONVERSATION_STATE_TTL_MINUTES:-60} + REDIS_URL: ${REDIS_URL:-redis://redis:6379/0} + REDIS_KEY_PREFIX: ${REDIS_KEY_PREFIX:-orquestrador} + REDIS_SOCKET_TIMEOUT_SECONDS: ${REDIS_SOCKET_TIMEOUT_SECONDS:-5} + services: mysql: image: mysql:8.4 @@ -30,33 +58,7 @@ services: telegram: build: . container_name: orquestrador_telegram - environment: - GOOGLE_PROJECT_ID: ${GOOGLE_PROJECT_ID:-local-dev} - GOOGLE_LOCATION: ${GOOGLE_LOCATION:-us-central1} - VERTEX_MODEL_NAME: ${VERTEX_MODEL_NAME:-gemini-2.5-pro} - ENVIRONMENT: ${ENVIRONMENT:-development} - DEBUG: ${ORQUESTRADOR_DEBUG:-false} - DB_HOST: mysql - DB_PORT: 3306 - DB_USER: root - DB_PASSWORD: root - DB_NAME: orquestrador_mock - MOCK_DB_HOST: mysql - MOCK_DB_PORT: 3306 - MOCK_DB_USER: root - MOCK_DB_PASSWORD: root - MOCK_DB_NAME: orquestrador_mock - AUTO_SEED_TOOLS: ${AUTO_SEED_TOOLS:-true} - AUTO_SEED_MOCK: ${AUTO_SEED_MOCK:-true} - MOCK_SEED_ENABLED: ${MOCK_SEED_ENABLED:-true} - TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:-} - TELEGRAM_POLLING_TIMEOUT: ${TELEGRAM_POLLING_TIMEOUT:-30} - TELEGRAM_REQUEST_TIMEOUT: ${TELEGRAM_REQUEST_TIMEOUT:-45} - CONVERSATION_STATE_BACKEND: ${CONVERSATION_STATE_BACKEND:-redis} - CONVERSATION_STATE_TTL_MINUTES: ${CONVERSATION_STATE_TTL_MINUTES:-60} - REDIS_URL: ${REDIS_URL:-redis://redis:6379/0} - REDIS_KEY_PREFIX: ${REDIS_KEY_PREFIX:-orquestrador} - REDIS_SOCKET_TIMEOUT_SECONDS: ${REDIS_SOCKET_TIMEOUT_SECONDS:-5} + environment: *orquestrador-env depends_on: mysql: condition: service_healthy @@ -64,5 +66,15 @@ services: condition: service_healthy restart: unless-stopped + bootstrap: + build: . + profiles: ["bootstrap"] + environment: *orquestrador-env + command: ["python", "-m", "app.db.bootstrap"] + depends_on: + mysql: + condition: service_healthy + restart: "no" + volumes: mysql_data: diff --git a/tests/test_runtime_bootstrap.py b/tests/test_runtime_bootstrap.py index 2d988f8..afec630 100644 --- a/tests/test_runtime_bootstrap.py +++ b/tests/test_runtime_bootstrap.py @@ -1,7 +1,9 @@ import unittest -from unittest.mock import patch +from unittest.mock import AsyncMock, patch +from app import main as main_module from app.core.settings import Settings +from app.db import bootstrap as bootstrap_module from app.db import init_db as init_db_module @@ -25,35 +27,35 @@ class SettingsParsingTests(unittest.TestCase): self.assertEqual(settings.conversation_state_backend, "redis") -class InitDbBootstrapTests(unittest.TestCase): - @patch.object(init_db_module, "seed_tools") - @patch.object(init_db_module, "seed_mock_data") - @patch.object(init_db_module.MockBase.metadata, "create_all") - @patch.object(init_db_module.Base.metadata, "create_all") - def test_init_db_respects_seed_flags( +class BootstrapRuntimeTests(unittest.TestCase): + @patch.object(bootstrap_module, "seed_tools") + @patch.object(bootstrap_module, "seed_mock_data") + @patch.object(bootstrap_module.MockBase.metadata, "create_all") + @patch.object(bootstrap_module.Base.metadata, "create_all") + def test_bootstrap_databases_respects_seed_flags( self, tools_create_all, mock_create_all, seed_mock_data, seed_tools, ): - with patch.object(init_db_module.settings, "auto_seed_tools", False), patch.object( - init_db_module.settings, + with patch.object(bootstrap_module.settings, "auto_seed_tools", False), patch.object( + bootstrap_module.settings, "auto_seed_mock", False, - ), patch.object(init_db_module.settings, "mock_seed_enabled", True): - init_db_module.init_db() + ), patch.object(bootstrap_module.settings, "mock_seed_enabled", True): + bootstrap_module.bootstrap_databases() tools_create_all.assert_called_once() mock_create_all.assert_called_once() seed_tools.assert_not_called() seed_mock_data.assert_not_called() - @patch.object(init_db_module, "seed_tools") - @patch.object(init_db_module, "seed_mock_data") - @patch.object(init_db_module.MockBase.metadata, "create_all") - @patch.object(init_db_module.Base.metadata, "create_all", side_effect=RuntimeError("tools db down")) - def test_init_db_raises_when_any_backend_fails( + @patch.object(bootstrap_module, "seed_tools") + @patch.object(bootstrap_module, "seed_mock_data") + @patch.object(bootstrap_module.MockBase.metadata, "create_all") + @patch.object(bootstrap_module.Base.metadata, "create_all", side_effect=RuntimeError("tools db down")) + def test_bootstrap_databases_raises_when_any_backend_fails( self, tools_create_all, mock_create_all, @@ -61,13 +63,31 @@ class InitDbBootstrapTests(unittest.TestCase): seed_tools, ): with self.assertRaisesRegex(RuntimeError, "tools=tools db down"): - init_db_module.init_db() + bootstrap_module.bootstrap_databases() tools_create_all.assert_called_once() mock_create_all.assert_called_once() seed_mock_data.assert_called_once() seed_tools.assert_not_called() + @patch.object(init_db_module, "bootstrap_databases") + def test_init_db_wrapper_delegates_to_bootstrap_databases(self, bootstrap_databases): + init_db_module.init_db() + + bootstrap_databases.assert_called_once_with() + + +class HttpStartupTests(unittest.IsolatedAsyncioTestCase): + async def test_startup_event_warms_llm_without_running_bootstrap(self): + with patch("app.main.LLMService") as llm_cls, patch( + "app.db.bootstrap.bootstrap_databases" + ) as bootstrap_databases: + llm_cls.return_value.warmup = AsyncMock() + await main_module.startup_event() + + llm_cls.return_value.warmup.assert_awaited_once() + bootstrap_databases.assert_not_called() + if __name__ == "__main__": unittest.main()