refactor: Substitui APScheduler por asyncio nativo para OCP
- Remove dependência apscheduler - Implementa loop asyncio com sleep calculado - Compatível com ambientes sem cron (OCP/Kubernetes) - Documenta solução em SCHEDULER.md
This commit is contained in:
60
backend/SCHEDULER.md
Normal file
60
backend/SCHEDULER.md
Normal file
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user