From 4cab56a1a0d76f70c9207560d286854c0584f87c Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 30 May 2026 19:24:34 -0400 Subject: [PATCH] prevent half connections staying open --- .env.example | 2 ++ Dockerfile | 4 ++-- README.md | 14 +++++++++++++ package-lock.json | 3 +++ package.json | 3 +++ src/services/stratum-v1.service.spec.ts | 26 +++++++++++++++++++++++++ src/services/stratum-v1.service.ts | 18 +++++++++++++++++ 7 files changed, 68 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index d8a70c2..9e08035 100644 --- a/.env.example +++ b/.env.example @@ -19,6 +19,8 @@ STRATUM_PORTS=3333,3332,3331,3330 STRATUM_SECURE=true SECURE_STRATUM_PORTS=4333,4332,4331,4330 +STRATUM_MAX_CONNECTIONS_PER_LISTENER=10000 +STRATUM_TLS_HANDSHAKE_TIMEOUT_MS=10000 #optional telegram bot #TELEGRAM_BOT_TOKEN= diff --git a/Dockerfile b/Dockerfile index e902ca9..07fa889 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ # Docker build environment # ############################ -FROM node:18.16.1-bookworm-slim AS build +FROM node:24.16.0-bookworm-slim AS build # Upgrade all packages and install dependencies RUN apt-get update \ @@ -24,7 +24,7 @@ RUN npm i && npm run build # Docker final environment # ############################ -FROM node:18.16.1-bookworm-slim +FROM node:24.16.0-bookworm-slim # Expose ports for Stratum and Bitcoin RPC EXPOSE 3333 3334 8332 diff --git a/README.md b/README.md index 2ed731e..cbc0641 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,20 @@ Install pm2 (https://pm2.keymetrics.io/) $ pm2 start dist/main.js ``` +When running the worker app in PM2 cluster mode, start the PM2 daemon with OS-level +connection scheduling. The environment variable must be present when the PM2 daemon +starts, not only in the worker configuration. + +```bash +$ NODE_CLUSTER_SCHED_POLICY=none pm2 start ecosystem.config.js +``` + +Cluster-mode connection dropping requires Node.js `22.12.0` or newer. + +`STRATUM_MAX_CONNECTIONS_PER_LISTENER` is enforced per worker and Stratum port. +Size it using the busiest port: `worker count * limit`. For example, 28 workers +with the default limit of `10000` allow up to `280000` connections on one port. + ## Docker Build container: diff --git a/package-lock.json b/package-lock.json index fcc7dae..8a8d020 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,9 @@ "version": "0.0.1", "hasInstallScript": true, "license": "UNLICENSED", + "engines": { + "node": ">=22.12.0" + }, "dependencies": { "@nestjs/axios": "^4.0.1", "@nestjs/cache-manager": "^3.1.2", diff --git a/package.json b/package.json index 94fb7cc..ba194dd 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,9 @@ "author": "", "private": true, "license": "UNLICENSED", + "engines": { + "node": ">=22.12.0" + }, "scripts": { "build": "nest build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", diff --git a/src/services/stratum-v1.service.spec.ts b/src/services/stratum-v1.service.spec.ts index 1031862..aca4cb4 100644 --- a/src/services/stratum-v1.service.spec.ts +++ b/src/services/stratum-v1.service.spec.ts @@ -6,6 +6,8 @@ describe('StratumV1Service', () => { const originalStratumSecure = process.env.STRATUM_SECURE; const originalSecureStratumPorts = process.env.SECURE_STRATUM_PORTS; const originalBackpressureEnabled = process.env.STRATUM_BACKPRESSURE_ENABLED; + const originalMaxConnectionsPerListener = process.env.STRATUM_MAX_CONNECTIONS_PER_LISTENER; + const originalTlsHandshakeTimeoutMs = process.env.STRATUM_TLS_HANDSHAKE_TIMEOUT_MS; let service: StratumV1Service; let clientService; @@ -37,6 +39,8 @@ describe('StratumV1Service', () => { restoreEnv('STRATUM_SECURE', originalStratumSecure); restoreEnv('SECURE_STRATUM_PORTS', originalSecureStratumPorts); restoreEnv('STRATUM_BACKPRESSURE_ENABLED', originalBackpressureEnabled); + restoreEnv('STRATUM_MAX_CONNECTIONS_PER_LISTENER', originalMaxConnectionsPerListener); + restoreEnv('STRATUM_TLS_HANDSHAKE_TIMEOUT_MS', originalTlsHandshakeTimeoutMs); consoleLogSpy.mockRestore(); consoleWarnSpy.mockRestore(); jest.useRealTimers(); @@ -119,6 +123,28 @@ describe('StratumV1Service', () => { expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('Resuming Stratum accepts')); }); + it('should cap listener connections and drop excess cluster connections', () => { + process.env.STRATUM_MAX_CONNECTIONS_PER_LISTENER = '250'; + const server = {} as any; + + (service as any).configureConnectionLimit(server); + + expect(server.maxConnections).toBe(250); + expect(server.dropMaxConnection).toBe(true); + }); + + it('should allow high-volume pools by default', () => { + delete process.env.STRATUM_MAX_CONNECTIONS_PER_LISTENER; + + expect((service as any).getMaxConnectionsPerListener()).toBe(10000); + }); + + it('should use an explicit TLS handshake timeout', () => { + process.env.STRATUM_TLS_HANDSHAKE_TIMEOUT_MS = '5000'; + + expect((service as any).getTlsHandshakeTimeoutMs()).toBe(5000); + }); + function restoreEnv(key: string, value: string | undefined) { if (value == null) { delete process.env[key]; diff --git a/src/services/stratum-v1.service.ts b/src/services/stratum-v1.service.ts index 4ba0282..434f071 100644 --- a/src/services/stratum-v1.service.ts +++ b/src/services/stratum-v1.service.ts @@ -29,6 +29,8 @@ 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; +const DEFAULT_TLS_HANDSHAKE_TIMEOUT_MS = 10000; @@ -162,6 +164,7 @@ export class StratumV1Service implements OnModuleInit { server.on('error', (err) => { console.error(`Server error: ${err.message}`); }); + this.configureConnectionLimit(server); return server; } @@ -186,6 +189,7 @@ export class StratumV1Service implements OnModuleInit { const tlsOptions: TlsOptions = { key: readFileSync(keyPath), cert: readFileSync(certPath), + handshakeTimeout: this.getTlsHandshakeTimeoutMs() }; const server = createServer(tlsOptions, async (socket: TLSSocket) => { @@ -242,6 +246,7 @@ export class StratumV1Service implements OnModuleInit { server.on('error', (err) => { console.error(`Server error: ${err.message}`); }); + this.configureConnectionLimit(server); return server; @@ -371,6 +376,19 @@ export class StratumV1Service implements OnModuleInit { return this.getPositiveIntegerEnv('STRATUM_BACKPRESSURE_HEALTHY_CHECKS', DEFAULT_BACKPRESSURE_HEALTHY_CHECKS); } + private configureConnectionLimit(server: Server) { + server.maxConnections = this.getMaxConnectionsPerListener(); + (server as Server & { dropMaxConnection: boolean }).dropMaxConnection = true; + } + + private getMaxConnectionsPerListener() { + return this.getPositiveIntegerEnv('STRATUM_MAX_CONNECTIONS_PER_LISTENER', DEFAULT_MAX_CONNECTIONS_PER_LISTENER); + } + + private getTlsHandshakeTimeoutMs() { + return this.getPositiveIntegerEnv('STRATUM_TLS_HANDSHAKE_TIMEOUT_MS', DEFAULT_TLS_HANDSHAKE_TIMEOUT_MS); + } + private getPositiveIntegerEnv(key: string, fallback: number) { const configured = parseInt(process.env[key], 10); if (Number.isFinite(configured) && configured > 0) {