From a10c86b5bee9e045de3f722febac007925c421b1 Mon Sep 17 00:00:00 2001 From: Filippo Vecchiato Date: Thu, 10 Oct 2024 17:16:47 +0200 Subject: [PATCH 01/11] lru ttl cache implementation --- packages/api-derive/src/util/cache.ts | 4 +- packages/rpc-core/src/bundle.ts | 19 +-- packages/rpc-core/src/lru.spec.ts | 57 +++++++ packages/rpc-core/src/lru.ts | 188 ++++++++++++++++++++++++ packages/rpc-provider/src/http/index.ts | 15 +- packages/rpc-provider/src/lru.ts | 60 +++++++- packages/rpc-provider/src/ws/index.ts | 6 + 7 files changed, 332 insertions(+), 17 deletions(-) create mode 100644 packages/rpc-core/src/lru.spec.ts create mode 100644 packages/rpc-core/src/lru.ts diff --git a/packages/api-derive/src/util/cache.ts b/packages/api-derive/src/util/cache.ts index c4c48cfbe6ac..3bab881ebf4b 100644 --- a/packages/api-derive/src/util/cache.ts +++ b/packages/api-derive/src/util/cache.ts @@ -10,7 +10,7 @@ interface CacheValue { x: number; } -const CHACHE_EXPIRY = 7 * (24 * 60) * (60 * 1000); +const CACHE_EXPIRY = 7 * (24 * 60) * (60 * 1000); let deriveCache: DeriveCache; @@ -43,7 +43,7 @@ function clearCache (cache: DeriveCache): void { const all: string[] = []; cache.forEach((key: string, { x }: CacheValue): void => { - ((now - x) > CHACHE_EXPIRY) && all.push(key); + ((now - x) > CACHE_EXPIRY) && all.push(key); }); // don't do delete inside loop, just in-case diff --git a/packages/rpc-core/src/bundle.ts b/packages/rpc-core/src/bundle.ts index 622ce98179df..815121f59b4e 100644 --- a/packages/rpc-core/src/bundle.ts +++ b/packages/rpc-core/src/bundle.ts @@ -15,6 +15,7 @@ import { rpcDefinitions } from '@polkadot/types'; import { hexToU8a, isFunction, isNull, isUndefined, lazyMethod, logger, memoize, objectSpread, u8aConcat, u8aToU8a } from '@polkadot/util'; import { drr, refCountDelay } from './util/index.js'; +import { DEFAULT_CAPACITY, LRUCache } from './lru.js'; export { packageInfo } from './packageInfo.js'; export * from './util/index.js'; @@ -93,7 +94,7 @@ export class RpcCore { readonly #instanceId: string; readonly #isPedantic: boolean; readonly #registryDefault: Registry; - readonly #storageCache = new Map(); + readonly #storageCache: LRUCache; #storageCacheHits = 0; #storageCacheSize = 0; @@ -123,7 +124,7 @@ export class RpcCore { // these are the base keys (i.e. part of jsonrpc) this.sections.push(...sectionNames); - + this.#storageCache = new LRUCache(DEFAULT_CAPACITY); // decorate all interfaces, defined and user on this instance this.addUserInterfaces(userRpc); } @@ -469,7 +470,7 @@ export class RpcCore { // - if a single result value, don't fill - it is not an update hole // - fallback to an empty option in all cases if (isNotFound && withCache) { - const cached = this.#storageCache.get(hexKey); + const cached = this.#storageCache.get(hexKey) as Codec | undefined; if (cached) { this.#storageCacheHits++; @@ -486,16 +487,16 @@ export class RpcCore { ? value : u8aToU8a(value); const codec = this._newType(registry, blockHash, key, input, isEmpty, entryIndex); - - // store the retrieved result - the only issue with this cache is that there is no - // clearing of it, so very long running processes (not just a couple of hours, longer) - // will increase memory beyond what is allowed. - this.#storageCache.set(hexKey, codec); - this.#storageCacheSize++; + + this._setToCache(hexKey, codec); return codec; } + private _setToCache (key: string, value: Codec): void { + this.#storageCache.set(key, value); + } + private _newType (registry: Registry, blockHash: Uint8Array | string | null | undefined, key: StorageKey, input: string | Uint8Array | null, isEmpty: boolean, entryIndex = -1): Codec { // single return value (via state.getStorage), decode the value based on the // outputType that we have specified. Fallback to Raw on nothing diff --git a/packages/rpc-core/src/lru.spec.ts b/packages/rpc-core/src/lru.spec.ts new file mode 100644 index 000000000000..0079eba6c300 --- /dev/null +++ b/packages/rpc-core/src/lru.spec.ts @@ -0,0 +1,57 @@ +// Copyright 2017-2024 @polkadot/rpc-provider authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +/// + +import { LRUCache } from './lru.js'; + +describe('LRUCache', (): void => { + it('allows getting of items below capacity', (): void => { + const keys = ['1', '2', '3', '4']; + const lru = new LRUCache(4); + + keys.forEach((k) => lru.set(k, `${k}${k}${k}`)); + + expect(lru.keys().join(', ')).toEqual(keys.reverse().join(', ')); + expect(lru.length === lru.lengthData && lru.length === lru.lengthRefs).toBe(true); + + keys.forEach((k) => expect(lru.get(k)).toEqual(`${k}${k}${k}`)); + }); + + it('drops items when at capacity', (): void => { + const keys = ['1', '2', '3', '4', '5', '6']; + const lru = new LRUCache(4); + + keys.forEach((k) => lru.set(k, `${k}${k}${k}`)); + + expect(lru.keys().join(', ')).toEqual(keys.slice(2).reverse().join(', ')); + expect(lru.length === lru.lengthData && lru.length === lru.lengthRefs).toBe(true); + + keys.slice(2).forEach((k) => expect(lru.get(k)).toEqual(`${k}${k}${k}`)); + }); + + it('adjusts the order as they are used', (): void => { + const keys = ['1', '2', '3', '4', '5']; + const lru = new LRUCache(4); + + keys.forEach((k) => lru.set(k, `${k}${k}${k}`)); + + expect(lru.entries()).toEqual([['5', '555'], ['4', '444'], ['3', '333'], ['2', '222']]); + expect(lru.length === lru.lengthData && lru.length === lru.lengthRefs).toBe(true); + + lru.get('3'); + + expect(lru.entries()).toEqual([['3', '333'], ['5', '555'], ['4', '444'], ['2', '222']]); + expect(lru.length === lru.lengthData && lru.length === lru.lengthRefs).toBe(true); + + lru.set('4', '4433'); + + expect(lru.entries()).toEqual([['4', '4433'], ['3', '333'], ['5', '555'], ['2', '222']]); + expect(lru.length === lru.lengthData && lru.length === lru.lengthRefs).toBe(true); + + lru.set('6', '666'); + + expect(lru.entries()).toEqual([['6', '666'], ['4', '4433'], ['3', '333'], ['5', '555']]); + expect(lru.length === lru.lengthData && lru.length === lru.lengthRefs).toBe(true); + }); +}); diff --git a/packages/rpc-core/src/lru.ts b/packages/rpc-core/src/lru.ts new file mode 100644 index 000000000000..e488a1fe5421 --- /dev/null +++ b/packages/rpc-core/src/lru.ts @@ -0,0 +1,188 @@ +// Copyright 2017-2024 @polkadot/rpc-provider authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +// Assuming all 1.5MB responses, we apply a default allowing for 192MB +// cache space (depending on the historic queries this would vary, metadata +// for Kusama/Polkadot/Substrate falls between 600-750K, 2x for estimate) +export const DEFAULT_CAPACITY = 64; + +class LRUNode { + readonly key: string; + #lastAccess: number; + readonly createdAt: number; + + public next: LRUNode; + public prev: LRUNode; + + constructor (key: string) { + this.key = key; + this.#lastAccess = Date.now(); + this.createdAt = this.#lastAccess; + this.next = this.prev = this; + } + + public refresh(): void { + this.#lastAccess = Date.now(); + } + + public get lastAccess(): number { + return this.#lastAccess; + } + +} + +// https://en.wikipedia.org/wiki/Cache_replacement_policies#LRU +export class LRUCache { + readonly capacity: number; + + readonly #data = new Map(); + readonly #refs = new Map(); + + #length = 0; + #head: LRUNode; + #tail: LRUNode; + // TTL + readonly #ttl: number; + readonly #ttlInterval: number; + #ttlP?: NodeJS.Timeout = undefined; + + constructor (capacity = DEFAULT_CAPACITY, ttl = 30000, ttlInterval = 15000) { + this.capacity = capacity; + this.#ttl = ttl; + this.#ttlInterval = ttlInterval; + this.#head = this.#tail = new LRUNode(''); + // make sure the interval is not longer than the ttl + if (this.#ttlInterval > this.#ttl) this.#ttlInterval = this.#ttl; + } + + get ttl (): number { + return this.#ttl; + } + + get ttlInterval (): number { + return this.#ttlInterval; + } + + get length (): number { + return this.#length; + } + + get lengthData (): number { + return this.#data.size; + } + + get lengthRefs (): number { + return this.#refs.size; + } + + entries (): [string, unknown][] { + const keys = this.keys(); + const count = keys.length; + const entries = new Array<[string, unknown]>(count); + + for (let i = 0; i < count; i++) { + const key = keys[i]; + + entries[i] = [key, this.#data.get(key)]; + } + + return entries; + } + + keys (): string[] { + const keys: string[] = []; + + if (this.#length) { + let curr = this.#head; + + while (curr !== this.#tail) { + keys.push(curr.key); + curr = curr.next; + } + + keys.push(curr.key); + } + + return keys; + } + + get (key: string): T | null { + const data = this.#data.get(key); + + if (data) { + this.#toHead(key); + + return data as T; + } + + return null; + } + + set (key: string, value: T): void { + if (this.#data.has(key)) { + this.#toHead(key); + } else { + const node = new LRUNode(key); + + this.#refs.set(node.key, node); + + if (this.length === 0) { + this.#head = this.#tail = node; + } else { + this.#head.prev = node; + node.next = this.#head; + this.#head = node; + } + + if (this.#length === this.capacity) { + this.#data.delete(this.#tail.key); + this.#refs.delete(this.#tail.key); + + this.#tail = this.#tail.prev; + this.#tail.next = this.#head; + } else { + this.#length += 1; + } + } + + if (this.#ttl > 0 && !this.#ttlP) { + this.#ttlP = setInterval(() => { + this.#ttlClean(); + }, this.#ttlInterval); + } + + this.#data.set(key, value); + } + + #ttlClean() { + // Find last node to keep + const expires = Date.now() - this.#ttl; + // traverse map to find the lastAccessed + while (this.#tail.lastAccess && this.#tail.lastAccess < expires && this.#length > 0) { + if (this.#ttlP && this.#length === 0) { + clearInterval(this.#ttlP); + this.#ttlP = undefined; + this.#head = this.#tail = new LRUNode(''); + } else { + this.#refs.delete(this.#tail.key); + this.#data.delete(this.#tail.key); + this.#length -= 1; + this.#tail = this.#tail.prev; + this.#tail.next = this.#head; + } + } + } + + #toHead (key: string): void { + const ref = this.#refs.get(key); + if (ref && ref !== this.#head) { + ref.refresh(); + ref.prev.next = ref.next; + ref.next.prev = ref.prev; + ref.next = this.#head; + + this.#head.prev = ref; + this.#head = ref; + } + } +} diff --git a/packages/rpc-provider/src/http/index.ts b/packages/rpc-provider/src/http/index.ts index 794702b65268..582f5b67ae59 100644 --- a/packages/rpc-provider/src/http/index.ts +++ b/packages/rpc-provider/src/http/index.ts @@ -8,7 +8,7 @@ import { fetch } from '@polkadot/x-fetch'; import { RpcCoder } from '../coder/index.js'; import defaults from '../defaults.js'; -import { LRUCache } from '../lru.js'; +import { DEFAULT_CAPACITY, LRUCache } from '../lru.js'; const ERROR_SUBSCRIBE = 'HTTP Provider does not have subscriptions, use WebSockets instead'; @@ -35,7 +35,8 @@ const l = logger('api-http'); * @see [[WsProvider]] */ export class HttpProvider implements ProviderInterface { - readonly #callCache = new LRUCache(); + readonly #callCache: LRUCache; + readonly #cacheCapacity: number; readonly #coder: RpcCoder; readonly #endpoint: string; readonly #headers: Record; @@ -44,7 +45,7 @@ export class HttpProvider implements ProviderInterface { /** * @param {string} endpoint The endpoint url starting with http:// */ - constructor (endpoint: string = defaults.HTTP_URL, headers: Record = {}) { + constructor (endpoint: string = defaults.HTTP_URL, headers: Record = {}, cacheCapacity?: number) { if (!/^(https|http):\/\//.test(endpoint)) { throw new Error(`Endpoint should start with 'http://' or 'https://', received '${endpoint}'`); } @@ -52,6 +53,9 @@ export class HttpProvider implements ProviderInterface { this.#coder = new RpcCoder(); this.#endpoint = endpoint; this.#headers = headers; + this.#callCache = new LRUCache(cacheCapacity === 0 ? 0 : cacheCapacity|| DEFAULT_CAPACITY); + this.#cacheCapacity = cacheCapacity === 0 ? 0 : cacheCapacity|| DEFAULT_CAPACITY; + this.#stats = { active: { requests: 0, subscriptions: 0 }, total: { bytesRecv: 0, bytesSent: 0, cached: 0, errors: 0, requests: 0, subscriptions: 0, timeout: 0 } @@ -125,6 +129,11 @@ export class HttpProvider implements ProviderInterface { this.#stats.total.requests++; const [, body] = this.#coder.encodeJson(method, params); + + if (this.#cacheCapacity === 0) { + return this.#send(body); + } + const cacheKey = isCacheable ? `${method}::${stringify(params)}` : ''; let resultPromise: Promise | null = isCacheable ? this.#callCache.get(cacheKey) diff --git a/packages/rpc-provider/src/lru.ts b/packages/rpc-provider/src/lru.ts index b9afa882acd4..e488a1fe5421 100644 --- a/packages/rpc-provider/src/lru.ts +++ b/packages/rpc-provider/src/lru.ts @@ -4,18 +4,31 @@ // Assuming all 1.5MB responses, we apply a default allowing for 192MB // cache space (depending on the historic queries this would vary, metadata // for Kusama/Polkadot/Substrate falls between 600-750K, 2x for estimate) -export const DEFAULT_CAPACITY = 128; +export const DEFAULT_CAPACITY = 64; class LRUNode { readonly key: string; + #lastAccess: number; + readonly createdAt: number; public next: LRUNode; public prev: LRUNode; constructor (key: string) { this.key = key; + this.#lastAccess = Date.now(); + this.createdAt = this.#lastAccess; this.next = this.prev = this; } + + public refresh(): void { + this.#lastAccess = Date.now(); + } + + public get lastAccess(): number { + return this.#lastAccess; + } + } // https://en.wikipedia.org/wiki/Cache_replacement_policies#LRU @@ -28,10 +41,26 @@ export class LRUCache { #length = 0; #head: LRUNode; #tail: LRUNode; + // TTL + readonly #ttl: number; + readonly #ttlInterval: number; + #ttlP?: NodeJS.Timeout = undefined; - constructor (capacity = DEFAULT_CAPACITY) { + constructor (capacity = DEFAULT_CAPACITY, ttl = 30000, ttlInterval = 15000) { this.capacity = capacity; + this.#ttl = ttl; + this.#ttlInterval = ttlInterval; this.#head = this.#tail = new LRUNode(''); + // make sure the interval is not longer than the ttl + if (this.#ttlInterval > this.#ttl) this.#ttlInterval = this.#ttl; + } + + get ttl (): number { + return this.#ttl; + } + + get ttlInterval (): number { + return this.#ttlInterval; } get length (): number { @@ -115,14 +144,39 @@ export class LRUCache { this.#length += 1; } } + + if (this.#ttl > 0 && !this.#ttlP) { + this.#ttlP = setInterval(() => { + this.#ttlClean(); + }, this.#ttlInterval); + } this.#data.set(key, value); } + #ttlClean() { + // Find last node to keep + const expires = Date.now() - this.#ttl; + // traverse map to find the lastAccessed + while (this.#tail.lastAccess && this.#tail.lastAccess < expires && this.#length > 0) { + if (this.#ttlP && this.#length === 0) { + clearInterval(this.#ttlP); + this.#ttlP = undefined; + this.#head = this.#tail = new LRUNode(''); + } else { + this.#refs.delete(this.#tail.key); + this.#data.delete(this.#tail.key); + this.#length -= 1; + this.#tail = this.#tail.prev; + this.#tail.next = this.#head; + } + } + } + #toHead (key: string): void { const ref = this.#refs.get(key); - if (ref && ref !== this.#head) { + ref.refresh(); ref.prev.next = ref.next; ref.next.prev = ref.prev; ref.next = this.#head; diff --git a/packages/rpc-provider/src/ws/index.ts b/packages/rpc-provider/src/ws/index.ts index 1cf65fee610d..fd3c6a2656d6 100644 --- a/packages/rpc-provider/src/ws/index.ts +++ b/packages/rpc-provider/src/ws/index.ts @@ -92,6 +92,7 @@ export class WsProvider implements ProviderInterface { readonly #isReadyPromise: Promise; readonly #stats: ProviderStats; readonly #waitingForId: Record> = {}; + readonly #cacheCapacity: number; #autoConnectMs: number; #endpointIndex: number; @@ -123,6 +124,7 @@ export class WsProvider implements ProviderInterface { } }); this.#callCache = new LRUCache(cacheCapacity || DEFAULT_CAPACITY); + this.#cacheCapacity = cacheCapacity || DEFAULT_CAPACITY; this.#eventemitter = new EventEmitter(); this.#autoConnectMs = autoConnectMs || 0; this.#coder = new RpcCoder(); @@ -312,6 +314,10 @@ export class WsProvider implements ProviderInterface { this.#stats.total.requests++; const [id, body] = this.#coder.encodeJson(method, params); + if (this.#cacheCapacity === 0) { + return this.#send(id, body, method, params, subscription); + } + const cacheKey = isCacheable ? `${method}::${stringify(params)}` : ''; let resultPromise: Promise | null = isCacheable ? this.#callCache.get(cacheKey) From 68357f5a401711ad293f3775ab14e19be3c05bfe Mon Sep 17 00:00:00 2001 From: Filippo Vecchiato Date: Fri, 11 Oct 2024 09:32:41 +0200 Subject: [PATCH 02/11] single export lru --- packages/rpc-core/src/bundle.ts | 2 +- packages/rpc-core/src/lru.spec.ts | 57 --------- packages/rpc-core/src/lru.ts | 188 ---------------------------- packages/rpc-provider/src/bundle.ts | 1 + packages/rpc-provider/src/index.ts | 2 +- packages/rpc-provider/src/mod.ts | 2 +- 6 files changed, 4 insertions(+), 248 deletions(-) delete mode 100644 packages/rpc-core/src/lru.spec.ts delete mode 100644 packages/rpc-core/src/lru.ts diff --git a/packages/rpc-core/src/bundle.ts b/packages/rpc-core/src/bundle.ts index 815121f59b4e..1a07c7ba0c9f 100644 --- a/packages/rpc-core/src/bundle.ts +++ b/packages/rpc-core/src/bundle.ts @@ -15,7 +15,7 @@ import { rpcDefinitions } from '@polkadot/types'; import { hexToU8a, isFunction, isNull, isUndefined, lazyMethod, logger, memoize, objectSpread, u8aConcat, u8aToU8a } from '@polkadot/util'; import { drr, refCountDelay } from './util/index.js'; -import { DEFAULT_CAPACITY, LRUCache } from './lru.js'; +import { DEFAULT_CAPACITY, LRUCache } from "@polkadot/rpc-provider"; export { packageInfo } from './packageInfo.js'; export * from './util/index.js'; diff --git a/packages/rpc-core/src/lru.spec.ts b/packages/rpc-core/src/lru.spec.ts deleted file mode 100644 index 0079eba6c300..000000000000 --- a/packages/rpc-core/src/lru.spec.ts +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2017-2024 @polkadot/rpc-provider authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -/// - -import { LRUCache } from './lru.js'; - -describe('LRUCache', (): void => { - it('allows getting of items below capacity', (): void => { - const keys = ['1', '2', '3', '4']; - const lru = new LRUCache(4); - - keys.forEach((k) => lru.set(k, `${k}${k}${k}`)); - - expect(lru.keys().join(', ')).toEqual(keys.reverse().join(', ')); - expect(lru.length === lru.lengthData && lru.length === lru.lengthRefs).toBe(true); - - keys.forEach((k) => expect(lru.get(k)).toEqual(`${k}${k}${k}`)); - }); - - it('drops items when at capacity', (): void => { - const keys = ['1', '2', '3', '4', '5', '6']; - const lru = new LRUCache(4); - - keys.forEach((k) => lru.set(k, `${k}${k}${k}`)); - - expect(lru.keys().join(', ')).toEqual(keys.slice(2).reverse().join(', ')); - expect(lru.length === lru.lengthData && lru.length === lru.lengthRefs).toBe(true); - - keys.slice(2).forEach((k) => expect(lru.get(k)).toEqual(`${k}${k}${k}`)); - }); - - it('adjusts the order as they are used', (): void => { - const keys = ['1', '2', '3', '4', '5']; - const lru = new LRUCache(4); - - keys.forEach((k) => lru.set(k, `${k}${k}${k}`)); - - expect(lru.entries()).toEqual([['5', '555'], ['4', '444'], ['3', '333'], ['2', '222']]); - expect(lru.length === lru.lengthData && lru.length === lru.lengthRefs).toBe(true); - - lru.get('3'); - - expect(lru.entries()).toEqual([['3', '333'], ['5', '555'], ['4', '444'], ['2', '222']]); - expect(lru.length === lru.lengthData && lru.length === lru.lengthRefs).toBe(true); - - lru.set('4', '4433'); - - expect(lru.entries()).toEqual([['4', '4433'], ['3', '333'], ['5', '555'], ['2', '222']]); - expect(lru.length === lru.lengthData && lru.length === lru.lengthRefs).toBe(true); - - lru.set('6', '666'); - - expect(lru.entries()).toEqual([['6', '666'], ['4', '4433'], ['3', '333'], ['5', '555']]); - expect(lru.length === lru.lengthData && lru.length === lru.lengthRefs).toBe(true); - }); -}); diff --git a/packages/rpc-core/src/lru.ts b/packages/rpc-core/src/lru.ts deleted file mode 100644 index e488a1fe5421..000000000000 --- a/packages/rpc-core/src/lru.ts +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright 2017-2024 @polkadot/rpc-provider authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -// Assuming all 1.5MB responses, we apply a default allowing for 192MB -// cache space (depending on the historic queries this would vary, metadata -// for Kusama/Polkadot/Substrate falls between 600-750K, 2x for estimate) -export const DEFAULT_CAPACITY = 64; - -class LRUNode { - readonly key: string; - #lastAccess: number; - readonly createdAt: number; - - public next: LRUNode; - public prev: LRUNode; - - constructor (key: string) { - this.key = key; - this.#lastAccess = Date.now(); - this.createdAt = this.#lastAccess; - this.next = this.prev = this; - } - - public refresh(): void { - this.#lastAccess = Date.now(); - } - - public get lastAccess(): number { - return this.#lastAccess; - } - -} - -// https://en.wikipedia.org/wiki/Cache_replacement_policies#LRU -export class LRUCache { - readonly capacity: number; - - readonly #data = new Map(); - readonly #refs = new Map(); - - #length = 0; - #head: LRUNode; - #tail: LRUNode; - // TTL - readonly #ttl: number; - readonly #ttlInterval: number; - #ttlP?: NodeJS.Timeout = undefined; - - constructor (capacity = DEFAULT_CAPACITY, ttl = 30000, ttlInterval = 15000) { - this.capacity = capacity; - this.#ttl = ttl; - this.#ttlInterval = ttlInterval; - this.#head = this.#tail = new LRUNode(''); - // make sure the interval is not longer than the ttl - if (this.#ttlInterval > this.#ttl) this.#ttlInterval = this.#ttl; - } - - get ttl (): number { - return this.#ttl; - } - - get ttlInterval (): number { - return this.#ttlInterval; - } - - get length (): number { - return this.#length; - } - - get lengthData (): number { - return this.#data.size; - } - - get lengthRefs (): number { - return this.#refs.size; - } - - entries (): [string, unknown][] { - const keys = this.keys(); - const count = keys.length; - const entries = new Array<[string, unknown]>(count); - - for (let i = 0; i < count; i++) { - const key = keys[i]; - - entries[i] = [key, this.#data.get(key)]; - } - - return entries; - } - - keys (): string[] { - const keys: string[] = []; - - if (this.#length) { - let curr = this.#head; - - while (curr !== this.#tail) { - keys.push(curr.key); - curr = curr.next; - } - - keys.push(curr.key); - } - - return keys; - } - - get (key: string): T | null { - const data = this.#data.get(key); - - if (data) { - this.#toHead(key); - - return data as T; - } - - return null; - } - - set (key: string, value: T): void { - if (this.#data.has(key)) { - this.#toHead(key); - } else { - const node = new LRUNode(key); - - this.#refs.set(node.key, node); - - if (this.length === 0) { - this.#head = this.#tail = node; - } else { - this.#head.prev = node; - node.next = this.#head; - this.#head = node; - } - - if (this.#length === this.capacity) { - this.#data.delete(this.#tail.key); - this.#refs.delete(this.#tail.key); - - this.#tail = this.#tail.prev; - this.#tail.next = this.#head; - } else { - this.#length += 1; - } - } - - if (this.#ttl > 0 && !this.#ttlP) { - this.#ttlP = setInterval(() => { - this.#ttlClean(); - }, this.#ttlInterval); - } - - this.#data.set(key, value); - } - - #ttlClean() { - // Find last node to keep - const expires = Date.now() - this.#ttl; - // traverse map to find the lastAccessed - while (this.#tail.lastAccess && this.#tail.lastAccess < expires && this.#length > 0) { - if (this.#ttlP && this.#length === 0) { - clearInterval(this.#ttlP); - this.#ttlP = undefined; - this.#head = this.#tail = new LRUNode(''); - } else { - this.#refs.delete(this.#tail.key); - this.#data.delete(this.#tail.key); - this.#length -= 1; - this.#tail = this.#tail.prev; - this.#tail.next = this.#head; - } - } - } - - #toHead (key: string): void { - const ref = this.#refs.get(key); - if (ref && ref !== this.#head) { - ref.refresh(); - ref.prev.next = ref.next; - ref.next.prev = ref.prev; - ref.next = this.#head; - - this.#head.prev = ref; - this.#head = ref; - } - } -} diff --git a/packages/rpc-provider/src/bundle.ts b/packages/rpc-provider/src/bundle.ts index 06b0acaf1aaf..15e83c2f6920 100644 --- a/packages/rpc-provider/src/bundle.ts +++ b/packages/rpc-provider/src/bundle.ts @@ -5,3 +5,4 @@ export { HttpProvider } from './http/index.js'; export { packageInfo } from './packageInfo.js'; export { ScProvider } from './substrate-connect/index.js'; export { WsProvider } from './ws/index.js'; +export {DEFAULT_CAPACITY, LRUCache} from './lru.js'; \ No newline at end of file diff --git a/packages/rpc-provider/src/index.ts b/packages/rpc-provider/src/index.ts index e88d86bac6df..07af3e1478d4 100644 --- a/packages/rpc-provider/src/index.ts +++ b/packages/rpc-provider/src/index.ts @@ -3,4 +3,4 @@ import './packageDetect.js'; -export * from './bundle.js'; +export * from './bundle.js'; \ No newline at end of file diff --git a/packages/rpc-provider/src/mod.ts b/packages/rpc-provider/src/mod.ts index aa7b729d07ce..4ef63635b855 100644 --- a/packages/rpc-provider/src/mod.ts +++ b/packages/rpc-provider/src/mod.ts @@ -1,4 +1,4 @@ // Copyright 2017-2024 @polkadot/rpc-provider authors & contributors // SPDX-License-Identifier: Apache-2.0 -export * from './index.js'; +export * from './index.js'; \ No newline at end of file From b5f21d29d552d4be0b229f2fb3e19959ccd0dc1b Mon Sep 17 00:00:00 2001 From: Filippo Vecchiato Date: Fri, 11 Oct 2024 17:40:58 +0200 Subject: [PATCH 03/11] fix lint --- packages/rpc-core/src/bundle.ts | 4 ++-- packages/rpc-provider/src/bundle.ts | 2 +- packages/rpc-provider/src/http/index.ts | 6 +++--- packages/rpc-provider/src/index.ts | 2 +- packages/rpc-provider/src/lru.ts | 18 +++++++++++------- packages/rpc-provider/src/mod.ts | 2 +- packages/rpc-provider/src/ws/index.ts | 3 ++- 7 files changed, 21 insertions(+), 16 deletions(-) diff --git a/packages/rpc-core/src/bundle.ts b/packages/rpc-core/src/bundle.ts index 1a07c7ba0c9f..9ca8ed87d354 100644 --- a/packages/rpc-core/src/bundle.ts +++ b/packages/rpc-core/src/bundle.ts @@ -11,11 +11,11 @@ import type { RpcCoreStats, RpcInterfaceMethod } from './types/index.js'; import { Observable, publishReplay, refCount } from 'rxjs'; +import { DEFAULT_CAPACITY, LRUCache } from '@polkadot/rpc-provider'; import { rpcDefinitions } from '@polkadot/types'; import { hexToU8a, isFunction, isNull, isUndefined, lazyMethod, logger, memoize, objectSpread, u8aConcat, u8aToU8a } from '@polkadot/util'; import { drr, refCountDelay } from './util/index.js'; -import { DEFAULT_CAPACITY, LRUCache } from "@polkadot/rpc-provider"; export { packageInfo } from './packageInfo.js'; export * from './util/index.js'; @@ -487,7 +487,7 @@ export class RpcCore { ? value : u8aToU8a(value); const codec = this._newType(registry, blockHash, key, input, isEmpty, entryIndex); - + this._setToCache(hexKey, codec); return codec; diff --git a/packages/rpc-provider/src/bundle.ts b/packages/rpc-provider/src/bundle.ts index 15e83c2f6920..8a41c307b73d 100644 --- a/packages/rpc-provider/src/bundle.ts +++ b/packages/rpc-provider/src/bundle.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 export { HttpProvider } from './http/index.js'; +export { DEFAULT_CAPACITY, LRUCache } from './lru.js'; export { packageInfo } from './packageInfo.js'; export { ScProvider } from './substrate-connect/index.js'; export { WsProvider } from './ws/index.js'; -export {DEFAULT_CAPACITY, LRUCache} from './lru.js'; \ No newline at end of file diff --git a/packages/rpc-provider/src/http/index.ts b/packages/rpc-provider/src/http/index.ts index 582f5b67ae59..21f1a70c37bc 100644 --- a/packages/rpc-provider/src/http/index.ts +++ b/packages/rpc-provider/src/http/index.ts @@ -53,8 +53,8 @@ export class HttpProvider implements ProviderInterface { this.#coder = new RpcCoder(); this.#endpoint = endpoint; this.#headers = headers; - this.#callCache = new LRUCache(cacheCapacity === 0 ? 0 : cacheCapacity|| DEFAULT_CAPACITY); - this.#cacheCapacity = cacheCapacity === 0 ? 0 : cacheCapacity|| DEFAULT_CAPACITY; + this.#callCache = new LRUCache(cacheCapacity === 0 ? 0 : cacheCapacity || DEFAULT_CAPACITY); + this.#cacheCapacity = cacheCapacity === 0 ? 0 : cacheCapacity || DEFAULT_CAPACITY; this.#stats = { active: { requests: 0, subscriptions: 0 }, @@ -133,7 +133,7 @@ export class HttpProvider implements ProviderInterface { if (this.#cacheCapacity === 0) { return this.#send(body); } - + const cacheKey = isCacheable ? `${method}::${stringify(params)}` : ''; let resultPromise: Promise | null = isCacheable ? this.#callCache.get(cacheKey) diff --git a/packages/rpc-provider/src/index.ts b/packages/rpc-provider/src/index.ts index 07af3e1478d4..e88d86bac6df 100644 --- a/packages/rpc-provider/src/index.ts +++ b/packages/rpc-provider/src/index.ts @@ -3,4 +3,4 @@ import './packageDetect.js'; -export * from './bundle.js'; \ No newline at end of file +export * from './bundle.js'; diff --git a/packages/rpc-provider/src/lru.ts b/packages/rpc-provider/src/lru.ts index e488a1fe5421..61e2533f1465 100644 --- a/packages/rpc-provider/src/lru.ts +++ b/packages/rpc-provider/src/lru.ts @@ -21,14 +21,13 @@ class LRUNode { this.next = this.prev = this; } - public refresh(): void { + public refresh (): void { this.#lastAccess = Date.now(); } - public get lastAccess(): number { + public get lastAccess (): number { return this.#lastAccess; } - } // https://en.wikipedia.org/wiki/Cache_replacement_policies#LRU @@ -51,8 +50,11 @@ export class LRUCache { this.#ttl = ttl; this.#ttlInterval = ttlInterval; this.#head = this.#tail = new LRUNode(''); + // make sure the interval is not longer than the ttl - if (this.#ttlInterval > this.#ttl) this.#ttlInterval = this.#ttl; + if (this.#ttlInterval > this.#ttl) { + this.#ttlInterval = this.#ttl; + } } get ttl (): number { @@ -144,7 +146,7 @@ export class LRUCache { this.#length += 1; } } - + if (this.#ttl > 0 && !this.#ttlP) { this.#ttlP = setInterval(() => { this.#ttlClean(); @@ -154,10 +156,11 @@ export class LRUCache { this.#data.set(key, value); } - #ttlClean() { + #ttlClean () { // Find last node to keep const expires = Date.now() - this.#ttl; - // traverse map to find the lastAccessed + + // traverse map to find the lastAccessed while (this.#tail.lastAccess && this.#tail.lastAccess < expires && this.#length > 0) { if (this.#ttlP && this.#length === 0) { clearInterval(this.#ttlP); @@ -175,6 +178,7 @@ export class LRUCache { #toHead (key: string): void { const ref = this.#refs.get(key); + if (ref && ref !== this.#head) { ref.refresh(); ref.prev.next = ref.next; diff --git a/packages/rpc-provider/src/mod.ts b/packages/rpc-provider/src/mod.ts index 4ef63635b855..aa7b729d07ce 100644 --- a/packages/rpc-provider/src/mod.ts +++ b/packages/rpc-provider/src/mod.ts @@ -1,4 +1,4 @@ // Copyright 2017-2024 @polkadot/rpc-provider authors & contributors // SPDX-License-Identifier: Apache-2.0 -export * from './index.js'; \ No newline at end of file +export * from './index.js'; diff --git a/packages/rpc-provider/src/ws/index.ts b/packages/rpc-provider/src/ws/index.ts index fd3c6a2656d6..c75c63598c9d 100644 --- a/packages/rpc-provider/src/ws/index.ts +++ b/packages/rpc-provider/src/ws/index.ts @@ -314,10 +314,11 @@ export class WsProvider implements ProviderInterface { this.#stats.total.requests++; const [id, body] = this.#coder.encodeJson(method, params); + if (this.#cacheCapacity === 0) { return this.#send(id, body, method, params, subscription); } - + const cacheKey = isCacheable ? `${method}::${stringify(params)}` : ''; let resultPromise: Promise | null = isCacheable ? this.#callCache.get(cacheKey) From 580520af0a35cc79e1e07b53005c2064f8c63bd6 Mon Sep 17 00:00:00 2001 From: Filippo Vecchiato Date: Mon, 14 Oct 2024 09:39:32 +0200 Subject: [PATCH 04/11] fix hanging tests --- packages/rpc-provider/src/lru.spec.ts | 59 +++++++++++++++------------ packages/rpc-provider/src/lru.ts | 23 ++++++++++- 2 files changed, 55 insertions(+), 27 deletions(-) diff --git a/packages/rpc-provider/src/lru.spec.ts b/packages/rpc-provider/src/lru.spec.ts index 0079eba6c300..a2c1c0db68c4 100644 --- a/packages/rpc-provider/src/lru.spec.ts +++ b/packages/rpc-provider/src/lru.spec.ts @@ -5,53 +5,62 @@ import { LRUCache } from './lru.js'; + + describe('LRUCache', (): void => { - it('allows getting of items below capacity', (): void => { + let lru: LRUCache | undefined; + beforeEach((): void => { + lru = new LRUCache(4); + }); + + afterEach(async () => { + await lru?.clearInterval(); + lru = undefined; + }); + + it('allows getting of items below capacity', async (): Promise => { const keys = ['1', '2', '3', '4']; - const lru = new LRUCache(4); - - keys.forEach((k) => lru.set(k, `${k}${k}${k}`)); + keys.forEach((k) => lru?.set(k, `${k}${k}${k}`)); + let lruKeys = lru?.keys(); - expect(lru.keys().join(', ')).toEqual(keys.reverse().join(', ')); - expect(lru.length === lru.lengthData && lru.length === lru.lengthRefs).toBe(true); + expect(lruKeys && lruKeys.join(', ')).toBe(keys.reverse().join(', ')); + expect(lru?.length === lru?.lengthData && lru?.length === lru?.lengthRefs).toBe(true); - keys.forEach((k) => expect(lru.get(k)).toEqual(`${k}${k}${k}`)); + keys.forEach((k) => expect(lru?.get(k)).toEqual(`${k}${k}${k}`)); }); it('drops items when at capacity', (): void => { const keys = ['1', '2', '3', '4', '5', '6']; - const lru = new LRUCache(4); - keys.forEach((k) => lru.set(k, `${k}${k}${k}`)); + keys.forEach((k) => lru?.set(k, `${k}${k}${k}`)); - expect(lru.keys().join(', ')).toEqual(keys.slice(2).reverse().join(', ')); - expect(lru.length === lru.lengthData && lru.length === lru.lengthRefs).toBe(true); + expect(lru?.keys().join(', ')).toEqual(keys.slice(2).reverse().join(', ')); + expect(lru?.length === lru?.lengthData && lru?.length === lru?.lengthRefs).toBe(true); - keys.slice(2).forEach((k) => expect(lru.get(k)).toEqual(`${k}${k}${k}`)); + keys.slice(2).forEach((k) => expect(lru?.get(k)).toEqual(`${k}${k}${k}`)); }); it('adjusts the order as they are used', (): void => { const keys = ['1', '2', '3', '4', '5']; - const lru = new LRUCache(4); - keys.forEach((k) => lru.set(k, `${k}${k}${k}`)); + keys.forEach((k) => lru?.set(k, `${k}${k}${k}`)); - expect(lru.entries()).toEqual([['5', '555'], ['4', '444'], ['3', '333'], ['2', '222']]); - expect(lru.length === lru.lengthData && lru.length === lru.lengthRefs).toBe(true); + expect(lru?.entries()).toEqual([['5', '555'], ['4', '444'], ['3', '333'], ['2', '222']]); + expect(lru?.length === lru?.lengthData && lru?.length === lru?.lengthRefs).toBe(true); - lru.get('3'); + lru?.get('3'); - expect(lru.entries()).toEqual([['3', '333'], ['5', '555'], ['4', '444'], ['2', '222']]); - expect(lru.length === lru.lengthData && lru.length === lru.lengthRefs).toBe(true); + expect(lru?.entries()).toEqual([['3', '333'], ['5', '555'], ['4', '444'], ['2', '222']]); + expect(lru?.length === lru?.lengthData && lru?.length === lru?.lengthRefs).toBe(true); - lru.set('4', '4433'); + lru?.set('4', '4433'); - expect(lru.entries()).toEqual([['4', '4433'], ['3', '333'], ['5', '555'], ['2', '222']]); - expect(lru.length === lru.lengthData && lru.length === lru.lengthRefs).toBe(true); + expect(lru?.entries()).toEqual([['4', '4433'], ['3', '333'], ['5', '555'], ['2', '222']]); + expect(lru?.length === lru?.lengthData && lru?.length === lru?.lengthRefs).toBe(true); - lru.set('6', '666'); + lru?.set('6', '666'); - expect(lru.entries()).toEqual([['6', '666'], ['4', '4433'], ['3', '333'], ['5', '555']]); - expect(lru.length === lru.lengthData && lru.length === lru.lengthRefs).toBe(true); + expect(lru?.entries()).toEqual([['6', '666'], ['4', '4433'], ['3', '333'], ['5', '555']]); + expect(lru?.length === lru?.lengthData && lru?.length === lru?.lengthRefs).toBe(true); }); }); diff --git a/packages/rpc-provider/src/lru.ts b/packages/rpc-provider/src/lru.ts index 61e2533f1465..ce25d1e0437b 100644 --- a/packages/rpc-provider/src/lru.ts +++ b/packages/rpc-provider/src/lru.ts @@ -4,6 +4,16 @@ // Assuming all 1.5MB responses, we apply a default allowing for 192MB // cache space (depending on the historic queries this would vary, metadata // for Kusama/Polkadot/Substrate falls between 600-750K, 2x for estimate) + +import {createWriteStream} from 'fs' +import { config } from 'process' + +console.log = async (message: any) => { + const tty = createWriteStream('/dev/tty') + const msg = typeof message === 'string' ? message : JSON.stringify(message, null, 2) + return tty.write(msg + '\n') +} + export const DEFAULT_CAPACITY = 64; class LRUNode { @@ -43,7 +53,7 @@ export class LRUCache { // TTL readonly #ttl: number; readonly #ttlInterval: number; - #ttlP?: NodeJS.Timeout = undefined; + #ttlP: NodeJS.Timeout | undefined = undefined; constructor (capacity = DEFAULT_CAPACITY, ttl = 30000, ttlInterval = 15000) { this.capacity = capacity; @@ -125,7 +135,6 @@ export class LRUCache { this.#toHead(key); } else { const node = new LRUNode(key); - this.#refs.set(node.key, node); if (this.length === 0) { @@ -154,6 +163,8 @@ export class LRUCache { } this.#data.set(key, value); + + return; } #ttlClean () { @@ -189,4 +200,12 @@ export class LRUCache { this.#head = ref; } } + + // eslint-disable-next-line @typescript-eslint/require-await + public async clearInterval (): Promise { + if (this.#ttlP) { + clearInterval(this.#ttlP); + this.#ttlP = undefined; + } + } } From 61a7990c440b93a24debd10196c7b47272589c66 Mon Sep 17 00:00:00 2001 From: Filippo Vecchiato Date: Tue, 15 Oct 2024 11:22:21 +0200 Subject: [PATCH 05/11] clear intervals in RPCcore --- packages/rpc-core/src/bundle.ts | 3 ++- packages/rpc-provider/src/lru.spec.ts | 13 ++++++------ packages/rpc-provider/src/lru.ts | 30 +++++++++------------------ 3 files changed, 18 insertions(+), 28 deletions(-) diff --git a/packages/rpc-core/src/bundle.ts b/packages/rpc-core/src/bundle.ts index 9ca8ed87d354..9e08efc2bd11 100644 --- a/packages/rpc-core/src/bundle.ts +++ b/packages/rpc-core/src/bundle.ts @@ -146,7 +146,8 @@ export class RpcCore { /** * @description Manually disconnect from the attached provider */ - public disconnect (): Promise { + public async disconnect (): Promise { + await this.#storageCache.clearInterval(); return this.provider.disconnect(); } diff --git a/packages/rpc-provider/src/lru.spec.ts b/packages/rpc-provider/src/lru.spec.ts index a2c1c0db68c4..d36e05b677c4 100644 --- a/packages/rpc-provider/src/lru.spec.ts +++ b/packages/rpc-provider/src/lru.spec.ts @@ -5,25 +5,24 @@ import { LRUCache } from './lru.js'; - - describe('LRUCache', (): void => { let lru: LRUCache | undefined; + beforeEach((): void => { lru = new LRUCache(4); }); - afterEach(async () => { await lru?.clearInterval(); lru = undefined; }); - - it('allows getting of items below capacity', async (): Promise => { + + it('allows getting of items below capacity', (): void => { const keys = ['1', '2', '3', '4']; + keys.forEach((k) => lru?.set(k, `${k}${k}${k}`)); - let lruKeys = lru?.keys(); + const lruKeys = lru?.keys(); - expect(lruKeys && lruKeys.join(', ')).toBe(keys.reverse().join(', ')); + expect(lruKeys?.join(', ')).toBe(keys.reverse().join(', ')); expect(lru?.length === lru?.lengthData && lru?.length === lru?.lengthRefs).toBe(true); keys.forEach((k) => expect(lru?.get(k)).toEqual(`${k}${k}${k}`)); diff --git a/packages/rpc-provider/src/lru.ts b/packages/rpc-provider/src/lru.ts index ce25d1e0437b..9483b718b4f3 100644 --- a/packages/rpc-provider/src/lru.ts +++ b/packages/rpc-provider/src/lru.ts @@ -5,15 +5,6 @@ // cache space (depending on the historic queries this would vary, metadata // for Kusama/Polkadot/Substrate falls between 600-750K, 2x for estimate) -import {createWriteStream} from 'fs' -import { config } from 'process' - -console.log = async (message: any) => { - const tty = createWriteStream('/dev/tty') - const msg = typeof message === 'string' ? message : JSON.stringify(message, null, 2) - return tty.write(msg + '\n') -} - export const DEFAULT_CAPACITY = 64; class LRUNode { @@ -53,7 +44,7 @@ export class LRUCache { // TTL readonly #ttl: number; readonly #ttlInterval: number; - #ttlP: NodeJS.Timeout | undefined = undefined; + #ttlTimerId: ReturnType | null = null; constructor (capacity = DEFAULT_CAPACITY, ttl = 30000, ttlInterval = 15000) { this.capacity = capacity; @@ -135,6 +126,7 @@ export class LRUCache { this.#toHead(key); } else { const node = new LRUNode(key); + this.#refs.set(node.key, node); if (this.length === 0) { @@ -156,15 +148,13 @@ export class LRUCache { } } - if (this.#ttl > 0 && !this.#ttlP) { - this.#ttlP = setInterval(() => { + if (this.#ttl > 0 && !this.#ttlTimerId) { + this.#ttlTimerId = setInterval(() => { this.#ttlClean(); }, this.#ttlInterval); } this.#data.set(key, value); - - return; } #ttlClean () { @@ -173,9 +163,9 @@ export class LRUCache { // traverse map to find the lastAccessed while (this.#tail.lastAccess && this.#tail.lastAccess < expires && this.#length > 0) { - if (this.#ttlP && this.#length === 0) { - clearInterval(this.#ttlP); - this.#ttlP = undefined; + if (this.#ttlTimerId && this.#length === 0) { + clearInterval(this.#ttlTimerId); + this.#ttlTimerId = null; this.#head = this.#tail = new LRUNode(''); } else { this.#refs.delete(this.#tail.key); @@ -203,9 +193,9 @@ export class LRUCache { // eslint-disable-next-line @typescript-eslint/require-await public async clearInterval (): Promise { - if (this.#ttlP) { - clearInterval(this.#ttlP); - this.#ttlP = undefined; + if (this.#ttlTimerId) { + clearInterval(this.#ttlTimerId); + this.#ttlTimerId = null; } } } From eb813876a2a4398b9e34fce79cd61442fe5b96d5 Mon Sep 17 00:00:00 2001 From: Filippo Vecchiato Date: Tue, 15 Oct 2024 11:26:33 +0200 Subject: [PATCH 06/11] clear linting issues --- packages/rpc-core/src/bundle.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/rpc-core/src/bundle.ts b/packages/rpc-core/src/bundle.ts index 9e08efc2bd11..7a99908b054b 100644 --- a/packages/rpc-core/src/bundle.ts +++ b/packages/rpc-core/src/bundle.ts @@ -148,6 +148,7 @@ export class RpcCore { */ public async disconnect (): Promise { await this.#storageCache.clearInterval(); + return this.provider.disconnect(); } From f9dec4961bddb9143c3957a769c30a3de1f72e0d Mon Sep 17 00:00:00 2001 From: Filippo Vecchiato Date: Tue, 15 Oct 2024 14:51:30 +0200 Subject: [PATCH 07/11] checking export issue on deno ci --- packages/rpc-core/src/bundle.ts | 2 +- packages/rpc-core/src/lru.spec.ts | 65 +++++++++ packages/rpc-core/src/lru.ts | 201 ++++++++++++++++++++++++++++ packages/rpc-provider/src/bundle.ts | 1 - 4 files changed, 267 insertions(+), 2 deletions(-) create mode 100644 packages/rpc-core/src/lru.spec.ts create mode 100644 packages/rpc-core/src/lru.ts diff --git a/packages/rpc-core/src/bundle.ts b/packages/rpc-core/src/bundle.ts index 7a99908b054b..bd45bb77d418 100644 --- a/packages/rpc-core/src/bundle.ts +++ b/packages/rpc-core/src/bundle.ts @@ -11,7 +11,7 @@ import type { RpcCoreStats, RpcInterfaceMethod } from './types/index.js'; import { Observable, publishReplay, refCount } from 'rxjs'; -import { DEFAULT_CAPACITY, LRUCache } from '@polkadot/rpc-provider'; +import { DEFAULT_CAPACITY, LRUCache } from './lru.js'; import { rpcDefinitions } from '@polkadot/types'; import { hexToU8a, isFunction, isNull, isUndefined, lazyMethod, logger, memoize, objectSpread, u8aConcat, u8aToU8a } from '@polkadot/util'; diff --git a/packages/rpc-core/src/lru.spec.ts b/packages/rpc-core/src/lru.spec.ts new file mode 100644 index 000000000000..d36e05b677c4 --- /dev/null +++ b/packages/rpc-core/src/lru.spec.ts @@ -0,0 +1,65 @@ +// Copyright 2017-2024 @polkadot/rpc-provider authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +/// + +import { LRUCache } from './lru.js'; + +describe('LRUCache', (): void => { + let lru: LRUCache | undefined; + + beforeEach((): void => { + lru = new LRUCache(4); + }); + afterEach(async () => { + await lru?.clearInterval(); + lru = undefined; + }); + + it('allows getting of items below capacity', (): void => { + const keys = ['1', '2', '3', '4']; + + keys.forEach((k) => lru?.set(k, `${k}${k}${k}`)); + const lruKeys = lru?.keys(); + + expect(lruKeys?.join(', ')).toBe(keys.reverse().join(', ')); + expect(lru?.length === lru?.lengthData && lru?.length === lru?.lengthRefs).toBe(true); + + keys.forEach((k) => expect(lru?.get(k)).toEqual(`${k}${k}${k}`)); + }); + + it('drops items when at capacity', (): void => { + const keys = ['1', '2', '3', '4', '5', '6']; + + keys.forEach((k) => lru?.set(k, `${k}${k}${k}`)); + + expect(lru?.keys().join(', ')).toEqual(keys.slice(2).reverse().join(', ')); + expect(lru?.length === lru?.lengthData && lru?.length === lru?.lengthRefs).toBe(true); + + keys.slice(2).forEach((k) => expect(lru?.get(k)).toEqual(`${k}${k}${k}`)); + }); + + it('adjusts the order as they are used', (): void => { + const keys = ['1', '2', '3', '4', '5']; + + keys.forEach((k) => lru?.set(k, `${k}${k}${k}`)); + + expect(lru?.entries()).toEqual([['5', '555'], ['4', '444'], ['3', '333'], ['2', '222']]); + expect(lru?.length === lru?.lengthData && lru?.length === lru?.lengthRefs).toBe(true); + + lru?.get('3'); + + expect(lru?.entries()).toEqual([['3', '333'], ['5', '555'], ['4', '444'], ['2', '222']]); + expect(lru?.length === lru?.lengthData && lru?.length === lru?.lengthRefs).toBe(true); + + lru?.set('4', '4433'); + + expect(lru?.entries()).toEqual([['4', '4433'], ['3', '333'], ['5', '555'], ['2', '222']]); + expect(lru?.length === lru?.lengthData && lru?.length === lru?.lengthRefs).toBe(true); + + lru?.set('6', '666'); + + expect(lru?.entries()).toEqual([['6', '666'], ['4', '4433'], ['3', '333'], ['5', '555']]); + expect(lru?.length === lru?.lengthData && lru?.length === lru?.lengthRefs).toBe(true); + }); +}); diff --git a/packages/rpc-core/src/lru.ts b/packages/rpc-core/src/lru.ts new file mode 100644 index 000000000000..9483b718b4f3 --- /dev/null +++ b/packages/rpc-core/src/lru.ts @@ -0,0 +1,201 @@ +// Copyright 2017-2024 @polkadot/rpc-provider authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +// Assuming all 1.5MB responses, we apply a default allowing for 192MB +// cache space (depending on the historic queries this would vary, metadata +// for Kusama/Polkadot/Substrate falls between 600-750K, 2x for estimate) + +export const DEFAULT_CAPACITY = 64; + +class LRUNode { + readonly key: string; + #lastAccess: number; + readonly createdAt: number; + + public next: LRUNode; + public prev: LRUNode; + + constructor (key: string) { + this.key = key; + this.#lastAccess = Date.now(); + this.createdAt = this.#lastAccess; + this.next = this.prev = this; + } + + public refresh (): void { + this.#lastAccess = Date.now(); + } + + public get lastAccess (): number { + return this.#lastAccess; + } +} + +// https://en.wikipedia.org/wiki/Cache_replacement_policies#LRU +export class LRUCache { + readonly capacity: number; + + readonly #data = new Map(); + readonly #refs = new Map(); + + #length = 0; + #head: LRUNode; + #tail: LRUNode; + // TTL + readonly #ttl: number; + readonly #ttlInterval: number; + #ttlTimerId: ReturnType | null = null; + + constructor (capacity = DEFAULT_CAPACITY, ttl = 30000, ttlInterval = 15000) { + this.capacity = capacity; + this.#ttl = ttl; + this.#ttlInterval = ttlInterval; + this.#head = this.#tail = new LRUNode(''); + + // make sure the interval is not longer than the ttl + if (this.#ttlInterval > this.#ttl) { + this.#ttlInterval = this.#ttl; + } + } + + get ttl (): number { + return this.#ttl; + } + + get ttlInterval (): number { + return this.#ttlInterval; + } + + get length (): number { + return this.#length; + } + + get lengthData (): number { + return this.#data.size; + } + + get lengthRefs (): number { + return this.#refs.size; + } + + entries (): [string, unknown][] { + const keys = this.keys(); + const count = keys.length; + const entries = new Array<[string, unknown]>(count); + + for (let i = 0; i < count; i++) { + const key = keys[i]; + + entries[i] = [key, this.#data.get(key)]; + } + + return entries; + } + + keys (): string[] { + const keys: string[] = []; + + if (this.#length) { + let curr = this.#head; + + while (curr !== this.#tail) { + keys.push(curr.key); + curr = curr.next; + } + + keys.push(curr.key); + } + + return keys; + } + + get (key: string): T | null { + const data = this.#data.get(key); + + if (data) { + this.#toHead(key); + + return data as T; + } + + return null; + } + + set (key: string, value: T): void { + if (this.#data.has(key)) { + this.#toHead(key); + } else { + const node = new LRUNode(key); + + this.#refs.set(node.key, node); + + if (this.length === 0) { + this.#head = this.#tail = node; + } else { + this.#head.prev = node; + node.next = this.#head; + this.#head = node; + } + + if (this.#length === this.capacity) { + this.#data.delete(this.#tail.key); + this.#refs.delete(this.#tail.key); + + this.#tail = this.#tail.prev; + this.#tail.next = this.#head; + } else { + this.#length += 1; + } + } + + if (this.#ttl > 0 && !this.#ttlTimerId) { + this.#ttlTimerId = setInterval(() => { + this.#ttlClean(); + }, this.#ttlInterval); + } + + this.#data.set(key, value); + } + + #ttlClean () { + // Find last node to keep + const expires = Date.now() - this.#ttl; + + // traverse map to find the lastAccessed + while (this.#tail.lastAccess && this.#tail.lastAccess < expires && this.#length > 0) { + if (this.#ttlTimerId && this.#length === 0) { + clearInterval(this.#ttlTimerId); + this.#ttlTimerId = null; + this.#head = this.#tail = new LRUNode(''); + } else { + this.#refs.delete(this.#tail.key); + this.#data.delete(this.#tail.key); + this.#length -= 1; + this.#tail = this.#tail.prev; + this.#tail.next = this.#head; + } + } + } + + #toHead (key: string): void { + const ref = this.#refs.get(key); + + if (ref && ref !== this.#head) { + ref.refresh(); + ref.prev.next = ref.next; + ref.next.prev = ref.prev; + ref.next = this.#head; + + this.#head.prev = ref; + this.#head = ref; + } + } + + // eslint-disable-next-line @typescript-eslint/require-await + public async clearInterval (): Promise { + if (this.#ttlTimerId) { + clearInterval(this.#ttlTimerId); + this.#ttlTimerId = null; + } + } +} diff --git a/packages/rpc-provider/src/bundle.ts b/packages/rpc-provider/src/bundle.ts index 8a41c307b73d..06b0acaf1aaf 100644 --- a/packages/rpc-provider/src/bundle.ts +++ b/packages/rpc-provider/src/bundle.ts @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 export { HttpProvider } from './http/index.js'; -export { DEFAULT_CAPACITY, LRUCache } from './lru.js'; export { packageInfo } from './packageInfo.js'; export { ScProvider } from './substrate-connect/index.js'; export { WsProvider } from './ws/index.js'; From 1317474bd201e4bc4e25bdc81f581ecd0a2a5df6 Mon Sep 17 00:00:00 2001 From: Filippo Vecchiato Date: Tue, 15 Oct 2024 15:18:13 +0200 Subject: [PATCH 08/11] lint --- packages/rpc-core/src/bundle.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rpc-core/src/bundle.ts b/packages/rpc-core/src/bundle.ts index bd45bb77d418..49b879029f23 100644 --- a/packages/rpc-core/src/bundle.ts +++ b/packages/rpc-core/src/bundle.ts @@ -11,11 +11,11 @@ import type { RpcCoreStats, RpcInterfaceMethod } from './types/index.js'; import { Observable, publishReplay, refCount } from 'rxjs'; -import { DEFAULT_CAPACITY, LRUCache } from './lru.js'; import { rpcDefinitions } from '@polkadot/types'; import { hexToU8a, isFunction, isNull, isUndefined, lazyMethod, logger, memoize, objectSpread, u8aConcat, u8aToU8a } from '@polkadot/util'; import { drr, refCountDelay } from './util/index.js'; +import { DEFAULT_CAPACITY, LRUCache } from './lru.js'; export { packageInfo } from './packageInfo.js'; export * from './util/index.js'; From d2c1258800646ca106d3af992ad0f898f84a9484 Mon Sep 17 00:00:00 2001 From: Filippo Vecchiato Date: Tue, 15 Oct 2024 19:18:18 +0200 Subject: [PATCH 09/11] deno update and rebase --- packages/rpc-core/src/bundle.ts | 2 +- packages/rpc-core/src/lru.spec.ts | 65 --------- packages/rpc-core/src/lru.ts | 201 ---------------------------- packages/rpc-provider/src/bundle.ts | 1 + 4 files changed, 2 insertions(+), 267 deletions(-) delete mode 100644 packages/rpc-core/src/lru.spec.ts delete mode 100644 packages/rpc-core/src/lru.ts diff --git a/packages/rpc-core/src/bundle.ts b/packages/rpc-core/src/bundle.ts index 49b879029f23..7a99908b054b 100644 --- a/packages/rpc-core/src/bundle.ts +++ b/packages/rpc-core/src/bundle.ts @@ -11,11 +11,11 @@ import type { RpcCoreStats, RpcInterfaceMethod } from './types/index.js'; import { Observable, publishReplay, refCount } from 'rxjs'; +import { DEFAULT_CAPACITY, LRUCache } from '@polkadot/rpc-provider'; import { rpcDefinitions } from '@polkadot/types'; import { hexToU8a, isFunction, isNull, isUndefined, lazyMethod, logger, memoize, objectSpread, u8aConcat, u8aToU8a } from '@polkadot/util'; import { drr, refCountDelay } from './util/index.js'; -import { DEFAULT_CAPACITY, LRUCache } from './lru.js'; export { packageInfo } from './packageInfo.js'; export * from './util/index.js'; diff --git a/packages/rpc-core/src/lru.spec.ts b/packages/rpc-core/src/lru.spec.ts deleted file mode 100644 index d36e05b677c4..000000000000 --- a/packages/rpc-core/src/lru.spec.ts +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2017-2024 @polkadot/rpc-provider authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -/// - -import { LRUCache } from './lru.js'; - -describe('LRUCache', (): void => { - let lru: LRUCache | undefined; - - beforeEach((): void => { - lru = new LRUCache(4); - }); - afterEach(async () => { - await lru?.clearInterval(); - lru = undefined; - }); - - it('allows getting of items below capacity', (): void => { - const keys = ['1', '2', '3', '4']; - - keys.forEach((k) => lru?.set(k, `${k}${k}${k}`)); - const lruKeys = lru?.keys(); - - expect(lruKeys?.join(', ')).toBe(keys.reverse().join(', ')); - expect(lru?.length === lru?.lengthData && lru?.length === lru?.lengthRefs).toBe(true); - - keys.forEach((k) => expect(lru?.get(k)).toEqual(`${k}${k}${k}`)); - }); - - it('drops items when at capacity', (): void => { - const keys = ['1', '2', '3', '4', '5', '6']; - - keys.forEach((k) => lru?.set(k, `${k}${k}${k}`)); - - expect(lru?.keys().join(', ')).toEqual(keys.slice(2).reverse().join(', ')); - expect(lru?.length === lru?.lengthData && lru?.length === lru?.lengthRefs).toBe(true); - - keys.slice(2).forEach((k) => expect(lru?.get(k)).toEqual(`${k}${k}${k}`)); - }); - - it('adjusts the order as they are used', (): void => { - const keys = ['1', '2', '3', '4', '5']; - - keys.forEach((k) => lru?.set(k, `${k}${k}${k}`)); - - expect(lru?.entries()).toEqual([['5', '555'], ['4', '444'], ['3', '333'], ['2', '222']]); - expect(lru?.length === lru?.lengthData && lru?.length === lru?.lengthRefs).toBe(true); - - lru?.get('3'); - - expect(lru?.entries()).toEqual([['3', '333'], ['5', '555'], ['4', '444'], ['2', '222']]); - expect(lru?.length === lru?.lengthData && lru?.length === lru?.lengthRefs).toBe(true); - - lru?.set('4', '4433'); - - expect(lru?.entries()).toEqual([['4', '4433'], ['3', '333'], ['5', '555'], ['2', '222']]); - expect(lru?.length === lru?.lengthData && lru?.length === lru?.lengthRefs).toBe(true); - - lru?.set('6', '666'); - - expect(lru?.entries()).toEqual([['6', '666'], ['4', '4433'], ['3', '333'], ['5', '555']]); - expect(lru?.length === lru?.lengthData && lru?.length === lru?.lengthRefs).toBe(true); - }); -}); diff --git a/packages/rpc-core/src/lru.ts b/packages/rpc-core/src/lru.ts deleted file mode 100644 index 9483b718b4f3..000000000000 --- a/packages/rpc-core/src/lru.ts +++ /dev/null @@ -1,201 +0,0 @@ -// Copyright 2017-2024 @polkadot/rpc-provider authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -// Assuming all 1.5MB responses, we apply a default allowing for 192MB -// cache space (depending on the historic queries this would vary, metadata -// for Kusama/Polkadot/Substrate falls between 600-750K, 2x for estimate) - -export const DEFAULT_CAPACITY = 64; - -class LRUNode { - readonly key: string; - #lastAccess: number; - readonly createdAt: number; - - public next: LRUNode; - public prev: LRUNode; - - constructor (key: string) { - this.key = key; - this.#lastAccess = Date.now(); - this.createdAt = this.#lastAccess; - this.next = this.prev = this; - } - - public refresh (): void { - this.#lastAccess = Date.now(); - } - - public get lastAccess (): number { - return this.#lastAccess; - } -} - -// https://en.wikipedia.org/wiki/Cache_replacement_policies#LRU -export class LRUCache { - readonly capacity: number; - - readonly #data = new Map(); - readonly #refs = new Map(); - - #length = 0; - #head: LRUNode; - #tail: LRUNode; - // TTL - readonly #ttl: number; - readonly #ttlInterval: number; - #ttlTimerId: ReturnType | null = null; - - constructor (capacity = DEFAULT_CAPACITY, ttl = 30000, ttlInterval = 15000) { - this.capacity = capacity; - this.#ttl = ttl; - this.#ttlInterval = ttlInterval; - this.#head = this.#tail = new LRUNode(''); - - // make sure the interval is not longer than the ttl - if (this.#ttlInterval > this.#ttl) { - this.#ttlInterval = this.#ttl; - } - } - - get ttl (): number { - return this.#ttl; - } - - get ttlInterval (): number { - return this.#ttlInterval; - } - - get length (): number { - return this.#length; - } - - get lengthData (): number { - return this.#data.size; - } - - get lengthRefs (): number { - return this.#refs.size; - } - - entries (): [string, unknown][] { - const keys = this.keys(); - const count = keys.length; - const entries = new Array<[string, unknown]>(count); - - for (let i = 0; i < count; i++) { - const key = keys[i]; - - entries[i] = [key, this.#data.get(key)]; - } - - return entries; - } - - keys (): string[] { - const keys: string[] = []; - - if (this.#length) { - let curr = this.#head; - - while (curr !== this.#tail) { - keys.push(curr.key); - curr = curr.next; - } - - keys.push(curr.key); - } - - return keys; - } - - get (key: string): T | null { - const data = this.#data.get(key); - - if (data) { - this.#toHead(key); - - return data as T; - } - - return null; - } - - set (key: string, value: T): void { - if (this.#data.has(key)) { - this.#toHead(key); - } else { - const node = new LRUNode(key); - - this.#refs.set(node.key, node); - - if (this.length === 0) { - this.#head = this.#tail = node; - } else { - this.#head.prev = node; - node.next = this.#head; - this.#head = node; - } - - if (this.#length === this.capacity) { - this.#data.delete(this.#tail.key); - this.#refs.delete(this.#tail.key); - - this.#tail = this.#tail.prev; - this.#tail.next = this.#head; - } else { - this.#length += 1; - } - } - - if (this.#ttl > 0 && !this.#ttlTimerId) { - this.#ttlTimerId = setInterval(() => { - this.#ttlClean(); - }, this.#ttlInterval); - } - - this.#data.set(key, value); - } - - #ttlClean () { - // Find last node to keep - const expires = Date.now() - this.#ttl; - - // traverse map to find the lastAccessed - while (this.#tail.lastAccess && this.#tail.lastAccess < expires && this.#length > 0) { - if (this.#ttlTimerId && this.#length === 0) { - clearInterval(this.#ttlTimerId); - this.#ttlTimerId = null; - this.#head = this.#tail = new LRUNode(''); - } else { - this.#refs.delete(this.#tail.key); - this.#data.delete(this.#tail.key); - this.#length -= 1; - this.#tail = this.#tail.prev; - this.#tail.next = this.#head; - } - } - } - - #toHead (key: string): void { - const ref = this.#refs.get(key); - - if (ref && ref !== this.#head) { - ref.refresh(); - ref.prev.next = ref.next; - ref.next.prev = ref.prev; - ref.next = this.#head; - - this.#head.prev = ref; - this.#head = ref; - } - } - - // eslint-disable-next-line @typescript-eslint/require-await - public async clearInterval (): Promise { - if (this.#ttlTimerId) { - clearInterval(this.#ttlTimerId); - this.#ttlTimerId = null; - } - } -} diff --git a/packages/rpc-provider/src/bundle.ts b/packages/rpc-provider/src/bundle.ts index 06b0acaf1aaf..8a41c307b73d 100644 --- a/packages/rpc-provider/src/bundle.ts +++ b/packages/rpc-provider/src/bundle.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 export { HttpProvider } from './http/index.js'; +export { DEFAULT_CAPACITY, LRUCache } from './lru.js'; export { packageInfo } from './packageInfo.js'; export { ScProvider } from './substrate-connect/index.js'; export { WsProvider } from './ws/index.js'; From 2c11b7804bc149410acb62ebf988d47b65858763 Mon Sep 17 00:00:00 2001 From: Filippo Vecchiato Date: Wed, 16 Oct 2024 09:20:16 +0200 Subject: [PATCH 10/11] fixes to stats --- packages/rpc-core/src/bundle.ts | 3 +-- packages/rpc-provider/src/lru.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/rpc-core/src/bundle.ts b/packages/rpc-core/src/bundle.ts index 7a99908b054b..2c4078828a3f 100644 --- a/packages/rpc-core/src/bundle.ts +++ b/packages/rpc-core/src/bundle.ts @@ -96,7 +96,6 @@ export class RpcCore { readonly #registryDefault: Registry; readonly #storageCache: LRUCache; #storageCacheHits = 0; - #storageCacheSize = 0; #getBlockRegistry?: (blockHash: Uint8Array) => Promise<{ registry: Registry }>; #getBlockHash?: (blockNumber: AnyNumber) => Promise; @@ -163,7 +162,7 @@ export class RpcCore { ...stats, core: { cacheHits: this.#storageCacheHits, - cacheSize: this.#storageCacheSize + cacheSize: this.#storageCache.length, } } : undefined; diff --git a/packages/rpc-provider/src/lru.ts b/packages/rpc-provider/src/lru.ts index 9483b718b4f3..2a5cc3cdbf40 100644 --- a/packages/rpc-provider/src/lru.ts +++ b/packages/rpc-provider/src/lru.ts @@ -41,7 +41,7 @@ export class LRUCache { #length = 0; #head: LRUNode; #tail: LRUNode; - // TTL + readonly #ttl: number; readonly #ttlInterval: number; #ttlTimerId: ReturnType | null = null; From a6e1a8582ca74236715c2b9b804ba6c24c28d086 Mon Sep 17 00:00:00 2001 From: Filippo Vecchiato Date: Wed, 16 Oct 2024 09:22:55 +0200 Subject: [PATCH 11/11] fixes to lint --- packages/rpc-core/src/bundle.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rpc-core/src/bundle.ts b/packages/rpc-core/src/bundle.ts index 2c4078828a3f..6c86702666f7 100644 --- a/packages/rpc-core/src/bundle.ts +++ b/packages/rpc-core/src/bundle.ts @@ -162,7 +162,7 @@ export class RpcCore { ...stats, core: { cacheHits: this.#storageCacheHits, - cacheSize: this.#storageCache.length, + cacheSize: this.#storageCache.length } } : undefined;