@libp2p/tcp 10.0.5 → 10.0.6-24fa1d5af

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
 
@@ -127,92 +147,67 @@ export const toMultiaddrConnection = (socket: Socket, options: ToConnectionOptio
127
147
  timeline: { open: Date.now() },
128
148
 
129
149
  async close (options: AbortOptions = {}) {
130
- if (socket.destroyed) {
131
- log('The %s socket is destroyed', lOptsStr)
150
+ if (socket.closed) {
151
+ log('the %s %s socket is already closed', direction, lOptsStr)
132
152
  return
133
153
  }
134
154
 
135
- if (closePromise != null) {
136
- log('The %s socket is closed or closing', lOptsStr)
137
- return closePromise
138
- }
139
-
140
- if (options.signal == null) {
141
- const signal = AbortSignal.timeout(closeTimeout)
142
-
143
- options = {
144
- ...options,
145
- signal
146
- }
155
+ if (socket.destroyed) {
156
+ log('the %s %s socket is already destroyed', direction, lOptsStr)
157
+ return
147
158
  }
148
159
 
149
- const abortSignalListener = (): void => {
150
- socket.destroy(new AbortError('Destroying socket after timeout'))
160
+ if (closePromise != null) {
161
+ return closePromise.promise
151
162
  }
152
163
 
153
- options.signal?.addEventListener('abort', abortSignalListener)
154
-
155
164
  try {
156
- log('%s closing socket', lOptsStr)
157
- closePromise = new Promise<void>((resolve, reject) => {
158
- socket.once('close', () => {
159
- // socket completely closed
160
- log('%s socket closed', lOptsStr)
165
+ closePromise = pDefer()
161
166
 
162
- resolve()
163
- })
164
- socket.once('error', (err: Error) => {
165
- log('%s socket error', lOptsStr, err)
166
-
167
- // error closing socket
168
- if (maConn.timeline.close == null) {
169
- maConn.timeline.close = Date.now()
170
- }
171
- if (!socket.destroyed) {
172
- reject(err)
173
- }
174
- // if socket is destroyed, 'closed' event will be emitted later to resolve the promise
175
- })
167
+ // close writable end of socket
168
+ socket.end()
176
169
 
177
- // shorten inactivity timeout
178
- socket.setTimeout(closeTimeout)
170
+ // convert EventEmitter to EventTarget
171
+ const eventTarget = socketToEventTarget(socket)
179
172
 
180
- // close writable end of the socket
181
- socket.end()
173
+ // don't wait forever to close
174
+ const signal = options.signal ?? AbortSignal.timeout(closeTimeout)
182
175
 
183
- if (socket.writableLength > 0) {
184
- // there are outgoing bytes waiting to be sent
185
- socket.once('drain', () => {
186
- log('%s socket drained', lOptsStr)
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'
181
+ })
182
+ log('%s %s socket drained', direction, lOptsStr)
183
+ }
187
184
 
188
- // all bytes have been sent we can destroy the socket (maybe) before the timeout
189
- socket.destroy()
190
- })
191
- } else {
192
- // nothing to send, destroy immediately, no need for the timeout
193
- socket.destroy()
194
- }
195
- })
185
+ await Promise.all([
186
+ raceEvent(eventTarget, 'close', signal, {
187
+ errorEvent: 'error'
188
+ }),
196
189
 
197
- await closePromise
190
+ // all bytes have been sent we can destroy the socket
191
+ socket.destroy()
192
+ ])
198
193
  } catch (err: any) {
199
194
  this.abort(err)
200
195
  } finally {
201
- options.signal?.removeEventListener('abort', abortSignalListener)
196
+ closePromise.resolve()
202
197
  }
203
198
  },
204
199
 
205
200
  abort: (err: Error) => {
206
- log('%s socket abort due to error', lOptsStr, err)
201
+ log('%s %s socket abort due to error - %e', direction, lOptsStr, err)
207
202
 
208
203
  // the abortSignalListener may already destroyed the socket with an error
209
- if (!socket.destroyed) {
210
- socket.destroy(err)
211
- }
204
+ socket.destroy()
212
205
 
213
- if (maConn.timeline.close == null) {
214
- maConn.timeline.close = Date.now()
215
- }
206
+ // closing a socket is always asynchronous (must wait for "close" event)
207
+ // but the tests expect this to be a synchronous operation so we have to
208
+ // set the close time here. the tests should be refactored to reflect
209
+ // reality.
210
+ maConn.timeline.close = Date.now()
216
211
  },
217
212
 
218
213
  log
@@ -220,3 +215,17 @@ export const toMultiaddrConnection = (socket: Socket, options: ToConnectionOptio
220
215
 
221
216
  return maConn
222
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
  /**
@@ -1,19 +0,0 @@
1
- {
2
- "CloseServerOnMaxConnectionsOpts": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_tcp.CloseServerOnMaxConnectionsOpts.html",
3
- "TCPComponents": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_tcp.TCPComponents.html",
4
- ".:TCPComponents": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_tcp.TCPComponents.html",
5
- "TCPCreateListenerOptions": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_tcp.TCPCreateListenerOptions.html",
6
- ".:TCPCreateListenerOptions": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_tcp.TCPCreateListenerOptions.html",
7
- "TCPDialOptions": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_tcp.TCPDialOptions.html",
8
- ".:TCPDialOptions": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_tcp.TCPDialOptions.html",
9
- "TCPMetrics": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_tcp.TCPMetrics.html",
10
- ".:TCPMetrics": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_tcp.TCPMetrics.html",
11
- "TCPOptions": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_tcp.TCPOptions.html",
12
- ".:TCPOptions": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_tcp.TCPOptions.html",
13
- "TCPSocketOptions": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_tcp.TCPSocketOptions.html",
14
- ".:TCPSocketOptions": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_tcp.TCPSocketOptions.html",
15
- "TCPDialEvents": "https://libp2p.github.io/js-libp2p/types/_libp2p_tcp.TCPDialEvents.html",
16
- ".:TCPDialEvents": "https://libp2p.github.io/js-libp2p/types/_libp2p_tcp.TCPDialEvents.html",
17
- "tcp": "https://libp2p.github.io/js-libp2p/functions/_libp2p_tcp.tcp.html",
18
- ".:tcp": "https://libp2p.github.io/js-libp2p/functions/_libp2p_tcp.tcp.html"
19
- }