From c6aaf66e875e66dedf2ba245017976409a66e02c Mon Sep 17 00:00:00 2001 From: Frederico Castro Date: Wed, 10 Dec 2025 01:55:55 -0300 Subject: [PATCH] refactor: Substitui APScheduler por asyncio nativo para OCP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove dependência apscheduler - Implementa loop asyncio com sleep calculado - Compatível com ambientes sem cron (OCP/Kubernetes) - Documenta solução em SCHEDULER.md --- backend/SCHEDULER.md | 60 ++++++++++++++++ backend/requirements.txt | 1 - backend/src/application/jobs/scheduler.py | 83 ++++++++++++++--------- backend/src/interface/api/app.py | 3 +- 4 files changed, 112 insertions(+), 35 deletions(-) create mode 100644 backend/SCHEDULER.md diff --git a/backend/SCHEDULER.md b/backend/SCHEDULER.md new file mode 100644 index 0000000..45fe075 --- /dev/null +++ b/backend/SCHEDULER.md @@ -0,0 +1,60 @@ +# Scheduler de Ranking - Solução para OCP + +## Problema + +Em ambientes OpenShift Container Platform (OCP), não é possível usar cron dentro dos PODs. + +## Solução Implementada + +Loop asyncio nativo Python sem dependências externas. + +### Características + +- **100% Python nativo** - Usa apenas `asyncio` e `datetime` +- **Portável** - Funciona em qualquer ambiente Docker/Kubernetes/OCP +- **Sem dependências** - Removemos `apscheduler` do requirements.txt +- **Simples** - Loop infinito com sleep calculado até próximo horário + +### Funcionamento + +```python +# Calcula tempo até próxima execução (ex: 3h da manhã) +proxima_execucao = datetime.now().replace(hour=3, minute=0, second=0) +if agora >= proxima_execucao: + proxima_execucao += timedelta(days=1) + +# Aguarda assincronamente +await asyncio.sleep(segundos_ate_proxima) + +# Executa job +await job.executar(limpar_antes=True) +``` + +### Configuração + +Padrão: **3h da manhã** (horário do servidor) + +Para alterar: edite `backend/src/interface/api/app.py` linha 23: + +```python +await scheduler.iniciar(hora_alvo=3) # Altere para outra hora +``` + +### Logs + +``` +Próxima execução do ranking: 11/12/2025 03:00:00 +Scheduler do ranking iniciado: job rodará diariamente às 3h +``` + +### Execução Manual + +Se precisar rodar fora do schedule: + +```bash +curl -X POST "http://localhost:8000/api/v1/ranking/processar?limpar=true" +``` + +### Migração para OCP + +Nenhuma configuração adicional necessária. O container já está pronto. diff --git a/backend/requirements.txt b/backend/requirements.txt index ae680ec..ca29160 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -7,4 +7,3 @@ python-dateutil==2.8.2 httpx==0.26.0 python-dotenv==1.0.0 aiohttp==3.9.1 -apscheduler==3.10.4 diff --git a/backend/src/application/jobs/scheduler.py b/backend/src/application/jobs/scheduler.py index 818c7ae..ec69885 100644 --- a/backend/src/application/jobs/scheduler.py +++ b/backend/src/application/jobs/scheduler.py @@ -1,5 +1,5 @@ -from apscheduler.schedulers.asyncio import AsyncIOScheduler -from apscheduler.triggers.cron import CronTrigger +import asyncio +from datetime import datetime, time, timedelta from typing import Optional from .processar_ranking import ProcessarRankingJob @@ -8,43 +8,62 @@ from .processar_ranking import ProcessarRankingJob class RankingScheduler: def __init__(self, job: ProcessarRankingJob): self.job = job - self.scheduler: Optional[AsyncIOScheduler] = None + self.task: Optional[asyncio.Task] = None + self.running = False - def iniciar(self) -> None: + async def _aguardar_proximo_horario(self, hora_alvo: int = 3) -> None: """ - Inicia o scheduler e agenda o job para rodar diariamente às 3h. + Aguarda até a próxima ocorrência do horário alvo (padrão: 3h). """ - if self.scheduler and self.scheduler.running: + agora = datetime.now() + proxima_execucao = agora.replace(hour=hora_alvo, minute=0, second=0, microsecond=0) + + if agora >= proxima_execucao: + proxima_execucao += timedelta(days=1) + + segundos_ate_proxima = (proxima_execucao - agora).total_seconds() + print(f"Próxima execução do ranking: {proxima_execucao.strftime('%d/%m/%Y %H:%M:%S')}") + + await asyncio.sleep(segundos_ate_proxima) + + async def _loop_diario(self, hora_alvo: int = 3) -> None: + """ + Loop infinito que executa o job diariamente no horário especificado. + """ + while self.running: + try: + await self._aguardar_proximo_horario(hora_alvo) + + if not self.running: + break + + print(f"[{datetime.now().strftime('%d/%m/%Y %H:%M:%S')}] Executando job de ranking automático") + await self.job.executar(limpar_antes=True) + + except asyncio.CancelledError: + print("Scheduler cancelado") + break + except Exception as e: + print(f"Erro no scheduler: {e}") + await asyncio.sleep(3600) + + async def iniciar(self, hora_alvo: int = 3) -> None: + """ + Inicia o scheduler em background. + O job rodará diariamente no horário especificado (padrão: 3h). + """ + if self.running: return - self.scheduler = AsyncIOScheduler() - - self.scheduler.add_job( - self.job.executar, - trigger=CronTrigger(hour=3, minute=0), - id='ranking_diario', - name='Processamento diário do ranking de consultores', - replace_existing=True, - kwargs={"limpar_antes": True} - ) - - self.scheduler.start() + self.running = True + self.task = asyncio.create_task(self._loop_diario(hora_alvo)) + await asyncio.sleep(0.1) + print(f"Scheduler do ranking iniciado: job rodará diariamente às {hora_alvo}h") def parar(self) -> None: """ Para o scheduler. """ - if self.scheduler and self.scheduler.running: - self.scheduler.shutdown(wait=False) - - def executar_agora(self) -> None: - """ - Executa o job imediatamente (fora do agendamento). - """ - if self.scheduler: - self.scheduler.add_job( - self.job.executar, - id='ranking_manual', - replace_existing=True, - kwargs={"limpar_antes": True} - ) + self.running = False + if self.task: + self.task.cancel() diff --git a/backend/src/interface/api/app.py b/backend/src/interface/api/app.py index 23b1daf..6f8951a 100644 --- a/backend/src/interface/api/app.py +++ b/backend/src/interface/api/app.py @@ -20,8 +20,7 @@ async def lifespan(app: FastAPI): try: job = get_processar_job() scheduler = RankingScheduler(job) - scheduler.iniciar() - print("Scheduler do ranking iniciado: job rodará diariamente às 3h") + await scheduler.iniciar() except Exception as e: print(f"AVISO: Scheduler não iniciou: {e}")