From 0b030fecca9f4dd699b7b4890d56cedcfb5cd136 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 13 Jul 2026 22:58:54 -0400 Subject: [PATCH] Split maintenance duties from notifier --- ecosystem.config.js | 14 +++++ src/ecosystem.config.spec.ts | 13 ++++ src/maintenance-main.ts | 16 +++++ src/maintenance.module.ts | 53 ++++++++++++++++ src/notifier.module.ts | 6 -- src/services/pool-summary-refresh.service.ts | 65 -------------------- 6 files changed, 96 insertions(+), 71 deletions(-) create mode 100644 src/maintenance-main.ts create mode 100644 src/maintenance.module.ts delete mode 100644 src/services/pool-summary-refresh.service.ts diff --git a/ecosystem.config.js b/ecosystem.config.js index 5f88259..a8c5a93 100644 --- a/ecosystem.config.js +++ b/ecosystem.config.js @@ -60,6 +60,20 @@ module.exports = { }, time: true, }, + // Non-hot-path master duties: notifications, reporting, and cleanup. + { + ...dockerLogConfig, + name: 'maintenance', + script: './dist/maintenance-main.js', + instances: 1, + exec_mode: 'fork', + env: { + MASTER: 'true', + API_ENABLED: 'false', + NODE_CLUSTER_SCHED_POLICY: 'none', + }, + time: true, + }, // Worker instances { ...dockerLogConfig, diff --git a/src/ecosystem.config.spec.ts b/src/ecosystem.config.spec.ts index 7796927..489a2f7 100644 --- a/src/ecosystem.config.spec.ts +++ b/src/ecosystem.config.spec.ts @@ -46,6 +46,19 @@ describe('PM2 worker sizing', () => { ); }); + it('runs maintenance duties outside the isolated notifier process', () => { + const config = require('../ecosystem.config.js'); + + expect(config.apps.find((app) => app.name === 'maintenance')).toEqual( + expect.objectContaining({ + script: './dist/maintenance-main.js', + instances: 1, + exec_mode: 'fork', + env: expect.objectContaining({ MASTER: 'true', API_ENABLED: 'false' }), + }), + ); + }); + it('rejects invalid fixed worker counts instead of silently starting no workers', () => { process.env.STRATUM_WORKERS = 'many'; diff --git a/src/maintenance-main.ts b/src/maintenance-main.ts new file mode 100644 index 0000000..5fbf7e7 --- /dev/null +++ b/src/maintenance-main.ts @@ -0,0 +1,16 @@ +import { NestFactory } from '@nestjs/core'; + +import { MaintenanceModule } from './maintenance.module'; + +async function bootstrap(): Promise { + process.env.MASTER = 'true'; + process.env.API_ENABLED = 'false'; + + const application = await NestFactory.createApplicationContext( + MaintenanceModule, + ); + application.enableShutdownHooks(); + console.log('Maintenance services started'); +} + +void bootstrap(); diff --git a/src/maintenance.module.ts b/src/maintenance.module.ts new file mode 100644 index 0000000..3d77ce1 --- /dev/null +++ b/src/maintenance.module.ts @@ -0,0 +1,53 @@ +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { UserAgentReportModule } from './ORM/_views/user-agent-report/user-agent-report.module'; +import { ClientModule } from './ORM/client/client.module'; +import { RpcBlocksModule } from './ORM/rpc-block/rpc-block.module'; +import { ShareAccountingModule } from './ORM/share-accounting/share-accounting.module'; +import { TelegramSubscriptionsModule } from './ORM/telegram-subscriptions/telegram-subscriptions.module'; +import { createDatabaseOptions } from './database.config'; +import { AppService } from './services/app.service'; +import { DiscordService } from './services/discord.service'; +import { NotificationService } from './services/notification.service'; +import { RedisMessagingModule } from './services/redis-messaging.module'; +import { TelegramService } from './services/telegram.service'; + +/** + * Background process for non-hot-path master duties. Keep this separate from + * NotifierModule so chat integrations, reporting, and cleanup timers cannot + * delay block-template notification. + */ +@Module({ + imports: [ + ConfigModule.forRoot(), + TypeOrmModule.forRootAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (configService: ConfigService) => createDatabaseOptions({ + ...process.env, + DB_HOST: configService.get('DB_HOST'), + DB_PORT: configService.get('DB_PORT'), + DB_USERNAME: configService.get('DB_USERNAME'), + DB_PASSWORD: configService.get('DB_PASSWORD'), + DB_DATABASE: configService.get('DB_DATABASE'), + DB_LOGGING: configService.get('DB_LOGGING'), + DB_POOL_SIZE: configService.get('DB_POOL_SIZE'), + }), + }), + RedisMessagingModule, + ClientModule, + RpcBlocksModule, + UserAgentReportModule, + ShareAccountingModule, + TelegramSubscriptionsModule, + ], + providers: [ + AppService, + DiscordService, + NotificationService, + TelegramService, + ], +}) +export class MaintenanceModule { } diff --git a/src/notifier.module.ts b/src/notifier.module.ts index ba8b29f..c21c204 100644 --- a/src/notifier.module.ts +++ b/src/notifier.module.ts @@ -3,12 +3,9 @@ import { ConfigModule, ConfigService } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; import { createDatabaseOptions } from './database.config'; -import { AcceptedShareEntity } from './ORM/accepted-share/accepted-share.entity'; import { PayoutSnapshotModule } from './ORM/payout-snapshot/payout-snapshot.module'; import { RpcBlocksModule } from './ORM/rpc-block/rpc-block.module'; -import { ShareAccountingService } from './ORM/share-accounting/share-accounting.service'; import { BitcoinRpcService } from './services/bitcoin-rpc.service'; -import { PoolSummaryRefreshService } from './services/pool-summary-refresh.service'; import { RedisMessagingModule } from './services/redis-messaging.module'; /** @@ -33,15 +30,12 @@ import { RedisMessagingModule } from './services/redis-messaging.module'; DB_POOL_SIZE: configService.get('DB_POOL_SIZE'), }), }), - TypeOrmModule.forFeature([AcceptedShareEntity]), RedisMessagingModule, RpcBlocksModule, PayoutSnapshotModule, ], providers: [ BitcoinRpcService, - PoolSummaryRefreshService, - ShareAccountingService, ], }) export class NotifierModule { } diff --git a/src/services/pool-summary-refresh.service.ts b/src/services/pool-summary-refresh.service.ts deleted file mode 100644 index bfbf1df..0000000 --- a/src/services/pool-summary-refresh.service.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; - -import { ShareAccountingService } from '../ORM/share-accounting/share-accounting.service'; - -const DEFAULT_REFRESH_INTERVAL_MS = 5 * 60 * 1000; -const DEFAULT_STARTUP_DELAY_MS = 15 * 1000; - -@Injectable() -export class PoolSummaryRefreshService implements OnModuleInit, OnModuleDestroy { - private timer: NodeJS.Timeout | null = null; - private startupTimer: NodeJS.Timeout | null = null; - private refreshing = false; - - constructor(private readonly shareAccountingService: ShareAccountingService) { } - - public onModuleInit(): void { - if (process.env.MASTER !== 'true' || process.env.API_ONLY === 'true') { - return; - } - - this.startupTimer = setTimeout(() => { - void this.refresh(); - }, this.readPositiveInt('POOL_SUMMARY_REFRESH_STARTUP_DELAY_MS', DEFAULT_STARTUP_DELAY_MS)); - this.startupTimer.unref?.(); - - this.timer = setInterval(() => { - void this.refresh(); - }, this.readPositiveInt('POOL_SUMMARY_REFRESH_INTERVAL_MS', DEFAULT_REFRESH_INTERVAL_MS)); - this.timer.unref?.(); - } - - public async onModuleDestroy(): Promise { - if (this.startupTimer != null) { - clearTimeout(this.startupTimer); - this.startupTimer = null; - } - - if (this.timer != null) { - clearInterval(this.timer); - this.timer = null; - } - } - - private async refresh(): Promise { - if (this.refreshing) { - return; - } - - this.refreshing = true; - try { - await this.shareAccountingService.refreshPoolSummary(); - await this.shareAccountingService.refreshPoolSummary('pplns'); - await this.shareAccountingService.refreshPoolSummary('solo'); - } catch (error) { - console.error(`Failed refreshing pool accounting summary: ${error.message}`); - } finally { - this.refreshing = false; - } - } - - private readPositiveInt(name: string, fallback: number): number { - const value = Number(process.env[name]); - return Number.isInteger(value) && value > 0 ? value : fallback; - } -}