mirror of
https://github.com/benjamin-wilson/public-pool.git
synced 2026-09-29 09:05:06 -07:00
Split maintenance duties from notifier
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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();
|
||||
@@ -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 { }
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user