import { Injectable, OnModuleInit } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Server, Socket } from 'net'; import { monitorEventLoopDelay } from 'perf_hooks'; import { StratumV1Client } from '../models/StratumV1Client'; import { AddressSettingsService } from '../ORM/address-settings/address-settings.service'; import { BlocksService } from '../ORM/blocks/blocks.service'; import { ClientStatisticsService } from '../ORM/client-statistics/client-statistics.service'; import { ClientService } from '../ORM/client/client.service'; import { BitcoinRpcService } from './bitcoin-rpc.service'; import { ExternalSharesService } from './external-shares.service'; import { NotificationService } from './notification.service'; import { StratumV1JobsService } from './stratum-v1-jobs.service'; interface StratumListenerState { port: number; server: Server | null; paused: boolean; } const DEFAULT_BACKPRESSURE_CHECK_INTERVAL_MS = 5000; const DEFAULT_BACKPRESSURE_EVENT_LOOP_P95_MS = 2000; const DEFAULT_BACKPRESSURE_EVENT_LOOP_RESUME_P95_MS = 250; const DEFAULT_BACKPRESSURE_RSS_MB = 2500; const DEFAULT_BACKPRESSURE_RESUME_RSS_MB = 2000; const DEFAULT_BACKPRESSURE_HEALTHY_CHECKS = 3; const DEFAULT_MAX_CONNECTIONS_PER_LISTENER = 10000; @Injectable() export class StratumV1Service implements OnModuleInit { private readonly listeners: StratumListenerState[] = []; private readonly eventLoopDelay = monitorEventLoopDelay({ resolution: 20 }); private backpressureMonitor: NodeJS.Timeout | null = null; private healthyBackpressureChecks = 0; constructor( private readonly bitcoinRpcService: BitcoinRpcService, private readonly clientService: ClientService, private readonly clientStatisticsService: ClientStatisticsService, private readonly notificationService: NotificationService, private readonly blocksService: BlocksService, private readonly configService: ConfigService, private readonly stratumV1JobsService: StratumV1JobsService, private readonly addressSettingsService: AddressSettingsService, private readonly externalSharesService: ExternalSharesService ) { } async onModuleInit(): Promise { if (process.env.NODE_APP_INSTANCE == '0' || process.env.MASTER == 'true') { await this.clientService.deleteAll(); } if (process.env.MASTER == 'true') { console.log('Master process skipping Stratum socket listeners'); return; } setTimeout(() => { this.startSocketServer(this.getStratumPort()); }, 1000 * 10); this.startBackpressureMonitor(); } private startSocketServer(port: number) { const listener: StratumListenerState = { port, server: null, paused: false }; this.listeners.push(listener); this.listen(listener); } private createSocketServer(): Server { const server = new Server(async (socket: Socket) => { //5 min socket.setTimeout(1000 * 60 * 5); const client = new StratumV1Client( socket, this.stratumV1JobsService, this.bitcoinRpcService, this.clientService, this.clientStatisticsService, this.notificationService, this.blocksService, this.configService, this.addressSettingsService, this.externalSharesService ); socket.on('close', async (hadError: boolean) => { if (client.extraNonceAndSessionId != null) { // Handle socket disconnection await client.destroy(); console.log(`Client ${client.extraNonceAndSessionId} disconnected, hadError?:${hadError}`); } }); socket.on('timeout', () => { console.log('socket timeout'); socket.end(); socket.destroy(); }); socket.on('error', async (error: Error) => { }); // //console.log(`Client disconnected, socket error, ${client.extraNonceAndSessionId}`); }); server.on('error', (err) => { console.error(`Server error: ${err.message}`); }); this.configureConnectionLimit(server); return server; } private listen(listener: StratumListenerState) { if (listener.server != null) { return; } const server = this.createSocketServer(); listener.server = server; listener.paused = false; server.listen(listener.port, () => { console.log(`Stratum server is listening on port ${listener.port}`); }); } private startBackpressureMonitor() { if (this.isBackpressureDisabled() || this.backpressureMonitor != null) { return; } this.eventLoopDelay.enable(); this.backpressureMonitor = setInterval(() => { this.checkBackpressure(); }, this.getBackpressureCheckIntervalMs()); } private checkBackpressure() { const eventLoopP95Ms = this.getEventLoopP95Ms(); const rssMb = Math.round(process.memoryUsage().rss / 1024 / 1024); const overloaded = eventLoopP95Ms >= this.getBackpressureEventLoopP95Ms() || rssMb >= this.getBackpressureRssMb(); const paused = this.listeners.some(listener => listener.paused); if (overloaded) { this.healthyBackpressureChecks = 0; if (!paused) { this.pauseAccepting(eventLoopP95Ms, rssMb); } this.eventLoopDelay.reset(); return; } if (!paused) { this.eventLoopDelay.reset(); return; } const healthy = eventLoopP95Ms <= this.getBackpressureResumeEventLoopP95Ms() && rssMb <= this.getBackpressureResumeRssMb(); if (!healthy) { this.healthyBackpressureChecks = 0; this.eventLoopDelay.reset(); return; } this.healthyBackpressureChecks++; if (this.healthyBackpressureChecks >= this.getBackpressureHealthyChecks()) { this.resumeAccepting(eventLoopP95Ms, rssMb); this.healthyBackpressureChecks = 0; } this.eventLoopDelay.reset(); } private pauseAccepting(eventLoopP95Ms: number, rssMb: number) { console.warn(`Pausing Stratum accepts: eventLoopP95Ms=${eventLoopP95Ms}, rssMb=${rssMb}`); for (const listener of this.listeners) { if (listener.paused || listener.server == null) { continue; } const server = listener.server; listener.server = null; listener.paused = true; server.close((error) => { if (error != null) { console.error(`Error while pausing Stratum listener on port ${listener.port}: ${error.message}`); } }); } } private resumeAccepting(eventLoopP95Ms: number, rssMb: number) { console.warn(`Resuming Stratum accepts: eventLoopP95Ms=${eventLoopP95Ms}, rssMb=${rssMb}`); for (const listener of this.listeners) { if (!listener.paused || listener.server != null) { continue; } this.listen(listener); } } private configureConnectionLimit(server: Server) { server.maxConnections = this.getMaxConnectionsPerListener(); (server as Server & { dropMaxConnection: boolean }).dropMaxConnection = true; } private getEventLoopP95Ms() { return Math.round(this.eventLoopDelay.percentile(95) / 1e6); } private isBackpressureDisabled() { return process.env.STRATUM_BACKPRESSURE_ENABLED?.toLowerCase() === 'false'; } private getBackpressureCheckIntervalMs() { return this.getPositiveIntegerEnv('STRATUM_BACKPRESSURE_CHECK_INTERVAL_MS', DEFAULT_BACKPRESSURE_CHECK_INTERVAL_MS); } private getBackpressureEventLoopP95Ms() { return this.getPositiveIntegerEnv('STRATUM_BACKPRESSURE_EVENT_LOOP_P95_MS', DEFAULT_BACKPRESSURE_EVENT_LOOP_P95_MS); } private getBackpressureResumeEventLoopP95Ms() { return this.getPositiveIntegerEnv('STRATUM_BACKPRESSURE_EVENT_LOOP_RESUME_P95_MS', DEFAULT_BACKPRESSURE_EVENT_LOOP_RESUME_P95_MS); } private getBackpressureRssMb() { return this.getPositiveIntegerEnv('STRATUM_BACKPRESSURE_RSS_MB', DEFAULT_BACKPRESSURE_RSS_MB); } private getBackpressureResumeRssMb() { return this.getPositiveIntegerEnv('STRATUM_BACKPRESSURE_RESUME_RSS_MB', DEFAULT_BACKPRESSURE_RESUME_RSS_MB); } private getBackpressureHealthyChecks() { return this.getPositiveIntegerEnv('STRATUM_BACKPRESSURE_HEALTHY_CHECKS', DEFAULT_BACKPRESSURE_HEALTHY_CHECKS); } private getMaxConnectionsPerListener() { return this.getPositiveIntegerEnv('STRATUM_MAX_CONNECTIONS_PER_LISTENER', DEFAULT_MAX_CONNECTIONS_PER_LISTENER); } private getStratumPort() { return this.getPositiveIntegerEnv('STRATUM_PORT', 3333); } private getPositiveIntegerEnv(key: string, fallback: number) { const configured = parseInt(process.env[key], 10); if (Number.isFinite(configured) && configured > 0) { return configured; } return fallback; } }