import { config } from "../config/index.js"; import { descarregarModelo } from "../services/ollamaClient.js"; import { partesLocaisSaoPaulo } from "./timezone.js"; // jobs que usam o Ollama em lote (avaliação, ingest RAG) — só um por vez, pra não somar // memória/GPU e travar a máquina let jobPesadoAtual = null; // janela "HH-HH" no fuso de São Paulo (ex.: "19-07" atravessa a meia-noite); vazia = sempre export function dentroDaJanela(date = new Date()) { const janela = config.jobsPesados.janela; if (!janela) return true; const m = /^(\d{1,2})-(\d{1,2})$/.exec(janela); if (!m) return true; const inicio = Number(m[1]); const fim = Number(m[2]); const hora = Number(partesLocaisSaoPaulo(date, { hour: "numeric", hourCycle: "h23" }).hour); if (inicio === fim) return true; return inicio < fim ? hora >= inicio && hora < fim : hora >= inicio || hora < fim; } export function pausaEntreItens() { const ms = config.jobsPesados.pausaMs; return ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve(); } // roda `fn` se nenhum outro job pesado estiver rodando e se estiver dentro da janela; // caso contrário devolve null (o ciclo é só pulado, o próximo tenta de novo) export async function comLockPesado(nome, fn) { if (!dentroDaJanela()) { console.log(`[${nome}] fora da janela JOBS_PESADOS_JANELA=${config.jobsPesados.janela}, pulando`); return null; } if (jobPesadoAtual) { console.log(`[${nome}] ocupado por ${jobPesadoAtual}, pulando este ciclo`); return null; } jobPesadoAtual = nome; try { return await fn(); } finally { jobPesadoAtual = null; } } // disparo manual (admin via HTTP): ignora a janela, mas respeita a exclusão mútua com os jobs — // se houver um job pesado rodando, falha com 409 em vez de somar carga no Ollama export async function comLockPesadoManual(nome, fn) { if (jobPesadoAtual) { // o frontend mostra a mensagem crua — por isso em português const err = new Error(`Há outro processamento pesado em andamento (${jobPesadoAtual}). Tente de novo em alguns minutos.`); err.statusCode = 409; throw err; } jobPesadoAtual = nome; try { return await fn(); } finally { jobPesadoAtual = null; } } // best-effort: falha ao descarregar não deve derrubar o job export async function descarregarModelos(nome, modelos) { if (!config.jobsPesados.descarregarAoFim) return; for (const model of new Set(modelos.filter(Boolean))) { try { await descarregarModelo(model); } catch (err) { console.warn(`[${nome}] falha ao descarregar ${model}:`, err.message); } } }