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:
Frederico Castro
2025-12-10 01:55:55 -03:00
parent 3ea6a4409e
commit c6aaf66e87
4 changed files with 112 additions and 35 deletions

View File

@@ -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()