Skip to content

Commit 1c54723

Browse files
authored
fix: fixes cache leak and unhandled promise rejection (#152)
Fixes some bugs in the new request cache: 1. When stopping the client we were supposed to remove the response cache, instead we were deleting an item from the cache 2. When stopping we were `void`ing the cache deletion - this can cause unhandled promise rejections so switches to `await`ing the promise instead 3. When starting the cache was being created asynchronously but without waiting for the promise to resolve, so you could stop the client before the cache was intialized which would mean it is never cleaned up Also not a bug but it also makes the cache name configurable.
1 parent 1e60306 commit 1c54723

File tree

7 files changed

+77
-37
lines changed

7 files changed

+77
-37
lines changed

packages/client/src/client.ts

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ const log = logger('delegated-routing-v1-http-api-client')
2121
const defaultValues = {
2222
concurrentRequests: 4,
2323
timeout: 30e3,
24-
cacheTTL: 5 * 60 * 1000 // 5 minutes default as per https://specs.ipfs.tech/routing/http-routing-v1/#response-headers
24+
cacheTTL: 5 * 60 * 1000, // 5 minutes default as per https://specs.ipfs.tech/routing/http-routing-v1/#response-headers
25+
cacheName: 'delegated-routing-v1-cache'
2526
}
2627

2728
export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV1HttpApiClient {
@@ -35,6 +36,7 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
3536
private readonly filterAddrs?: string[]
3637
private readonly filterProtocols?: string[]
3738
private readonly inFlightRequests: Map<string, Promise<Response>>
39+
private readonly cacheName: string
3840
private cache?: Cache
3941
private readonly cacheTTL: number
4042
/**
@@ -55,17 +57,8 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
5557
this.contentRouting = new DelegatedRoutingV1HttpApiClientContentRouting(this)
5658
this.peerRouting = new DelegatedRoutingV1HttpApiClientPeerRouting(this)
5759

60+
this.cacheName = init.cacheName ?? defaultValues.cacheName
5861
this.cacheTTL = init.cacheTTL ?? defaultValues.cacheTTL
59-
const cacheEnabled = (typeof globalThis.caches !== 'undefined') && (this.cacheTTL > 0)
60-
61-
if (cacheEnabled) {
62-
log('cache enabled with ttl %d', this.cacheTTL)
63-
globalThis.caches.open('delegated-routing-v1-cache').then(cache => {
64-
this.cache = cache
65-
}).catch(() => {
66-
this.cache = undefined
67-
})
68-
}
6962
}
7063

7164
get [contentRoutingSymbol] (): ContentRouting {
@@ -80,19 +73,30 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
8073
return this.started
8174
}
8275

83-
start (): void {
76+
async start (): Promise<void> {
77+
if (this.started) {
78+
return
79+
}
80+
8481
this.started = true
82+
83+
if (this.cacheTTL > 0) {
84+
this.cache = await globalThis.caches?.open(this.cacheName)
85+
86+
if (this.cache != null) {
87+
log('cache enabled with ttl %d', this.cacheTTL)
88+
}
89+
}
8590
}
8691

87-
stop (): void {
92+
async stop (): Promise<void> {
8893
this.httpQueue.clear()
8994
this.shutDownController.abort()
90-
this.started = false
9195

9296
// Clear the cache when stopping
93-
if (this.cache != null) {
94-
void this.cache.delete('delegated-routing-v1-cache')
95-
}
97+
await globalThis.caches?.delete(this.cacheName)
98+
99+
this.started = false
96100
}
97101

98102
async * getProviders (cid: CID, options: GetProvidersOptions = {}): AsyncGenerator<PeerRecord> {

packages/client/src/index.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,15 @@ export interface DelegatedRoutingV1HttpApiClientInit extends FilterOptions {
144144
* If 0, caching is disabled
145145
*/
146146
cacheTTL?: number
147+
148+
/**
149+
* Where a [Cache](https://developer.mozilla.org/en-US/docs/Web/API/Cache) is
150+
* available in the global scope, we will store request/responses to avoid
151+
* making duplicate requests.
152+
*
153+
* @default 'delegated-routing-v1-cache'
154+
*/
155+
cacheName?: string
147156
}
148157

149158
export interface GetIPNSOptions extends AbortOptions {
@@ -182,11 +191,17 @@ export interface DelegatedRoutingV1HttpApiClient {
182191
*/
183192
putIPNS(libp2pKey: CID<unknown, 0x72, 0x00 | 0x12, 1>, record: IPNSRecord, options?: AbortOptions): Promise<void>
184193

194+
/**
195+
* Create the request/response cache used to ensure duplicate requests aren't
196+
* made for the same data
197+
*/
198+
start(): Promise<void>
199+
185200
/**
186201
* Shut down any currently running HTTP requests and clear up any resources
187202
* that are in use
188203
*/
189-
stop(): void
204+
stop(): Promise<void>
190205
}
191206

192207
/**
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { expect } from 'aegir/chai'
2+
import { DefaultDelegatedRoutingV1HttpApiClient } from '../src/client.js'
3+
import { itBrowser } from './fixtures/it.js'
4+
5+
describe('client', () => {
6+
itBrowser('should remove cache on stop', async function () {
7+
const cacheName = 'test-cache'
8+
9+
const client = new DefaultDelegatedRoutingV1HttpApiClient('http://example.com', {
10+
cacheName
11+
})
12+
await client.start()
13+
await client.stop()
14+
15+
await expect(globalThis.caches.has(cacheName)).to.eventually.be.false('did not remove cache on stop')
16+
})
17+
})
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/* eslint-env mocha */
2+
3+
import { isBrowser } from 'wherearewe'
4+
5+
export const itBrowser = (isBrowser ? it : it.skip)

packages/client/test/index.spec.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,32 @@
11
/* eslint-env mocha */
22

33
import { generateKeyPair } from '@libp2p/crypto/keys'
4+
import { start, stop } from '@libp2p/interface'
45
import { peerIdFromPrivateKey, peerIdFromString } from '@libp2p/peer-id'
56
import { multiaddr } from '@multiformats/multiaddr'
67
import { expect } from 'aegir/chai'
78
import { createIPNSRecord, marshalIPNSRecord } from 'ipns'
89
import all from 'it-all'
910
import { CID } from 'multiformats/cid'
10-
import { isBrowser } from 'wherearewe'
1111
import { createDelegatedRoutingV1HttpApiClient, type DelegatedRoutingV1HttpApiClient } from '../src/index.js'
12+
import { itBrowser } from './fixtures/it.js'
1213

1314
if (process.env.ECHO_SERVER == null) {
1415
throw new Error('Echo server not configured correctly')
1516
}
1617

1718
const serverUrl = process.env.ECHO_SERVER
18-
const itBrowser = (isBrowser ? it : it.skip)
1919

2020
describe('delegated-routing-v1-http-api-client', () => {
2121
let client: DelegatedRoutingV1HttpApiClient
2222

23-
beforeEach(() => {
23+
beforeEach(async () => {
2424
client = createDelegatedRoutingV1HttpApiClient(new URL(serverUrl), { cacheTTL: 0 })
25+
await start(client)
2526
})
2627

2728
afterEach(async () => {
28-
if (client != null) {
29-
client.stop()
30-
}
29+
await stop(client)
3130
})
3231

3332
it('should find providers', async () => {
@@ -357,6 +356,7 @@ describe('delegated-routing-v1-http-api-client', () => {
357356
const clientWithShortTTL = createDelegatedRoutingV1HttpApiClient(new URL(serverUrl), {
358357
cacheTTL: shortTTL
359358
})
359+
await start(clientWithShortTTL)
360360

361361
const cid = CID.parse('QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn')
362362
const providers = [{
@@ -395,6 +395,6 @@ describe('delegated-routing-v1-http-api-client', () => {
395395
callCount = parseInt(await (await fetch(`${process.env.ECHO_SERVER}/get-call-count`)).text(), 10)
396396
expect(callCount).to.equal(2) // Second server call after cache expired
397397

398-
clientWithShortTTL.stop()
398+
await stop(clientWithShortTTL)
399399
})
400400
})

packages/client/test/routings.spec.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
/* eslint-env mocha */
33

44
import { generateKeyPair } from '@libp2p/crypto/keys'
5-
import { contentRoutingSymbol, peerRoutingSymbol } from '@libp2p/interface'
5+
import { contentRoutingSymbol, peerRoutingSymbol, start, stop } from '@libp2p/interface'
66
import { peerIdFromPrivateKey } from '@libp2p/peer-id'
77
import { expect } from 'aegir/chai'
88
import { createIPNSRecord, marshalIPNSRecord, multihashToIPNSRoutingKey } from 'ipns'
@@ -22,14 +22,13 @@ if (serverUrl == null) {
2222
describe('libp2p content-routing', () => {
2323
let client: DelegatedRoutingV1HttpApiClient
2424

25-
beforeEach(() => {
25+
beforeEach(async () => {
2626
client = createDelegatedRoutingV1HttpApiClient(new URL(serverUrl), { cacheTTL: 0 })
27+
await start(client)
2728
})
2829

2930
afterEach(async () => {
30-
if (client != null) {
31-
client.stop()
32-
}
31+
await stop(client)
3332

3433
const res = await fetch(`${process.env.ECHO_SERVER}/reset-call-count`)
3534
await res.text()
@@ -192,14 +191,13 @@ describe('libp2p content-routing', () => {
192191
describe('libp2p peer-routing', () => {
193192
let client: DelegatedRoutingV1HttpApiClient
194193

195-
beforeEach(() => {
194+
beforeEach(async () => {
196195
client = createDelegatedRoutingV1HttpApiClient(new URL(serverUrl))
196+
await start(client)
197197
})
198198

199199
afterEach(async () => {
200-
if (client != null) {
201-
client.stop()
202-
}
200+
await stop(client)
203201
})
204202

205203
describe('peer routing', () => {

packages/interop/test/index.spec.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { createDelegatedRoutingV1HttpApiClient } from '@helia/delegated-routing-
44
import { createDelegatedRoutingV1HttpApiServer } from '@helia/delegated-routing-v1-http-api-server'
55
import { ipns } from '@helia/ipns'
66
import { generateKeyPair } from '@libp2p/crypto/keys'
7+
import { start, stop } from '@libp2p/interface'
78
import { expect } from 'aegir/chai'
89
import { createIPNSRecord } from 'ipns'
910
import first from 'it-first'
@@ -43,12 +44,12 @@ describe('delegated-routing-v1-http-api interop', () => {
4344
await node.libp2p.dial(remote.libp2p.getMultiaddrs())
4445
}
4546
}
47+
48+
await start(client)
4649
})
4750

4851
afterEach(async () => {
49-
if (client != null) {
50-
client.stop()
51-
}
52+
await stop(client)
5253

5354
if (server != null) {
5455
await server.close()

0 commit comments

Comments
 (0)