1
1
import net from 'net'
2
+ import { CodeError } from '@libp2p/interface/errors'
2
3
import { EventEmitter , CustomEvent } from '@libp2p/interface/events'
3
4
import { logger } from '@libp2p/logger'
4
5
import { CODE_P2P } from './constants.js'
@@ -46,17 +47,25 @@ interface Context extends TCPCreateListenerOptions {
46
47
closeServerOnMaxConnections ?: CloseServerOnMaxConnectionsOpts
47
48
}
48
49
49
- const SERVER_STATUS_UP = 1
50
- const SERVER_STATUS_DOWN = 0
51
-
52
50
export interface TCPListenerMetrics {
53
51
status : MetricGroup
54
52
errors : CounterGroup
55
53
events : CounterGroup
56
54
}
57
55
58
- type Status = { started : false } | {
59
- started : true
56
+ enum TCPListenerStatusCode {
57
+ /**
58
+ * When server object is initialized but we don't know the listening address yet or
59
+ * the server object is stopped manually, can be resumed only by calling listen()
60
+ **/
61
+ INACTIVE = 0 ,
62
+ ACTIVE = 1 ,
63
+ /* During the connection limits */
64
+ PAUSED = 2 ,
65
+ }
66
+
67
+ type Status = { code : TCPListenerStatusCode . INACTIVE } | {
68
+ code : Exclude < TCPListenerStatusCode , TCPListenerStatusCode . INACTIVE >
60
69
listeningAddr : Multiaddr
61
70
peerId : string | null
62
71
netConfig : NetConfig
@@ -66,7 +75,7 @@ export class TCPListener extends EventEmitter<ListenerEvents> implements Listene
66
75
private readonly server : net . Server
67
76
/** Keep track of open connections to destroy in case of timeout */
68
77
private readonly connections = new Set < MultiaddrConnection > ( )
69
- private status : Status = { started : false }
78
+ private status : Status = { code : TCPListenerStatusCode . INACTIVE }
70
79
private metrics ?: TCPListenerMetrics
71
80
private addr : string
72
81
@@ -88,7 +97,7 @@ export class TCPListener extends EventEmitter<ListenerEvents> implements Listene
88
97
if ( context . closeServerOnMaxConnections != null ) {
89
98
// Sanity check options
90
99
if ( context . closeServerOnMaxConnections . closeAbove < context . closeServerOnMaxConnections . listenBelow ) {
91
- throw Error ( 'closeAbove must be >= listenBelow' )
100
+ throw new CodeError ( 'closeAbove must be >= listenBelow' , 'ERROR_CONNECTION_LIMITS ')
92
101
}
93
102
}
94
103
@@ -133,7 +142,7 @@ export class TCPListener extends EventEmitter<ListenerEvents> implements Listene
133
142
}
134
143
135
144
this . metrics ?. status . update ( {
136
- [ this . addr ] : SERVER_STATUS_UP
145
+ [ this . addr ] : TCPListenerStatusCode . ACTIVE
137
146
} )
138
147
}
139
148
@@ -145,13 +154,22 @@ export class TCPListener extends EventEmitter<ListenerEvents> implements Listene
145
154
} )
146
155
. on ( 'close' , ( ) => {
147
156
this . metrics ?. status . update ( {
148
- [ this . addr ] : SERVER_STATUS_DOWN
157
+ [ this . addr ] : this . status . code
149
158
} )
150
- this . dispatchEvent ( new CustomEvent ( 'close' ) )
159
+
160
+ // If this event is emitted, the transport manager will remove the listener from it's cache
161
+ // in the meanwhile if the connections are dropped then listener will start listening again
162
+ // and the transport manager will not be able to close the server
163
+ if ( this . status . code !== TCPListenerStatusCode . PAUSED ) {
164
+ this . dispatchEvent ( new CustomEvent ( 'close' ) )
165
+ }
151
166
} )
152
167
}
153
168
154
169
private onSocket ( socket : net . Socket ) : void {
170
+ if ( this . status . code !== TCPListenerStatusCode . ACTIVE ) {
171
+ throw new CodeError ( 'Server is is not listening yet' , 'ERR_SERVER_NOT_RUNNING' )
172
+ }
155
173
// Avoid uncaught errors caused by unstable connections
156
174
socket . on ( 'error' , err => {
157
175
log ( 'socket error' , err )
@@ -161,7 +179,7 @@ export class TCPListener extends EventEmitter<ListenerEvents> implements Listene
161
179
let maConn : MultiaddrConnection
162
180
try {
163
181
maConn = toMultiaddrConnection ( socket , {
164
- listeningAddr : this . status . started ? this . status . listeningAddr : undefined ,
182
+ listeningAddr : this . status . listeningAddr ,
165
183
socketInactivityTimeout : this . context . socketInactivityTimeout ,
166
184
socketCloseTimeout : this . context . socketCloseTimeout ,
167
185
metrics : this . metrics ?. events ,
@@ -189,9 +207,9 @@ export class TCPListener extends EventEmitter<ListenerEvents> implements Listene
189
207
) {
190
208
// The most likely case of error is if the port taken by this application is binded by
191
209
// another process during the time the server if closed. In that case there's not much
192
- // we can do. netListen () will be called again every time a connection is dropped, which
210
+ // we can do. resume () will be called again every time a connection is dropped, which
193
211
// acts as an eventual retry mechanism. onListenError allows the consumer act on this.
194
- this . netListen ( ) . catch ( e => {
212
+ this . resume ( ) . catch ( e => {
195
213
log . error ( 'error attempting to listen server once connection count under limit' , e )
196
214
this . context . closeServerOnMaxConnections ?. onListenError ?.( e as Error )
197
215
} )
@@ -206,7 +224,9 @@ export class TCPListener extends EventEmitter<ListenerEvents> implements Listene
206
224
this . context . closeServerOnMaxConnections != null &&
207
225
this . connections . size >= this . context . closeServerOnMaxConnections . closeAbove
208
226
) {
209
- this . netClose ( )
227
+ this . pause ( false ) . catch ( e => {
228
+ log . error ( 'error attempting to close server once connection count over limit' , e )
229
+ } )
210
230
}
211
231
212
232
this . dispatchEvent ( new CustomEvent < Connection > ( 'connection' , { detail : conn } ) )
@@ -232,7 +252,7 @@ export class TCPListener extends EventEmitter<ListenerEvents> implements Listene
232
252
}
233
253
234
254
getAddrs ( ) : Multiaddr [ ] {
235
- if ( ! this . status . started ) {
255
+ if ( this . status . code === TCPListenerStatusCode . INACTIVE ) {
236
256
return [ ]
237
257
}
238
258
@@ -264,35 +284,44 @@ export class TCPListener extends EventEmitter<ListenerEvents> implements Listene
264
284
}
265
285
266
286
async listen ( ma : Multiaddr ) : Promise < void > {
267
- if ( this . status . started ) {
268
- throw Error ( 'server is already listening' )
287
+ if ( this . status . code === TCPListenerStatusCode . ACTIVE || this . status . code === TCPListenerStatusCode . PAUSED ) {
288
+ throw new CodeError ( 'server is already listening' , 'ERR_SERVER_ALREADY_LISTENING ')
269
289
}
270
290
271
291
const peerId = ma . getPeerId ( )
272
292
const listeningAddr = peerId == null ? ma . decapsulateCode ( CODE_P2P ) : ma
273
293
const { backlog } = this . context
274
294
275
- this . status = {
276
- started : true ,
277
- listeningAddr,
278
- peerId,
279
- netConfig : multiaddrToNetConfig ( listeningAddr , { backlog } )
280
- }
295
+ try {
296
+ this . status = {
297
+ code : TCPListenerStatusCode . ACTIVE ,
298
+ listeningAddr,
299
+ peerId,
300
+ netConfig : multiaddrToNetConfig ( listeningAddr , { backlog } )
301
+ }
281
302
282
- await this . netListen ( )
303
+ await this . resume ( )
304
+ } catch ( err ) {
305
+ this . status = { code : TCPListenerStatusCode . INACTIVE }
306
+ throw err
307
+ }
283
308
}
284
309
285
310
async close ( ) : Promise < void > {
286
- await Promise . all (
287
- Array . from ( this . connections . values ( ) ) . map ( async maConn => { await attemptClose ( maConn ) } )
288
- )
289
-
290
- // netClose already checks if server.listening
291
- this . netClose ( )
311
+ // Close connections and server the same time to avoid any race condition
312
+ await Promise . all ( [
313
+ Promise . all ( Array . from ( this . connections . values ( ) ) . map ( async maConn => attemptClose ( maConn ) ) ) ,
314
+ this . pause ( true ) . catch ( e => {
315
+ log . error ( 'error attempting to close server once connection count over limit' , e )
316
+ } )
317
+ ] )
292
318
}
293
319
294
- private async netListen ( ) : Promise < void > {
295
- if ( ! this . status . started || this . server . listening ) {
320
+ /**
321
+ * Can resume a stopped or start an inert server
322
+ */
323
+ private async resume ( ) : Promise < void > {
324
+ if ( this . server . listening || this . status . code === TCPListenerStatusCode . INACTIVE ) {
296
325
return
297
326
}
298
327
@@ -304,11 +333,17 @@ export class TCPListener extends EventEmitter<ListenerEvents> implements Listene
304
333
this . server . listen ( netConfig , resolve )
305
334
} )
306
335
336
+ this . status = { ...this . status , code : TCPListenerStatusCode . ACTIVE }
307
337
log ( 'Listening on %s' , this . server . address ( ) )
308
338
}
309
339
310
- private netClose ( ) : void {
311
- if ( ! this . status . started || ! this . server . listening ) {
340
+ private async pause ( permanent : boolean ) : Promise < void > {
341
+ if ( ! this . server . listening && this . status . code === TCPListenerStatusCode . PAUSED && permanent ) {
342
+ this . status = { code : TCPListenerStatusCode . INACTIVE }
343
+ return
344
+ }
345
+
346
+ if ( ! this . server . listening || this . status . code !== TCPListenerStatusCode . ACTIVE ) {
312
347
return
313
348
}
314
349
@@ -326,9 +361,12 @@ export class TCPListener extends EventEmitter<ListenerEvents> implements Listene
326
361
// Stops the server from accepting new connections and keeps existing connections.
327
362
// 'close' event is emitted only emitted when all connections are ended.
328
363
// The optional callback will be called once the 'close' event occurs.
329
- //
330
- // NOTE: Since we want to keep existing connections and have checked `!this.server.listening` it's not necessary
331
- // to pass a callback to close.
332
- this . server . close ( )
364
+
365
+ // We need to set this status before closing server, so other procedures are aware
366
+ // during the time the server is closing
367
+ this . status = permanent ? { code : TCPListenerStatusCode . INACTIVE } : { ...this . status , code : TCPListenerStatusCode . PAUSED }
368
+ await new Promise < void > ( ( resolve , reject ) => {
369
+ this . server . close ( err => { ( err != null ) ? reject ( err ) : resolve ( ) } )
370
+ } )
333
371
}
334
372
}
0 commit comments