Files
oO/services/api/src/signals/scheduler.ts
alvis c4960d0601 feat(observability): structured logs, W3C trace IDs, Sentry hooks (#18)
- TS: pino + pino-http; every HTTP request log includes traceId from
  W3C traceparent header (generated if absent); forwarded to ml/serving
  on all /score, /generate, /reward, and /api/ml proxy calls
- Python: structlog JSON; FastAPI middleware binds trace_id via
  contextvars so every log line within a request carries it
- Sentry: optional SENTRY_DSN init in both runtimes (no-op if unset)
- Replace all console.* calls across services/api with pino logger
- Update tests to spy on logger instead of console

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 03:37:28 +00:00

57 lines
1.7 KiB
TypeScript

/**
* Todoist background sync scheduler (Issue #22).
*
* Periodically fetches Todoist tasks for every user with an active token so
* that signals are fresh before the next /recommend call. The on-demand fetch
* in TodoistSignalSource remains as the freshness-critical fallback — if a user
* hits /recommend and the cache is stale, it re-fetches inline.
*
* Interval: TODOIST_SYNC_INTERVAL_MS (default 15 min).
*/
import { db } from '../db/index.js';
import { integrationTokens } from '../db/schema.js';
import { eq } from 'drizzle-orm';
import { todoistSource } from './todoist.js';
import { logger } from '../logger.js';
const DEFAULT_INTERVAL_MS = 15 * 60 * 1000;
export function startTodoistSyncScheduler(intervalMs = DEFAULT_INTERVAL_MS): NodeJS.Timeout {
async function syncAll(): Promise<void> {
let users: { userId: string }[] = [];
try {
users = await db
.select({ userId: integrationTokens.userId })
.from(integrationTokens)
.where(eq(integrationTokens.tokenStatus, 'active'));
} catch (err: any) {
logger.error({ err }, 'scheduler: failed to query users');
return;
}
if (!users.length) return;
const results = await Promise.allSettled(
users.map((u) => todoistSource.fetchSignals(u.userId)),
);
let ok = 0;
let failed = 0;
for (const r of results) {
if (r.status === 'fulfilled') ok++;
else { failed++; logger.error({ err: r.reason }, 'scheduler: sync error'); }
}
logger.info({ ok, failed, total: users.length }, 'scheduler: todoist sync');
}
// Run once shortly after startup, then on interval
const delay = setTimeout(() => {
syncAll();
setInterval(syncAll, intervalMs);
}, 10_000);
return delay;
}