@libp2p/tcp 10.0.6 → 10.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,14 @@
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,6 +19,7 @@ interface ToConnectionOptions {
16
19
  metrics?: CounterGroup
17
20
  metricPrefix?: string
18
21
  logger: ComponentLogger
22
+ direction: 'inbound' | 'outbound'
19
23
  }
20
24
 
21
25
  /**
@@ -23,12 +27,15 @@ interface ToConnectionOptions {
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,96 +148,66 @@ 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
@@ -225,3 +215,17 @@ export const toMultiaddrConnection = (socket: Socket, options: ToConnectionOptio
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
+ }
package/src/tcp.ts CHANGED
@@ -36,7 +36,7 @@ import { TCPListener } from './listener.js'
36
36
  import { toMultiaddrConnection } from './socket-to-conn.js'
37
37
  import { multiaddrToNetConfig } from './utils.js'
38
38
  import type { TCPComponents, TCPCreateListenerOptions, TCPDialEvents, TCPDialOptions, TCPMetrics, TCPOptions } from './index.js'
39
- import type { Logger, Connection, Transport, Listener } from '@libp2p/interface'
39
+ import type { Logger, Connection, Transport, Listener, MultiaddrConnection } from '@libp2p/interface'
40
40
  import type { Multiaddr } from '@multiformats/multiaddr'
41
41
  import type { Socket, IpcSocketConnectOpts, TcpSocketConnectOpts } from 'net'
42
42
 
@@ -53,7 +53,11 @@ export class TCP implements Transport<TCPDialEvents> {
53
53
 
54
54
  if (components.metrics != null) {
55
55
  this.metrics = {
56
- dialerEvents: components.metrics.registerCounterGroup('libp2p_tcp_dialer_events_total', {
56
+ events: components.metrics.registerCounterGroup('libp2p_tcp_dialer_events_total', {
57
+ label: 'event',
58
+ help: 'Total count of TCP dialer events by type'
59
+ }),
60
+ errors: components.metrics.registerCounterGroup('libp2p_tcp_dialer_errors_total', {
57
61
  label: 'event',
58
62
  help: 'Total count of TCP dialer events by type'
59
63
  })
@@ -76,23 +80,28 @@ export class TCP implements Transport<TCPDialEvents> {
76
80
  // options.signal destroys the socket before 'connect' event
77
81
  const socket = await this._connect(ma, options)
78
82
 
79
- // Avoid uncaught errors caused by unstable connections
80
- socket.on('error', err => {
81
- this.log('socket error', err)
82
- })
83
+ let maConn: MultiaddrConnection
83
84
 
84
- const maConn = toMultiaddrConnection(socket, {
85
- remoteAddr: ma,
86
- socketInactivityTimeout: this.opts.outboundSocketInactivityTimeout,
87
- socketCloseTimeout: this.opts.socketCloseTimeout,
88
- metrics: this.metrics?.dialerEvents,
89
- logger: this.components.logger
90
- })
85
+ try {
86
+ maConn = toMultiaddrConnection(socket, {
87
+ remoteAddr: ma,
88
+ socketInactivityTimeout: this.opts.outboundSocketInactivityTimeout,
89
+ socketCloseTimeout: this.opts.socketCloseTimeout,
90
+ metrics: this.metrics?.events,
91
+ logger: this.components.logger,
92
+ direction: 'outbound'
93
+ })
94
+ } catch (err: any) {
95
+ this.metrics?.errors.increment({ outbound_to_connection: true })
96
+ socket.destroy(err)
97
+ throw err
98
+ }
91
99
 
92
100
  try {
93
101
  this.log('new outbound connection %s', maConn.remoteAddr)
94
102
  return await options.upgrader.upgradeOutbound(maConn, options)
95
103
  } catch (err: any) {
104
+ this.metrics?.errors.increment({ outbound_upgrade: true })
96
105
  this.log.error('error upgrading outbound connection', err)
97
106
  maConn.abort(err)
98
107
  throw err
@@ -103,6 +112,8 @@ export class TCP implements Transport<TCPDialEvents> {
103
112
  options.signal?.throwIfAborted()
104
113
  options.onProgress?.(new CustomProgressEvent('tcp:open-connection'))
105
114
 
115
+ let rawSocket: Socket
116
+
106
117
  return new Promise<Socket>((resolve, reject) => {
107
118
  const start = Date.now()
108
119
  const cOpts = multiaddrToNetConfig(ma, {
@@ -111,35 +122,34 @@ export class TCP implements Transport<TCPDialEvents> {
111
122
  }) as (IpcSocketConnectOpts & TcpSocketConnectOpts)
112
123
 
113
124
  this.log('dialing %a', ma)
114
- const rawSocket = net.connect(cOpts)
125
+ rawSocket = net.connect(cOpts)
115
126
 
116
127
  const onError = (err: Error): void => {
128
+ this.log.error('dial to %a errored - %e', ma, err)
117
129
  const cOptsStr = cOpts.path ?? `${cOpts.host ?? ''}:${cOpts.port}`
118
130
  err.message = `connection error ${cOptsStr}: ${err.message}`
119
- this.metrics?.dialerEvents.increment({ error: true })
120
-
131
+ this.metrics?.events.increment({ error: true })
121
132
  done(err)
122
133
  }
123
134
 
124
135
  const onTimeout = (): void => {
125
136
  this.log('connection timeout %a', ma)
126
- this.metrics?.dialerEvents.increment({ timeout: true })
137
+ this.metrics?.events.increment({ timeout: true })
127
138
 
128
- const err = new TimeoutError(`connection timeout after ${Date.now() - start}ms`)
139
+ const err = new TimeoutError(`Connection timeout after ${Date.now() - start}ms`)
129
140
  // Note: this will result in onError() being called
130
141
  rawSocket.emit('error', err)
131
142
  }
132
143
 
133
144
  const onConnect = (): void => {
134
145
  this.log('connection opened %a', ma)
135
- this.metrics?.dialerEvents.increment({ connect: true })
146
+ this.metrics?.events.increment({ connect: true })
136
147
  done()
137
148
  }
138
149
 
139
150
  const onAbort = (): void => {
140
151
  this.log('connection aborted %a', ma)
141
- this.metrics?.dialerEvents.increment({ abort: true })
142
- rawSocket.destroy()
152
+ this.metrics?.events.increment({ abort: true })
143
153
  done(new AbortError())
144
154
  }
145
155
 
@@ -167,6 +177,10 @@ export class TCP implements Transport<TCPDialEvents> {
167
177
  options.signal.addEventListener('abort', onAbort)
168
178
  }
169
179
  })
180
+ .catch(err => {
181
+ rawSocket?.destroy()
182
+ throw err
183
+ })
170
184
  }
171
185
 
172
186
  /**