Skip to content

Commit 08dabd3

Browse files
authored
fix: close MultiaddrConnection once (#2478)
- It's designed to close MultiaddrConnection once, however in case there are some writable data remaining, socket is not destroyed yet https://github.com/libp2p/js-libp2p/blob/c5003d40207bc3be15645b5af93f8dec1fd5d55b/packages/transport-tcp/src/socket-to-conn.ts#L177 and when there is a called to `close()` again it'll run the function again - Instead use a variable to control it like other places
1 parent 7993470 commit 08dabd3

File tree

2 files changed

+71
-3
lines changed

2 files changed

+71
-3
lines changed

packages/transport-tcp/src/socket-to-conn.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ interface ToConnectionOptions {
2323
* https://github.com/libp2p/interface-transport#multiaddrconnection
2424
*/
2525
export const toMultiaddrConnection = (socket: Socket, options: ToConnectionOptions): MultiaddrConnection => {
26+
let status: 'open' | 'closing' | 'closed' = 'open'
2627
const log = options.logger.forComponent('libp2p:tcp:socket')
2728
const metrics = options.metrics
2829
const metricPrefix = options.metricPrefix ?? ''
@@ -126,10 +127,11 @@ export const toMultiaddrConnection = (socket: Socket, options: ToConnectionOptio
126127
timeline: { open: Date.now() },
127128

128129
async close (options: AbortOptions = {}) {
129-
if (socket.destroyed) {
130-
log('%s socket was already destroyed when trying to close', lOptsStr)
130+
if (status === 'closed' || status === 'closing' || socket.destroyed) {
131+
log('The %s socket is either closed, closing, or already destroyed', lOptsStr)
131132
return
132133
}
134+
status = 'closing'
133135

134136
if (options.signal == null) {
135137
const signal = AbortSignal.timeout(closeTimeout)
@@ -153,6 +155,7 @@ export const toMultiaddrConnection = (socket: Socket, options: ToConnectionOptio
153155
// socket completely closed
154156
log('%s socket closed', lOptsStr)
155157

158+
status = 'closed'
156159
resolve()
157160
})
158161
socket.once('error', (err: Error) => {
@@ -163,6 +166,7 @@ export const toMultiaddrConnection = (socket: Socket, options: ToConnectionOptio
163166
maConn.timeline.close = Date.now()
164167
}
165168

169+
status = 'closed'
166170
reject(err)
167171
})
168172

@@ -195,7 +199,10 @@ export const toMultiaddrConnection = (socket: Socket, options: ToConnectionOptio
195199
abort: (err: Error) => {
196200
log('%s socket abort due to error', lOptsStr, err)
197201

198-
socket.destroy(err)
202+
// the abortSignalListener may already destroyed the socket with an error
203+
if (!socket.destroyed) {
204+
socket.destroy(err)
205+
}
199206

200207
if (maConn.timeline.close == null) {
201208
maConn.timeline.close = Date.now()

packages/transport-tcp/test/socket-to-conn.spec.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import os from 'os'
33
import { defaultLogger } from '@libp2p/logger'
44
import { expect } from 'aegir/chai'
55
import defer from 'p-defer'
6+
import Sinon from 'sinon'
67
import { toMultiaddrConnection } from '../src/socket-to-conn.js'
78

89
async function setup (opts?: { server?: ServerOpts, client?: SocketConstructorOpts }): Promise<{ server: Server, serverSocket: Socket, clientSocket: Socket }> {
@@ -287,6 +288,66 @@ describe('socket-to-conn', () => {
287288
expect(serverSocket.destroyed).to.be.true()
288289
})
289290

291+
it('should not close MultiaddrConnection twice', async () => {
292+
({ server, clientSocket, serverSocket } = await setup())
293+
// proxyServerSocket.writableLength returns 100 which cause socket cannot be destroyed immediately
294+
const proxyServerSocket = new Proxy(serverSocket, {
295+
get (target, prop, receiver) {
296+
if (prop === 'writableLength') {
297+
return 100
298+
}
299+
return Reflect.get(target, prop, receiver)
300+
}
301+
})
302+
303+
// spy on `.destroy()` invocations
304+
const serverSocketDestroySpy = Sinon.spy(serverSocket, 'destroy')
305+
// promise that is resolved when our outgoing socket is closed
306+
const serverClosed = defer<boolean>()
307+
const socketCloseTimeout = 10
308+
309+
const inboundMaConn = toMultiaddrConnection(proxyServerSocket, {
310+
socketInactivityTimeout: 100,
311+
socketCloseTimeout,
312+
logger: defaultLogger()
313+
})
314+
expect(inboundMaConn.timeline.open).to.be.ok()
315+
expect(inboundMaConn.timeline.close).to.not.be.ok()
316+
317+
clientSocket.once('error', () => {})
318+
319+
serverSocket.once('close', () => {
320+
serverClosed.resolve(true)
321+
})
322+
323+
// send some data between the client and server
324+
clientSocket.write('hello')
325+
serverSocket.write('goodbye')
326+
327+
const signal = AbortSignal.timeout(socketCloseTimeout)
328+
const addEventListenerSpy = Sinon.spy(signal, 'addEventListener')
329+
330+
// the 2nd and 3rd call should return immediately
331+
await Promise.all([
332+
inboundMaConn.close({ signal }),
333+
inboundMaConn.close({ signal }),
334+
inboundMaConn.close({ signal })
335+
])
336+
337+
// server socket was closed for reading and writing
338+
await expect(serverClosed.promise).to.eventually.be.true()
339+
340+
// the connection closing was recorded
341+
expect(inboundMaConn.timeline.close).to.be.a('number')
342+
343+
// server socket is destroyed
344+
expect(serverSocket.destroyed).to.be.true()
345+
346+
// the server socket was only closed once
347+
expect(serverSocketDestroySpy.callCount).to.equal(1)
348+
expect(addEventListenerSpy.callCount).to.equal(1)
349+
})
350+
290351
it('should destroy a socket by timeout when containing MultiaddrConnection is closed', async () => {
291352
({ server, clientSocket, serverSocket } = await setup({
292353
server: {

0 commit comments

Comments
 (0)