Skip to content

Commit 7aa64a0

Browse files
committed
fix: fixes cache leak
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 unhandeled 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
1 parent eca8f31 commit 7aa64a0

File tree

6 files changed

+55
-18
lines changed

6 files changed

+55
-18
lines changed

packages/client/src/client.ts

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { ipnsValidator } from 'ipns/validator'
99
import { parse as ndjson } from 'it-ndjson'
1010
import defer from 'p-defer'
1111
import PQueue from 'p-queue'
12+
import { CACHE_NAME } from './constants.js'
1213
import { BadResponseError, InvalidRequestError } from './errors.js'
1314
import { DelegatedRoutingV1HttpApiClientContentRouting, DelegatedRoutingV1HttpApiClientPeerRouting } from './routings.js'
1415
import type { DelegatedRoutingV1HttpApiClient, DelegatedRoutingV1HttpApiClientInit, GetProvidersOptions, GetPeersOptions, GetIPNSOptions, PeerRecord } from './index.js'
@@ -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 ?? CACHE_NAME
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/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const CACHE_NAME = 'delegated-routing-v1-cache'

packages/client/src/index.ts

Lines changed: 9 additions & 0 deletions
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 {
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: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
/* eslint-env mocha */
22

33
import { generateKeyPair } from '@libp2p/crypto/keys'
4+
import { start } 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
@@ -357,6 +357,7 @@ describe('delegated-routing-v1-http-api-client', () => {
357357
const clientWithShortTTL = createDelegatedRoutingV1HttpApiClient(new URL(serverUrl), {
358358
cacheTTL: shortTTL
359359
})
360+
await start(clientWithShortTTL)
360361

361362
const cid = CID.parse('QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn')
362363
const providers = [{

0 commit comments

Comments
 (0)