batchCooking/services/tech-step-llm-worker/src/scheduler.ts
Nicolas b61636f73d feat(admin): monitoring des microservices + heartbeat du worker LLM
PR 4 du chantier admin. Board de sante temps reel des dependances.

- POST /internal/tech-steps/heartbeat (requireInternalWorker) ->
  recordWorkerHeartbeat : upsert WorkerHeartbeat (cle fixe
  "tech-step-llm-worker"), lastRunAt/lastResult pour un ping "job".
  Schema workerHeartbeatSchema dans packages/shared.
- services/tech-step-llm-worker : api-client.postHeartbeat (best-effort,
  ne throw jamais) appele au boot (index.ts), a chaque tick et apres
  chaque job (scheduler.ts, avec job/ok/counts).
- admin-monitoring.service.ts + GET /admin/monitoring (requireAdmin) :
  sonde active bornee (~2 s) de Postgres (SELECT 1), l'API (uptime/RSS),
  tech-step-intent-service (/health), et le worker via son heartbeat.
  Statut up/degraded/down/unknown ; une sonde down ne casse ni les autres
  ni l'endpoint. Seuils worker : > 8 j degraded, > 21 j down.
- MonitoringView / ServiceHealthView dans packages/shared.
- Front : MonitoringPage (grille de cartes coloree par statut, re-poll
  15 s), logique pure monitoring.ts, i18n admin.monitoring.*,
  AdminApiClient.getMonitoring.
- Tests : Mocha admin-monitoring.test.ts (heartbeat 401/400/upsert
  job+boot ; GET /admin/monitoring 401, board 4 cibles, worker unknown
  sans heartbeat puis up apres) ; Cypress monitoring.cy.ts (2 verts).
  Worker mocha : 6/6 toujours verts.
- specs/backend-architecture.md : section monitoring. .gitignore :
  apps/admin-web/cypress/{screenshots,videos,downloads}.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 19:05:41 +02:00

72 lines
2.8 KiB
TypeScript

import cron from "node-cron";
import { postHeartbeat } from "./api-client.js";
import { env } from "./config.js";
import { runAuditLowConfidenceJob } from "./jobs/audit-low-confidence.js";
import { runTransformCorrectionsJob } from "./jobs/transform-corrections.js";
import { TechStepLlmService } from "./llm-verdict.js";
import { loadTechStepTaxonomy } from "./tech-step-taxonomy.js";
/**
* Runs one full cycle: fetch the taxonomy, load the model, run both jobs,
* dispose the model. The model is never kept loaded between scheduled
* runs (see `llm-verdict.ts`'s `dispose()` doc comment) — this function's
* own duration (model load/dispose easily adds several seconds) is an
* accepted cost of keeping this process's idle RAM footprint low between
* runs, not something to optimize away.
*/
export async function runOnce(): Promise<void> {
console.info("[tech-step-llm-worker] starting scheduled run...");
const taxonomy = await loadTechStepTaxonomy();
const techStepKeys = taxonomy.map((techStep) => techStep.key);
const llm = new TechStepLlmService();
try {
await llm.initialize(techStepKeys);
const auditCount = await runAuditLowConfidenceJob(llm, {
locale: env.TECH_STEP_WORKER_LOCALE,
limit: env.TECH_STEP_WORKER_BATCH_LIMIT,
});
console.info(`[tech-step-llm-worker] audit-low-confidence: ${auditCount} suggestion(s)`);
await postHeartbeat({
event: "job",
job: "audit-low-confidence",
ok: true,
counts: { suggestions: auditCount },
});
const correctionCount = await runTransformCorrectionsJob(llm, {
limit: env.TECH_STEP_WORKER_BATCH_LIMIT,
});
console.info(`[tech-step-llm-worker] transform-corrections: ${correctionCount} suggestion(s)`);
await postHeartbeat({
event: "job",
job: "transform-corrections",
ok: true,
counts: { suggestions: correctionCount },
});
} finally {
await llm.dispose();
}
console.info("[tech-step-llm-worker] scheduled run complete.");
}
/**
* Starts the long-lived cron loop — {@link runOnce} fires on
* `env.TECH_STEP_WORKER_CRON`'s schedule, indefinitely, until the process
* is stopped. A run that throws is logged, not left to crash the process —
* the next scheduled fire still happens; a transient API/model failure on
* one run shouldn't permanently kill the worker until someone notices and
* manually restarts its container.
*/
export function startScheduler(): void {
console.info(`[tech-step-llm-worker] scheduling runs on "${env.TECH_STEP_WORKER_CRON}"`);
cron.schedule(env.TECH_STEP_WORKER_CRON, () => {
// Prove liveness even for a fire that then fails inside `runOnce`.
void postHeartbeat({ event: "tick" });
runOnce().catch((err: unknown) => {
console.error("[tech-step-llm-worker] scheduled run failed:", err);
void postHeartbeat({ event: "job", ok: false });
});
});
}