1
- import { AbortError , InvalidParametersError , TimeoutError } from '@libp2p/interface'
1
+ import { InvalidParametersError , TimeoutError } from '@libp2p/interface'
2
2
import { ipPortToMultiaddr as toMultiaddr } from '@libp2p/utils/ip-port-to-multiaddr'
3
+ import pDefer from 'p-defer'
4
+ import { raceEvent } from 'race-event'
3
5
import { duplex } from 'stream-to-it'
4
6
import { CLOSE_TIMEOUT , SOCKET_TIMEOUT } from './constants.js'
5
7
import { multiaddrToNetConfig } from './utils.js'
6
8
import type { ComponentLogger , MultiaddrConnection , CounterGroup } from '@libp2p/interface'
7
9
import type { AbortOptions , Multiaddr } from '@multiformats/multiaddr'
8
10
import type { Socket } from 'net'
11
+ import type { DeferredPromise } from 'p-defer'
9
12
10
13
interface ToConnectionOptions {
11
14
listeningAddr ?: Multiaddr
@@ -16,19 +19,23 @@ interface ToConnectionOptions {
16
19
metrics ?: CounterGroup
17
20
metricPrefix ?: string
18
21
logger : ComponentLogger
22
+ direction : 'inbound' | 'outbound'
19
23
}
20
24
21
25
/**
22
26
* Convert a socket into a MultiaddrConnection
23
27
* https://github.com/libp2p/interface-transport#multiaddrconnection
24
28
*/
25
29
export const toMultiaddrConnection = ( socket : Socket , options : ToConnectionOptions ) : MultiaddrConnection => {
26
- let closePromise : Promise < void > | null = null
30
+ let closePromise : DeferredPromise < void >
27
31
const log = options . logger . forComponent ( 'libp2p:tcp:socket' )
32
+ const direction = options . direction
28
33
const metrics = options . metrics
29
34
const metricPrefix = options . metricPrefix ?? ''
30
35
const inactivityTimeout = options . socketInactivityTimeout ?? SOCKET_TIMEOUT
31
36
const closeTimeout = options . socketCloseTimeout ?? CLOSE_TIMEOUT
37
+ let timedout = false
38
+ let errored = false
32
39
33
40
// Check if we are connected on a unix path
34
41
if ( options . listeningAddr ?. getPath ( ) != null ) {
@@ -39,6 +46,19 @@ export const toMultiaddrConnection = (socket: Socket, options: ToConnectionOptio
39
46
options . localAddr = options . remoteAddr
40
47
}
41
48
49
+ // handle socket errors
50
+ socket . on ( 'error' , err => {
51
+ errored = true
52
+
53
+ if ( ! timedout ) {
54
+ log . error ( '%s socket error - %e' , direction , err )
55
+ metrics ?. increment ( { [ `${ metricPrefix } error` ] : true } )
56
+ }
57
+
58
+ socket . destroy ( )
59
+ maConn . timeline . close = Date . now ( )
60
+ } )
61
+
42
62
let remoteAddr : Multiaddr
43
63
44
64
if ( options . remoteAddr != null ) {
@@ -59,37 +79,37 @@ export const toMultiaddrConnection = (socket: Socket, options: ToConnectionOptio
59
79
60
80
// by default there is no timeout
61
81
// https://nodejs.org/dist/latest-v16.x/docs/api/net.html#socketsettimeouttimeout-callback
62
- socket . setTimeout ( inactivityTimeout , ( ) => {
63
- log ( '%s socket read timeout' , lOptsStr )
64
- metrics ?. increment ( { [ `${ metricPrefix } timeout` ] : true } )
82
+ socket . setTimeout ( inactivityTimeout )
65
83
66
- // only destroy with an error if the remote has not sent the FIN message
67
- let err : Error | undefined
68
- if ( socket . readable ) {
69
- err = new TimeoutError ( 'Socket read timeout' )
70
- }
84
+ socket . once ( 'timeout' , ( ) => {
85
+ timedout = true
86
+ log ( '%s %s socket read timeout' , direction , lOptsStr )
87
+ metrics ?. increment ( { [ `${ metricPrefix } timeout` ] : true } )
71
88
72
89
// if the socket times out due to inactivity we must manually close the connection
73
90
// https://nodejs.org/dist/latest-v16.x/docs/api/net.html#event-timeout
74
- socket . destroy ( err )
91
+ socket . destroy ( new TimeoutError ( ) )
92
+ maConn . timeline . close = Date . now ( )
75
93
} )
76
94
77
95
socket . once ( 'close' , ( ) => {
78
- log ( '%s socket close' , lOptsStr )
79
- metrics ?. increment ( { [ `${ metricPrefix } close` ] : true } )
96
+ // record metric for clean exit
97
+ if ( ! timedout && ! errored ) {
98
+ log ( '%s %s socket close' , direction , lOptsStr )
99
+ metrics ?. increment ( { [ `${ metricPrefix } close` ] : true } )
100
+ }
80
101
81
102
// In instances where `close` was not explicitly called,
82
103
// such as an iterable stream ending, ensure we have set the close
83
104
// timeline
84
- if ( maConn . timeline . close == null ) {
85
- maConn . timeline . close = Date . now ( )
86
- }
105
+ socket . destroy ( )
106
+ maConn . timeline . close = Date . now ( )
87
107
} )
88
108
89
109
socket . once ( 'end' , ( ) => {
90
110
// the remote sent a FIN packet which means no more data will be sent
91
111
// https://nodejs.org/dist/latest-v16.x/docs/api/net.html#event-end
92
- log ( '%s socket end' , lOptsStr )
112
+ log ( '%s %s socket end' , direction , lOptsStr )
93
113
metrics ?. increment ( { [ `${ metricPrefix } end` ] : true } )
94
114
} )
95
115
@@ -111,7 +131,7 @@ export const toMultiaddrConnection = (socket: Socket, options: ToConnectionOptio
111
131
// If the source errored the socket will already have been destroyed by
112
132
// duplex(). If the socket errored it will already be
113
133
// destroyed. There's nothing to do here except log the error & return.
114
- log . error ( '%s error in sink' , lOptsStr , err )
134
+ log . error ( '%s %s error in sink - %e' , direction , lOptsStr , err )
115
135
}
116
136
}
117
137
@@ -128,100 +148,84 @@ export const toMultiaddrConnection = (socket: Socket, options: ToConnectionOptio
128
148
129
149
async close ( options : AbortOptions = { } ) {
130
150
if ( socket . closed ) {
131
- log ( 'The %s socket is already closed' , lOptsStr )
151
+ log ( 'the %s %s socket is already closed' , direction , lOptsStr )
132
152
return
133
153
}
134
154
135
155
if ( socket . destroyed ) {
136
- log ( 'The %s socket is already destroyed' , lOptsStr )
156
+ log ( 'the %s %s socket is already destroyed' , direction , lOptsStr )
137
157
return
138
158
}
139
159
140
- const abortSignalListener = ( ) : void => {
141
- socket . destroy ( new AbortError ( 'Destroying socket after timeout' ) )
160
+ if ( closePromise != null ) {
161
+ return closePromise . promise
142
162
}
143
163
144
164
try {
145
- if ( closePromise != null ) {
146
- log ( 'The %s socket is already closing' , lOptsStr )
147
- await closePromise
148
- return
149
- }
165
+ closePromise = pDefer ( )
150
166
151
- if ( options . signal == null ) {
152
- const signal = AbortSignal . timeout ( closeTimeout )
167
+ // close writable end of socket
168
+ socket . end ( )
153
169
154
- options = {
155
- ...options ,
156
- signal
157
- }
158
- }
170
+ // convert EventEmitter to EventTarget
171
+ const eventTarget = socketToEventTarget ( socket )
159
172
160
- options . signal ?. addEventListener ( 'abort' , abortSignalListener )
173
+ // don't wait forever to close
174
+ const signal = options . signal ?? AbortSignal . timeout ( closeTimeout )
161
175
162
- log ( '%s closing socket' , lOptsStr )
163
- closePromise = new Promise < void > ( ( resolve , reject ) => {
164
- socket . once ( 'close' , ( ) => {
165
- // socket completely closed
166
- log ( '%s socket closed' , lOptsStr )
167
- resolve ( )
176
+ // wait for any unsent data to be sent
177
+ if ( socket . writableLength > 0 ) {
178
+ log ( '%s %s draining socket' , direction , lOptsStr )
179
+ await raceEvent ( eventTarget , 'drain' , signal , {
180
+ errorEvent : 'error'
168
181
} )
169
- socket . once ( 'error' , ( err : Error ) => {
170
- log ( '%s socket error' , lOptsStr , err )
171
-
172
- if ( ! socket . destroyed ) {
173
- reject ( err )
174
- }
175
- // if socket is destroyed, 'closed' event will be emitted later to resolve the promise
176
- } )
177
-
178
- // shorten inactivity timeout
179
- socket . setTimeout ( closeTimeout )
180
-
181
- // close writable end of the socket
182
- socket . end ( )
183
-
184
- if ( socket . writableLength > 0 ) {
185
- // there are outgoing bytes waiting to be sent
186
- socket . once ( 'drain' , ( ) => {
187
- log ( '%s socket drained' , lOptsStr )
182
+ log ( '%s %s socket drained' , direction , lOptsStr )
183
+ }
188
184
189
- // all bytes have been sent we can destroy the socket (maybe) before the timeout
190
- socket . destroy ( )
191
- } )
192
- } else {
193
- // nothing to send, destroy immediately, no need for the timeout
194
- socket . destroy ( )
195
- }
196
- } )
185
+ await Promise . all ( [
186
+ raceEvent ( eventTarget , 'close' , signal , {
187
+ errorEvent : 'error'
188
+ } ) ,
197
189
198
- await closePromise
190
+ // all bytes have been sent we can destroy the socket
191
+ socket . destroy ( )
192
+ ] )
199
193
} catch ( err : any ) {
200
194
this . abort ( err )
201
195
} finally {
202
- options . signal ?. removeEventListener ( 'abort' , abortSignalListener )
196
+ closePromise . resolve ( )
203
197
}
204
198
} ,
205
199
206
200
abort : ( err : Error ) => {
207
- log ( '%s socket abort due to error' , lOptsStr , err )
201
+ log ( '%s %s socket abort due to error - %e' , direction , lOptsStr , err )
208
202
209
203
// the abortSignalListener may already destroyed the socket with an error
210
- if ( ! socket . destroyed ) {
211
- socket . destroy ( err )
212
- }
204
+ socket . destroy ( )
213
205
214
206
// closing a socket is always asynchronous (must wait for "close" event)
215
207
// but the tests expect this to be a synchronous operation so we have to
216
208
// set the close time here. the tests should be refactored to reflect
217
209
// reality.
218
- if ( maConn . timeline . close == null ) {
219
- maConn . timeline . close = Date . now ( )
220
- }
210
+ maConn . timeline . close = Date . now ( )
221
211
} ,
222
212
223
213
log
224
214
}
225
215
226
216
return maConn
227
217
}
218
+
219
+ function socketToEventTarget ( obj ?: any ) : EventTarget {
220
+ const eventTarget = {
221
+ addEventListener : ( type : any , cb : any ) => {
222
+ obj . addListener ( type , cb )
223
+ } ,
224
+ removeEventListener : ( type : any , cb : any ) => {
225
+ obj . removeListener ( type , cb )
226
+ }
227
+ }
228
+
229
+ // @ts -expect-error partial implementation
230
+ return eventTarget
231
+ }
0 commit comments