fix: time out slow reads (#1227)

There are a few places in the codebase where we send/receive data from the network without timeouts/abort controllers which means the user has to wait for the underlying socket to timeout which can take a long time depending on the platform, if at all.

This change ensures we can time out while running identify (both flavours), ping and fetch and adds tests to ensure there are no regressions.
This commit is contained in:
Alex Potsides
2022-05-25 18:15:21 +01:00
committed by GitHub
parent 5934b13cce
commit a1220d22f5
23 changed files with 1039 additions and 442 deletions

122
test/ping/index.spec.ts Normal file
View File

@@ -0,0 +1,122 @@
/* eslint-env mocha */
import { expect } from 'aegir/chai'
import sinon from 'sinon'
import { PingService } from '../../src/ping/index.js'
import Peers from '../fixtures/peers.js'
import { mockRegistrar, mockUpgrader, connectionPair } from '@libp2p/interface-compliance-tests/mocks'
import { createFromJSON } from '@libp2p/peer-id-factory'
import { Components } from '@libp2p/interfaces/components'
import { DefaultConnectionManager } from '../../src/connection-manager/index.js'
import { start, stop } from '@libp2p/interfaces/startable'
import { CustomEvent } from '@libp2p/interfaces/events'
import { TimeoutController } from 'timeout-abort-controller'
import delay from 'delay'
import { pipe } from 'it-pipe'
const defaultInit = {
protocolPrefix: 'ipfs'
}
async function createComponents (index: number) {
const peerId = await createFromJSON(Peers[index])
const components = new Components({
peerId,
registrar: mockRegistrar(),
upgrader: mockUpgrader(),
connectionManager: new DefaultConnectionManager({
minConnections: 50,
maxConnections: 1000,
autoDialInterval: 1000
})
})
return components
}
describe('ping', () => {
let localComponents: Components
let remoteComponents: Components
beforeEach(async () => {
localComponents = await createComponents(0)
remoteComponents = await createComponents(1)
await Promise.all([
start(localComponents),
start(remoteComponents)
])
})
afterEach(async () => {
sinon.restore()
await Promise.all([
stop(localComponents),
stop(remoteComponents)
])
})
it('should be able to ping another peer', async () => {
const localPing = new PingService(localComponents, defaultInit)
const remotePing = new PingService(remoteComponents, defaultInit)
await start(localPing)
await start(remotePing)
// simulate connection between nodes
const [localToRemote, remoteToLocal] = connectionPair(localComponents, remoteComponents)
localComponents.getUpgrader().dispatchEvent(new CustomEvent('connection', { detail: localToRemote }))
remoteComponents.getUpgrader().dispatchEvent(new CustomEvent('connection', { detail: remoteToLocal }))
// Run ping
await expect(localPing.ping(remoteComponents.getPeerId())).to.eventually.be.gte(0)
})
it('should time out pinging another peer when waiting for a pong', async () => {
const localPing = new PingService(localComponents, defaultInit)
const remotePing = new PingService(remoteComponents, defaultInit)
await start(localPing)
await start(remotePing)
// simulate connection between nodes
const [localToRemote, remoteToLocal] = connectionPair(localComponents, remoteComponents)
localComponents.getUpgrader().dispatchEvent(new CustomEvent('connection', { detail: localToRemote }))
remoteComponents.getUpgrader().dispatchEvent(new CustomEvent('connection', { detail: remoteToLocal }))
// replace existing handler with a really slow one
await remoteComponents.getRegistrar().unhandle(remotePing.protocol)
await remoteComponents.getRegistrar().handle(remotePing.protocol, ({ stream }) => {
void pipe(
stream,
async function * (source) {
for await (const chunk of source) {
// longer than the timeout
await delay(1000)
yield chunk
}
},
stream
)
})
const newStreamSpy = sinon.spy(localToRemote, 'newStream')
// 10 ms timeout
const timeoutController = new TimeoutController(10)
// Run ping, should time out
await expect(localPing.ping(remoteComponents.getPeerId(), {
signal: timeoutController.signal
}))
.to.eventually.be.rejected.with.property('code', 'ABORT_ERR')
// should have closed stream
expect(newStreamSpy).to.have.property('callCount', 1)
const { stream } = await newStreamSpy.getCall(0).returnValue
expect(stream).to.have.nested.property('timeline.close')
})
})

75
test/ping/ping.node.ts Normal file
View File

@@ -0,0 +1,75 @@
/* eslint-env mocha */
import { expect } from 'aegir/chai'
import pTimes from 'p-times'
import { pipe } from 'it-pipe'
import { createNode, populateAddressBooks } from '../utils/creators/peer.js'
import { createBaseOptions } from '../utils/base-options.js'
import { PROTOCOL } from '../../src/ping/constants.js'
import { Multiaddr } from '@multiformats/multiaddr'
import pDefer from 'p-defer'
import type { Libp2pNode } from '../../src/libp2p.js'
describe('ping', () => {
let nodes: Libp2pNode[]
beforeEach(async () => {
nodes = await Promise.all([
createNode({ config: createBaseOptions() }),
createNode({ config: createBaseOptions() }),
createNode({ config: createBaseOptions() })
])
await populateAddressBooks(nodes)
await nodes[0].components.getPeerStore().addressBook.set(nodes[1].peerId, nodes[1].getMultiaddrs())
await nodes[1].components.getPeerStore().addressBook.set(nodes[0].peerId, nodes[0].getMultiaddrs())
})
afterEach(async () => await Promise.all(nodes.map(async n => await n.stop())))
it('ping once from peer0 to peer1 using a multiaddr', async () => {
const ma = new Multiaddr(`${nodes[2].getMultiaddrs()[0].toString()}/p2p/${nodes[2].peerId.toString()}`)
const latency = await nodes[0].ping(ma)
expect(latency).to.be.a('Number')
})
it('ping once from peer0 to peer1 using a peerId', async () => {
const latency = await nodes[0].ping(nodes[1].peerId)
expect(latency).to.be.a('Number')
})
it('ping several times for getting an average', async () => {
const latencies = await pTimes(5, async () => await nodes[1].ping(nodes[0].peerId))
const averageLatency = latencies.reduce((p, c) => p + c, 0) / latencies.length
expect(averageLatency).to.be.a('Number')
})
it('only waits for the first response to arrive', async () => {
const defer = pDefer()
await nodes[1].unhandle(PROTOCOL)
await nodes[1].handle(PROTOCOL, ({ stream }) => {
void pipe(
stream,
async function * (stream) {
for await (const data of stream) {
yield data
// something longer than the test timeout
await defer.promise
}
},
stream
)
})
const latency = await nodes[0].ping(nodes[1].peerId)
expect(latency).to.be.a('Number')
defer.resolve()
})
})