Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 17 additions & 8 deletions listener/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,19 @@ import { EventDeduplicationService } from './services/event-deduplication-servic

dotenv.config();

// Track process startup time for uptime calculation
const PROCESS_START_TIME = Date.now();

async function main() {
const config = loadConfig();
// Validate all config values before starting any services (#494).
// This throws a descriptive ConfigError listing every problem found.
validateConfig(config);

let scheduler: NotificationScheduler | null = null;
let retryScheduler: RetryScheduler | null = null;
let notificationAPI: NotificationAPI | null = null;
let healthMonitor: NotificationHealthMonitor | null = null;
let subscriber: EventSubscriber | null = null;

let templateService: NotificationTemplateService | null = null;
let legacyTemplateService: TemplateService | null = null;
let cleanupService: CleanupService | null = null;
Expand All @@ -64,6 +67,12 @@ async function main() {
const db = await initializeDatabase(config.databasePath);

repository = new ScheduledNotificationRepository(db);

healthMonitor = new NotificationHealthMonitor(null, getWorkerManager(), {
repository,
getUptimeMs: () => Date.now() - PROCESS_START_TIME,
});

healthMonitor = new NotificationHealthMonitor(null, getWorkerManager(), {
repository,
});
Expand Down Expand Up @@ -91,7 +100,6 @@ async function main() {
logger.info('Notification metrics runner started successfully');
}

// Archive service: moves old notifications to the archive table.
const archiveCfg = loadArchiveConfig();
archiveStore = new ArchiveStore(db);
archiveService = new ArchiveService(db, archiveCfg);
Expand All @@ -109,16 +117,13 @@ async function main() {
templateService = new NotificationTemplateService(templateRepository);

if (config.scheduler?.enabled) {
repository = new ScheduledNotificationRepository(db);
notificationAPI = new NotificationAPI(repository);

// Initialize legacy template service
const legacyTemplateRepo = new TemplateRepository(db);
legacyTemplateService = new TemplateService(legacyTemplateRepo);

logger.info('Template service initialized successfully');

// Initialize scheduler with Discord service if available
let discordService: DiscordNotificationService | null = null;
if (config.discord) {
discordService = new DiscordNotificationService(config.discord);
Expand Down Expand Up @@ -163,6 +168,7 @@ async function main() {
healthMonitor.start();
}

subscriber = new EventSubscriber(config, deduplicationService);
const subscriber = new EventSubscriber(config, deduplicationService ?? undefined);
await subscriber.start();

Expand Down Expand Up @@ -207,8 +213,11 @@ async function main() {
await retryScheduler.stop();
}

if (subscriber) {
await subscriber.stop();
eventsServer.close();
}

eventsServer.close();

logger.info('Graceful shutdown completed successfully', { signal });
process.exit(0);
Expand Down Expand Up @@ -240,4 +249,4 @@ main().catch((err) => {
logger.error('Error starting service', { error: err });
}
process.exit(1);
});
});
22 changes: 22 additions & 0 deletions listener/src/services/notification-health-monitor.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NotificationHealthMonitor } from './notification-health-monitor';

describe('NotificationHealthMonitor', () => {
it('should include uptimeMs in the health report', () => {
const mockNow = () => 1000;
const mockGetUptimeMs = () => 5000;

const monitor = new NotificationHealthMonitor(null, null, {
now: mockNow,
getUptimeMs: mockGetUptimeMs,
});

monitor.start();
const report = monitor.getLastReport();

expect(report).not.toBeNull();
expect(report!.uptimeMs).toBe(5000);
expect(report!.status).toBe('healthy');

monitor.stop();
});
});
9 changes: 8 additions & 1 deletion listener/src/services/notification-health-monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ export interface HealthReport {
queue: QueueHealth;
workers: WorkerHealth;
registry: RegistryHealth;
/** Process uptime in milliseconds since startup. */
uptimeMs: number;
polling: PollingHealth;
}

Expand All @@ -66,6 +68,8 @@ export interface NotificationHealthMonitorOptions {
now?: () => number;
/** Optional repository used to surface DLQ depth in the health report. */
repository?: ScheduledNotificationRepository | null;
/** Function to calculate uptime in milliseconds. */
getUptimeMs?: () => number;
}

/**
Expand All @@ -80,6 +84,7 @@ export class NotificationHealthMonitor {
private readonly stallThresholdCycles: number;
private readonly maxProcessingDelayMs: number;
private readonly now: () => number;
private readonly getUptimeMs: () => number;

private queue: EventProcessingQueue | null;
private workerManager: WorkerManager | null;
Expand All @@ -105,6 +110,7 @@ export class NotificationHealthMonitor {
this.stallThresholdCycles = options.stallThresholdCycles ?? 3;
this.maxProcessingDelayMs = options.maxProcessingDelayMs ?? 60_000;
this.now = options.now ?? Date.now;
this.getUptimeMs = options.getUptimeMs ?? (() => 0);
}

start(): void {
Expand Down Expand Up @@ -151,6 +157,7 @@ export class NotificationHealthMonitor {
queue: queueHealth,
workers: workerHealth,
registry: registryHealth,
uptimeMs: this.getUptimeMs(),
polling: pollingHealth,
};

Expand Down Expand Up @@ -261,4 +268,4 @@ export class NotificationHealthMonitor {
if (statuses.includes('degraded')) return 'degraded';
return 'healthy';
}
}
}