Skip to content
This repository was archived by the owner on Aug 29, 2023. It is now read-only.

feat: add server.maxConnections option #213

Merged
merged 3 commits into from
Oct 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ export interface TCPOptions {
* When closing a socket, wait this long for it to close gracefully before it is closed more forcibly
*/
socketCloseTimeout?: number

/**
* Set this property to reject connections when the server's connection count gets high.
* https://nodejs.org/api/net.html#servermaxconnections
*/
maxConnections?: number
}

/**
Expand Down Expand Up @@ -158,6 +164,7 @@ export class TCP implements Transport {
createListener (options: TCPCreateListenerOptions): Listener {
return new TCPListener({
...options,
maxConnections: this.opts.maxConnections,
socketInactivityTimeout: this.opts.inboundSocketInactivityTimeout,
socketCloseTimeout: this.opts.socketCloseTimeout
})
Expand Down
8 changes: 8 additions & 0 deletions src/listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ interface Context extends TCPCreateListenerOptions {
upgrader: Upgrader
socketInactivityTimeout?: number
socketCloseTimeout?: number
maxConnections?: number
}

type Status = {started: false} | {started: true, listeningAddr: Multiaddr, peerId: string | null }
Expand All @@ -48,6 +49,13 @@ export class TCPListener extends EventEmitter<ListenerEvents> implements Listene

this.server = net.createServer(context, this.onSocket.bind(this))

// https://nodejs.org/api/net.html#servermaxconnections
// If set reject connections when the server's connection count gets high
// Useful to prevent too resource exhaustion via many open connections on high bursts of activity
if (context.maxConnections !== undefined) {
this.server.maxConnections = context.maxConnections
}

this.server
.on('listening', () => this.dispatchEvent(new CustomEvent('listening')))
.on('error', err => this.dispatchEvent(new CustomEvent<Error>('error', { detail: err })))
Expand Down
79 changes: 79 additions & 0 deletions test/max-connections.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { expect } from 'aegir/chai'
import net from 'node:net'
import { promisify } from 'node:util'
import { mockUpgrader } from '@libp2p/interface-mocks'
import { multiaddr } from '@multiformats/multiaddr'
import { TCP } from '../src/index.js'

describe('maxConnections', () => {
const afterEachCallbacks: Array<() => Promise<any> | any> = []
afterEach(async () => {
await Promise.all(afterEachCallbacks.map(fn => fn()))
afterEachCallbacks.length = 0
})

it('reject dial of connection above maxConnections', async () => {
const maxConnections = 2
const socketCount = 4
const port = 9900

const seenRemoteConnections = new Set<string>()
const tcp = new TCP({ maxConnections })

const upgrader = mockUpgrader()
const listener = tcp.createListener({ upgrader })
// eslint-disable-next-line @typescript-eslint/promise-function-async
afterEachCallbacks.push(() => listener.close())
await listener.listen(multiaddr(`/ip4/127.0.0.1/tcp/${port}`))

listener.addEventListener('connection', (conn) => {
seenRemoteConnections.add(conn.detail.remoteAddr.toString())
})

const sockets: net.Socket[] = []

for (let i = 0; i < socketCount; i++) {
const socket = net.connect({ port })
sockets.push(socket)

// eslint-disable-next-line @typescript-eslint/promise-function-async
afterEachCallbacks.unshift(async () => {
if (!socket.destroyed) {
socket.destroy()
await new Promise((resolve) => socket.on('close', resolve))
}
})

// Wait for connection so the order of sockets is stable, sockets expected to be alive are always [0,1]
await new Promise<void>((resolve, reject) => {
socket.on('connect', () => {
resolve()
})
socket.on('error', (err) => {
reject(err)
})
})
}

// With server.maxConnections the TCP socket is created and the initial handshake is completed
// Then in the server handler NodeJS javascript code will call socket.emit('drop') if over the limit
// https://github.com/nodejs/node/blob/fddc701d3c0eb4520f2af570876cc987ae6b4ba2/lib/net.js#L1706

// Wait for some time for server to drop all sockets above limit
await promisify(setTimeout)(250)

expect(seenRemoteConnections.size).equals(maxConnections, 'wrong serverConnections')

for (let i = 0; i < socketCount; i++) {
const socket = sockets[i]

if (i < maxConnections) {
// Assert socket connected
expect(socket.destroyed).equals(false, `socket ${i} under limit must not be destroyed`)
} else {
// Assert socket ended
expect(socket.destroyed).equals(true, `socket ${i} above limit must be destroyed`)
}
}
})
})