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

Commit ae1ffdb

Browse files
committed
Close TCP server on maxConnections
1 parent f5c6d00 commit ae1ffdb

File tree

2 files changed

+86
-15
lines changed

2 files changed

+86
-15
lines changed

src/listener.ts

+83-14
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import { toMultiaddrConnection } from './socket-to-conn.js'
44
import { CODE_P2P } from './constants.js'
55
import {
66
getMultiaddrs,
7-
multiaddrToNetConfig
7+
multiaddrToNetConfig,
8+
NetConfig
89
} from './utils.js'
910
import { EventEmitter, CustomEvent } from '@libp2p/interfaces/events'
1011
import type { MultiaddrConnection, Connection } from '@libp2p/interface-connection'
@@ -25,15 +26,26 @@ async function attemptClose (maConn: MultiaddrConnection) {
2526
}
2627
}
2728

29+
export interface LimitServerConnectionsOpts {
30+
acceptBelow: number
31+
rejectAbove: number
32+
}
33+
2834
interface Context extends TCPCreateListenerOptions {
2935
handler?: (conn: Connection) => void
3036
upgrader: Upgrader
3137
socketInactivityTimeout?: number
3238
socketCloseTimeout?: number
3339
maxConnections?: number
40+
limitServerConnections?: LimitServerConnectionsOpts
3441
}
3542

36-
type Status = {started: false} | {started: true, listeningAddr: Multiaddr, peerId: string | null }
43+
type Status = {started: false} | {
44+
started: true
45+
listeningAddr: Multiaddr
46+
peerId: string | null
47+
netConfig: NetConfig
48+
}
3749

3850
export class TCPListener extends EventEmitter<ListenerEvents> implements Listener {
3951
private readonly server: net.Server
@@ -89,12 +101,30 @@ export class TCPListener extends EventEmitter<ListenerEvents> implements Listene
89101

90102
socket.once('close', () => {
91103
this.connections.delete(maConn)
104+
105+
if (
106+
this.context.limitServerConnections != null &&
107+
this.connections.size < this.context.limitServerConnections.acceptBelow
108+
) {
109+
this.netListen().catch(e => {
110+
log.error('error attempting to listen server once connection count under limit', e)
111+
})
112+
}
92113
})
93114

94115
if (this.context.handler != null) {
95116
this.context.handler(conn)
96117
}
97118

119+
if (
120+
this.context.limitServerConnections != null &&
121+
this.connections.size >= this.context.limitServerConnections.rejectAbove
122+
) {
123+
this.netClose().catch(e => {
124+
log.error('error attempting to close server on too many inbound connections', e)
125+
})
126+
}
127+
98128
this.dispatchEvent(new CustomEvent<Connection>('connection', { detail: conn }))
99129
})
100130
.catch(async err => {
@@ -148,21 +178,21 @@ export class TCPListener extends EventEmitter<ListenerEvents> implements Listene
148178
}
149179

150180
async listen (ma: Multiaddr) {
181+
if (this.status.started) {
182+
throw Error('server is already listening')
183+
}
184+
151185
const peerId = ma.getPeerId()
152186
const listeningAddr = peerId == null ? ma.decapsulateCode(CODE_P2P) : ma
153187

154-
this.status = { started: true, listeningAddr, peerId }
188+
this.status = {
189+
started: true,
190+
listeningAddr,
191+
peerId,
192+
netConfig: multiaddrToNetConfig(listeningAddr)
193+
}
155194

156-
return await new Promise<void>((resolve, reject) => {
157-
const options = multiaddrToNetConfig(listeningAddr)
158-
this.server.listen(options, (err?: any) => {
159-
if (err != null) {
160-
return reject(err)
161-
}
162-
log('Listening on %s', this.server.address())
163-
resolve()
164-
})
165-
})
195+
await this.netListen()
166196
}
167197

168198
async close () {
@@ -174,8 +204,47 @@ export class TCPListener extends EventEmitter<ListenerEvents> implements Listene
174204
Array.from(this.connections.values()).map(async maConn => await attemptClose(maConn))
175205
)
176206

207+
await this.netClose()
208+
}
209+
210+
private async netListen (): Promise<void> {
211+
if (!this.status.started || this.server.listening) {
212+
return
213+
}
214+
215+
const netConfig = this.status.netConfig
216+
177217
await new Promise<void>((resolve, reject) => {
178-
this.server.close(err => (err != null) ? reject(err) : resolve())
218+
// NOTE: 'listening' event is only fired on success. Any error such as port already binded, is emitted via 'error'
219+
this.server.once('error', reject)
220+
this.server.listen(netConfig, resolve)
179221
})
222+
223+
log('Listening on %s', this.server.address())
224+
}
225+
226+
private async netClose (): Promise<void> {
227+
if (!this.status.started || !this.server.listening) {
228+
return
229+
}
230+
231+
log('Closing server on %s', this.server.address())
232+
233+
// NodeJS implementation tracks listening status with `this._handle` property.
234+
// - Server.close() sets this._handle to null immediately. If this._handle is null, ERR_SERVER_NOT_RUNNING is thrown
235+
// - Server.listening returns `this._handle !== null` https://github.com/nodejs/node/blob/386d761943bb1b217fba27d6b80b658c23009e60/lib/net.js#L1675
236+
// - Server.listen() if `this._handle !== null` throws ERR_SERVER_ALREADY_LISTEN
237+
//
238+
// NOTE: Both listen and close are technically not async actions, so it's not necessary to track
239+
// states 'pending-close' or 'pending-listen'
240+
241+
// From docs https://nodejs.org/api/net.html#serverclosecallback
242+
// Stops the server from accepting new connections and keeps existing connections.
243+
// 'close' event is emitted only emitted when all connections are ended.
244+
// The optional callback will be called once the 'close' event occurs.
245+
//
246+
// NOTE: Since we want to keep existing connections and have checked `!this.server.listening` it's not necessary
247+
// to pass a callback to close.
248+
this.server.close()
180249
}
181250
}

src/utils.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import path from 'path'
66

77
const ProtoFamily = { ip4: 'IPv4', ip6: 'IPv6' }
88

9-
export function multiaddrToNetConfig (addr: Multiaddr): ListenOptions | (IpcSocketConnectOpts & TcpSocketConnectOpts) {
9+
export type NetConfig = ListenOptions | (IpcSocketConnectOpts & TcpSocketConnectOpts)
10+
11+
export function multiaddrToNetConfig (addr: Multiaddr): NetConfig {
1012
const listenPath = addr.getPath()
1113

1214
// unix socket listening

0 commit comments

Comments
 (0)