← back to Ventura Corridor

src/jobs/magazine_watchdog.ts

77 lines

/**
 * Watchdog for the magazine generator. Runs every N min, checks whether:
 *   1. Any generate_features.ts process is alive
 *   2. magazine_features count grew in the last 10 min
 * If both fail, kicks off a fresh `npm run magazine:gen 200` in nohup.
 *
 *   $ npx tsx src/jobs/magazine_watchdog.ts
 */
import 'dotenv/config';
import { spawn, execSync } from 'node:child_process';
import { pool, query } from '../../db/pool.ts';
import { existsSync, mkdirSync, appendFileSync } from 'node:fs';
import { homedir } from 'node:os';
import path from 'node:path';

const PROJECT = path.join(homedir(), 'Projects', 'ventura-corridor');
const LOG_DIR = path.join(PROJECT, 'logs');
const LOG_FILE = path.join(LOG_DIR, 'magazine-watchdog.log');
const STALL_MIN = 10;

function log(msg: string) {
  if (!existsSync(LOG_DIR)) mkdirSync(LOG_DIR, { recursive: true });
  const line = `[${new Date().toISOString()}] ${msg}\n`;
  appendFileSync(LOG_FILE, line);
  process.stdout.write(line);
}

async function main() {
  // 1. Is there a live generator process?
  let alive = false;
  try {
    const out = execSync(`pgrep -af 'generate_features\\.ts' || true`, { encoding: 'utf8' });
    alive = out.trim().length > 0;
  } catch {}

  // 2. Has count grown in the last STALL_MIN min?
  const r = await query(`
    SELECT
      count(*)                                                                AS total,
      count(*) FILTER (WHERE generated_at > NOW() - INTERVAL '${STALL_MIN} minutes') AS recent
    FROM magazine_features
  `);
  const total = Number(r.rows[0].total);
  const recent = Number(r.rows[0].recent);

  log(`alive=${alive} total=${total} recent_${STALL_MIN}min=${recent}`);

  if (alive && recent > 0) {
    log(`OK · gen healthy`);
    await pool.end();
    return;
  }
  if (alive && recent === 0) {
    log(`STALL · gen process alive but no rows in ${STALL_MIN} min — killing + restarting`);
    try { execSync(`pkill -9 -f 'generate_features\\.ts' || true`); } catch {}
    await new Promise(r => setTimeout(r, 2000));
  } else if (!alive) {
    log(`DOWN · no gen process — starting`);
  }

  // Spawn fresh gen, detached
  const child = spawn('bash', ['-lc',
    `cd "${PROJECT}" && nohup npx tsx src/jobs/generate_features.ts 200 >> logs/magazine-gen-overnight.log 2>&1 &`
  ], { detached: true, stdio: 'ignore' });
  child.unref();
  await new Promise(r => setTimeout(r, 3000));
  let newPid = '';
  try { newPid = execSync(`pgrep -f 'generate_features\\.ts 200' | head -1`, { encoding: 'utf8' }).trim(); } catch {}
  log(`RESTART · started new gen pid=${newPid}`);
  await pool.end();
}

main().catch(e => {
  log(`fatal: ${e.message}`);
  process.exit(1);
});