Split maintenance duties from notifier

This commit is contained in:
Ben
2026-07-13 22:58:54 -04:00
parent 448f7d4c52
commit 0b030fecca
6 changed files with 96 additions and 71 deletions
+14
View File
@@ -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,
+13
View File
@@ -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';
+16
View File
@@ -0,0 +1,16 @@
import { NestFactory } from '@nestjs/core';
import { MaintenanceModule } from './maintenance.module';
async function bootstrap(): Promise<void> {
process.env.MASTER = 'true';
process.env.API_ENABLED = 'false';
const application = await NestFactory.createApplicationContext(
MaintenanceModule,
);
application.enableShutdownHooks();
console.log('Maintenance services started');
}
void bootstrap();
+53
View File
@@ -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 { }
-6
View File
@@ -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 { }
@@ -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<void> {
if (this.startupTimer != null) {
clearTimeout(this.startupTimer);
this.startupTimer = null;
}
if (this.timer != null) {
clearInterval(this.timer);
this.timer = null;
}
}
private async refresh(): Promise<void> {
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;
}
}