mirror of
https://github.com/benjamin-wilson/public-pool.git
synced 2026-09-29 09:05:06 -07:00
278 lines
9.4 KiB
TypeScript
278 lines
9.4 KiB
TypeScript
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<void> {
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|