Skip to content

Commit 6625a27

Browse files
authored
refactor: remove abortable iterator (#2237)
We use an `abortable-iterator` to race an abort signal against an incoming for streams and connections so we do not wait on slow connections forever. Unfortunately this introduces so much latency it becomes a performance bottleneck, and in all cases we can just add an `abort` listener to the incoming abort signal and close the underlying stream if it fires. Removing the latency added by the abortable iterator reveals that tests using it-pair, it-pushable and friends have many timing issues so it's necessary to ensure we are supplying data to streams as it is required. This opens up the possibility of having backpressure applied to protocol streams by the underlying transports which isn't currently possible.
1 parent 9c67c5b commit 6625a27

File tree

75 files changed

+1705
-1620
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

75 files changed

+1705
-1620
lines changed

packages/integration-tests/test/circuit-relay.node.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import all from 'it-all'
1616
import { pipe } from 'it-pipe'
1717
import { createLibp2p, type Libp2pOptions } from 'libp2p'
1818
import defer from 'p-defer'
19+
import pRetry from 'p-retry'
1920
import pWaitFor from 'p-wait-for'
2021
import sinon from 'sinon'
2122
import { Uint8ArrayList } from 'uint8arraylist'
@@ -502,13 +503,15 @@ describe('circuit-relay', () => {
502503
expect(conns).to.have.lengthOf(1)
503504
expect(conns).to.have.nested.property('[0].status', 'open')
504505

505-
// we should not have any streams with the hop codec
506-
const streams = local.getConnections(relay1.peerId)
507-
.map(conn => conn.streams)
508-
.flat()
509-
.filter(stream => stream.protocol === RELAY_V2_HOP_CODEC)
506+
await pRetry(() => {
507+
// we should not have any streams with the hop codec
508+
const streams = local.getConnections(relay1.peerId)
509+
.map(conn => conn.streams)
510+
.flat()
511+
.filter(stream => stream.protocol === RELAY_V2_HOP_CODEC)
510512

511-
expect(streams).to.be.empty()
513+
expect(streams).to.be.empty()
514+
})
512515
})
513516

514517
it('destination peer should stay connected to an already connected relay on hop failure', async () => {

packages/integration-tests/test/fetch.spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ async function createNode (): Promise<Libp2p<{ fetch: Fetch }>> {
1212
return createLibp2p(createBaseOptions({
1313
services: {
1414
fetch: fetch()
15+
},
16+
connectionManager: {
17+
minConnections: 0
1518
}
1619
}))
1720
}

packages/interface-compliance-tests/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,13 @@
110110
"@libp2p/peer-collections": "^4.0.8",
111111
"@libp2p/peer-id": "^3.0.6",
112112
"@libp2p/peer-id-factory": "^3.0.8",
113+
"@libp2p/utils": "^4.0.7",
113114
"@multiformats/multiaddr": "^12.1.10",
114115
"abortable-iterator": "^5.0.1",
115116
"aegir": "^41.0.2",
116117
"delay": "^6.0.0",
117118
"it-all": "^3.0.2",
118119
"it-drain": "^3.0.2",
119-
"it-handshake": "^4.1.3",
120120
"it-map": "^3.0.4",
121121
"it-ndjson": "^1.0.3",
122122
"it-pair": "^2.0.6",

packages/interface-compliance-tests/src/connection-encryption/utils/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { logger } from '@libp2p/logger'
12
import { multiaddr } from '@multiformats/multiaddr'
23
import { duplexPair } from 'it-pair/duplex'
34
import type { MultiaddrConnection } from '@libp2p/interface/connection'
@@ -14,7 +15,8 @@ export function createMaConnPair (): [MultiaddrConnection, MultiaddrConnection]
1415
remoteAddr: multiaddr('/ip4/127.0.0.1/tcp/4001'),
1516
timeline: {
1617
open: Date.now()
17-
}
18+
},
19+
log: logger('duplex-maconn')
1820
}
1921

2022
return output

packages/interface-compliance-tests/src/mocks/connection-encrypter.ts

Lines changed: 0 additions & 113 deletions
This file was deleted.

packages/interface-compliance-tests/src/mocks/connection.ts

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import { CodeError } from '@libp2p/interface/errors'
2-
import { defaultLogger } from '@libp2p/logger'
2+
import { defaultLogger, logger } from '@libp2p/logger'
33
import * as mss from '@libp2p/multistream-select'
44
import { peerIdFromString } from '@libp2p/peer-id'
5+
import { closeSource } from '@libp2p/utils/close-source'
56
import { duplexPair } from 'it-pair/duplex'
67
import { pipe } from 'it-pipe'
78
import { Uint8ArrayList } from 'uint8arraylist'
89
import { mockMultiaddrConnection } from './multiaddr-connection.js'
910
import { mockMuxer } from './muxer.js'
1011
import { mockRegistrar } from './registrar.js'
11-
import type { AbortOptions, ComponentLogger } from '@libp2p/interface'
12+
import type { AbortOptions, ComponentLogger, Logger } from '@libp2p/interface'
1213
import type { MultiaddrConnection, Connection, Stream, Direction, ConnectionTimeline, ConnectionStatus } from '@libp2p/interface/connection'
1314
import type { PeerId } from '@libp2p/interface/peer-id'
1415
import type { StreamMuxer, StreamMuxerFactory } from '@libp2p/interface/stream-muxer'
@@ -44,6 +45,7 @@ class MockConnection implements Connection {
4445
public streams: Stream[]
4546
public tags: string[]
4647
public transient: boolean
48+
public log: Logger
4749

4850
private readonly muxer: StreamMuxer
4951
private readonly maConn: MultiaddrConnection
@@ -67,6 +69,7 @@ class MockConnection implements Connection {
6769
this.maConn = maConn
6870
this.transient = false
6971
this.logger = logger
72+
this.log = logger.forComponent(this.id)
7073
}
7174

7275
async newStream (protocols: string | string[], options?: AbortOptions): Promise<Stream> {
@@ -139,7 +142,9 @@ export function mockConnection (maConn: MultiaddrConnection, opts: MockConnectio
139142
direction,
140143
onIncomingStream: (muxedStream) => {
141144
try {
142-
mss.handle(muxedStream, registrar.getProtocols())
145+
mss.handle(muxedStream, registrar.getProtocols(), {
146+
log
147+
})
143148
.then(({ stream, protocol }) => {
144149
log('%s: incoming stream opened on %s', direction, protocol)
145150
muxedStream.protocol = protocol
@@ -185,10 +190,12 @@ export interface StreamInit {
185190
}
186191

187192
export function mockStream (stream: Duplex<AsyncGenerator<Uint8ArrayList>, Source<Uint8ArrayList | Uint8Array>, Promise<void>>, init: StreamInit = {}): Stream {
188-
const originalSource = stream.source
193+
const id = `stream-${Date.now()}`
194+
const log = logger(`libp2p:mock-stream:${id}`)
189195

190196
// ensure stream output is `Uint8ArrayList` as it would be from an actual
191197
// Stream where everything is length-varint encoded
198+
const originalSource = stream.source
192199
stream.source = (async function * (): AsyncGenerator<Uint8ArrayList, any, unknown> {
193200
for await (const buf of originalSource) {
194201
if (buf instanceof Uint8Array) {
@@ -199,12 +206,44 @@ export function mockStream (stream: Duplex<AsyncGenerator<Uint8ArrayList>, Sourc
199206
}
200207
})()
201208

202-
return {
209+
const abortSinkController = new AbortController()
210+
const originalSink = stream.sink.bind(stream)
211+
stream.sink = async (source) => {
212+
abortSinkController.signal.addEventListener('abort', () => {
213+
closeSource(source, log)
214+
})
215+
216+
await originalSink(source)
217+
}
218+
219+
const mockStream: Stream = {
203220
...stream,
204-
close: async () => {},
205-
closeRead: async () => {},
206-
closeWrite: async () => {},
207-
abort: () => {},
221+
close: async (options) => {
222+
await mockStream.closeRead(options)
223+
await mockStream.closeWrite(options)
224+
},
225+
closeRead: async () => {
226+
closeSource(originalSource, log)
227+
mockStream.timeline.closeRead = Date.now()
228+
229+
if (mockStream.timeline.closeWrite != null) {
230+
mockStream.timeline.close = Date.now()
231+
}
232+
},
233+
closeWrite: async () => {
234+
abortSinkController.abort()
235+
mockStream.timeline.closeWrite = Date.now()
236+
237+
if (mockStream.timeline.closeRead != null) {
238+
mockStream.timeline.close = Date.now()
239+
}
240+
},
241+
abort: () => {
242+
closeSource(originalSource, log)
243+
mockStream.timeline.closeWrite = Date.now()
244+
mockStream.timeline.closeRead = Date.now()
245+
mockStream.timeline.close = Date.now()
246+
},
208247
direction: 'outbound',
209248
protocol: '/foo/1.0.0',
210249
timeline: {
@@ -215,8 +254,11 @@ export function mockStream (stream: Duplex<AsyncGenerator<Uint8ArrayList>, Sourc
215254
status: 'open',
216255
readStatus: 'ready',
217256
writeStatus: 'ready',
257+
log: logger('mock-stream'),
218258
...init
219259
}
260+
261+
return mockStream
220262
}
221263

222264
export interface StreamPairInit {

packages/interface-compliance-tests/src/mocks/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
export { mockConnectionEncrypter } from './connection-encrypter.js'
21
export { mockConnectionGater } from './connection-gater.js'
32
export { mockConnectionManager, mockNetwork } from './connection-manager.js'
43
export { mockConnection, mockStream, streamPair, connectionPair } from './connection.js'

packages/interface-compliance-tests/src/mocks/multiaddr-connection.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { logger } from '@libp2p/logger'
12
import { multiaddr } from '@multiformats/multiaddr'
23
import { abortableSource } from 'abortable-iterator'
34
import { duplexPair } from 'it-pair/duplex'
@@ -16,6 +17,7 @@ export function mockMultiaddrConnection (source: Duplex<AsyncGenerator<Uint8Arra
1617
open: Date.now()
1718
},
1819
remoteAddr: multiaddr(`/ip4/127.0.0.1/tcp/4001/p2p/${peerId.toString()}`),
20+
log: logger('mock-maconn'),
1921
...source
2022
}
2123

@@ -49,7 +51,8 @@ export function mockMultiaddrConnPair (opts: MockMultiaddrConnPairOptions): { in
4951
abort: (err: Error) => {
5052
outbound.timeline.close = Date.now()
5153
controller.abort(err)
52-
}
54+
},
55+
log: logger('mock-maconn-outbound')
5356
}
5457

5558
const inbound: MultiaddrConnection = {
@@ -65,7 +68,8 @@ export function mockMultiaddrConnPair (opts: MockMultiaddrConnPairOptions): { in
6568
abort: (err: Error) => {
6669
outbound.timeline.close = Date.now()
6770
controller.abort(err)
68-
}
71+
},
72+
log: logger('mock-maconn-inbound')
6973
}
7074

7175
// Make the sources abortable so we can close them easily

packages/interface-compliance-tests/src/mocks/muxer.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { AbstractStream, type AbstractStreamInit } from '@libp2p/interface/stream-muxer/stream'
21
import { type Logger, logger } from '@libp2p/logger'
2+
import { AbstractStream, type AbstractStreamInit } from '@libp2p/utils/abstract-stream'
33
import { abortableSource } from 'abortable-iterator'
44
import map from 'it-map'
55
import * as ndjson from 'it-ndjson'
@@ -249,14 +249,27 @@ class MockMuxer implements StreamMuxer {
249249
return
250250
}
251251

252-
this.log('closing muxed streams')
252+
const signal = options?.signal ?? AbortSignal.timeout(10)
253253

254-
await Promise.all(
255-
this.streams.map(async s => s.close())
256-
)
254+
try {
255+
// try to gracefully close all streams
256+
await Promise.all(
257+
this.streams.map(async s => s.close({
258+
signal
259+
}))
260+
)
261+
262+
this.input.end()
257263

258-
this.closeController.abort()
259-
this.input.end()
264+
// try to gracefully close the muxer
265+
await this.input.onEmpty({
266+
signal
267+
})
268+
269+
this.closeController.abort()
270+
} catch (err: any) {
271+
this.abort(err)
272+
}
260273
}
261274

262275
abort (err: Error): void {

0 commit comments

Comments
 (0)