-
Notifications
You must be signed in to change notification settings - Fork 683
Add in-memory cache for forking requests #1248
Changes from 2 commits
af90fb6
f87f511
59e2191
ecc1db6
80a9f4f
a1a71ef
6dde20f
590827a
be5fcae
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,31 +10,32 @@ import { Block } from "@ganache/ethereum-block"; | |
import { Address } from "@ganache/ethereum-address"; | ||
import { Account } from "@ganache/ethereum-utils"; | ||
import BlockManager from "../data-managers/block-manager"; | ||
import { ProviderHandler } from "./handlers/provider-handler"; | ||
|
||
function fetchChainId(fork: Fork) { | ||
return fork | ||
.request<string>("eth_chainId", []) | ||
.then(chainIdHex => parseInt(chainIdHex, 16)); | ||
async function fetchChainId(fork: Fork) { | ||
const chainIdHex = await fork.request<string>("eth_chainId", []); | ||
return parseInt(chainIdHex, 16); | ||
} | ||
function fetchNetworkId(fork: Fork) { | ||
return fork | ||
.request<string>("net_version", []) | ||
.then(networkIdStr => parseInt(networkIdStr, 10)); | ||
async function fetchNetworkId(fork: Fork) { | ||
const networkIdStr = await fork.request<string>("net_version", []); | ||
return parseInt(networkIdStr, 10); | ||
} | ||
function fetchBlockNumber(fork: Fork) { | ||
return fork.request<string>("eth_blockNumber", []); | ||
} | ||
function fetchBlock(fork: Fork, blockNumber: Quantity | Tag.LATEST) { | ||
return fork.request<any>("eth_getBlockByNumber", [blockNumber, true]); | ||
} | ||
function fetchNonce( | ||
async function fetchNonce( | ||
fork: Fork, | ||
address: Address, | ||
blockNumber: Quantity | Tag.LATEST | ||
) { | ||
return fork | ||
.request<string>("eth_getTransactionCount", [address, blockNumber]) | ||
.then(nonce => Quantity.from(nonce)); | ||
const nonce = await fork.request<string>("eth_getTransactionCount", [ | ||
address, | ||
blockNumber | ||
]); | ||
return Quantity.from(nonce); | ||
Comment on lines
+13
to
+38
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
} | ||
|
||
export class Fork { | ||
|
@@ -75,44 +76,10 @@ export class Fork { | |
} | ||
} | ||
} else if (forkingOptions.provider) { | ||
let id = 0; | ||
this.#handler = { | ||
request: <T>(method: string, params: any[]) => { | ||
// format params via JSON stringification because the params might | ||
// be Quantity or Data, which aren't valid as `params` themselves, | ||
// but when JSON stringified they are | ||
const paramCopy = JSON.parse(JSON.stringify(params)); | ||
if (forkingOptions.provider.request) { | ||
return forkingOptions.provider.request({ | ||
method, | ||
params: paramCopy | ||
}) as Promise<T>; | ||
} else if ((forkingOptions.provider as any).send) { | ||
// TODO: remove support for legacy providers | ||
// legacy `.send` | ||
console.warn( | ||
"WARNING: Ganache forking only supports EIP-1193-compliant providers. Legacy support for send is currently enabled, but will be removed in a future version _without_ a breaking change. To remove this warning, switch to an EIP-1193 provider. This error is probably caused by an old version of Web3's HttpProvider (or ganache < v7)" | ||
); | ||
return new Promise<T>((resolve, reject) => { | ||
(forkingOptions.provider as any).send( | ||
{ | ||
id: id++, | ||
jsonrpc: "2.0", | ||
method, | ||
params: paramCopy | ||
}, | ||
(err: Error, response: { result: T }) => { | ||
if (err) return void reject(err); | ||
resolve(response.result); | ||
} | ||
); | ||
}); | ||
} else { | ||
throw new Error("Forking `provider` must be EIP-1193 compatible"); | ||
} | ||
}, | ||
close: () => Promise.resolve() | ||
}; | ||
this.#handler = new ProviderHandler( | ||
options, | ||
this.#abortController.signal | ||
); | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,12 @@ | ||
import { EthereumInternalOptions } from "@ganache/ethereum-options"; | ||
import { JsonRpcError, JsonRpcResponse } from "@ganache/utils"; | ||
import { hasOwn, JsonRpcError } from "@ganache/utils"; | ||
import { AbortSignal } from "abort-controller"; | ||
import { OutgoingHttpHeaders } from "http"; | ||
import RateLimiter from "../rate-limiter/rate-limiter"; | ||
import LRU from "lru-cache"; | ||
import { AbortError, CodedError } from "@ganache/ethereum-utils"; | ||
|
||
const INVALID_RESPONSE = "Invalid response from fork provider: "; | ||
|
||
type Headers = OutgoingHttpHeaders & { authorization?: string }; | ||
|
||
|
@@ -11,12 +15,11 @@ const INVALID_AUTH_ERROR = | |
const WINDOW_SECONDS = 30; | ||
|
||
export class BaseHandler { | ||
static JSONRPC_PREFIX = '{"jsonrpc":"2.0","id":'; | ||
static JSONRPC_PREFIX = '{"jsonrpc":"2.0","id":' as const; | ||
protected id: number = 1; | ||
protected requestCache = new Map< | ||
string, | ||
Promise<JsonRpcError | JsonRpcResponse> | ||
>(); | ||
protected requestCache = new Map<string, Promise<unknown>>(); | ||
protected valueCache: LRU<string, string | Buffer>; | ||
|
||
protected limiter: RateLimiter; | ||
protected headers: Headers; | ||
protected abortSignal: AbortSignal; | ||
|
@@ -33,24 +36,31 @@ export class BaseHandler { | |
abortSignal | ||
); | ||
|
||
const headers: Headers = { | ||
"user-agent": userAgent | ||
}; | ||
if (origin) { | ||
headers["origin"] = origin; | ||
} | ||
this.valueCache = new LRU({ | ||
max: 1_073_741_824, // 1 gigabyte | ||
length: (value, key) => { | ||
return value.length + key.length; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To measure the cache size we measure the string length of the key and string length of the value. it's technically not perfect, but should be close enough for these purposes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Your comment should be part of the code |
||
} | ||
}); | ||
|
||
// we set our own Authentication headers, so username and password must be | ||
// removed from the url. (The values have already been copied to the options) | ||
url.password = url.username = ""; | ||
// we don't header-related things if we are using a provider instead of a url | ||
if (url) { | ||
const headers: Headers = { | ||
"user-agent": userAgent | ||
}; | ||
if (origin) { | ||
headers["origin"] = origin; | ||
} | ||
|
||
BaseHandler.setAuthHeaders(forkingOptions, headers); | ||
BaseHandler.setUserHeaders( | ||
forkingOptions, | ||
headers, | ||
!url.host.endsWith(".infura.io") | ||
); | ||
this.headers = headers; | ||
// we set our own Authentication headers, so username and password must be | ||
// removed from the url. (The values have already been copied to the options) | ||
url.password = url.username = ""; | ||
const isInfura = url.host.endsWith(".infura.io"); | ||
|
||
BaseHandler.setAuthHeaders(forkingOptions, headers); | ||
BaseHandler.setUserHeaders(forkingOptions, headers, !isInfura); | ||
this.headers = headers; | ||
} | ||
} | ||
|
||
/** | ||
|
@@ -122,4 +132,46 @@ export class BaseHandler { | |
} | ||
} | ||
} | ||
|
||
getFromCache<T>(key: string) { | ||
const cachedRequest = this.requestCache.get(key); | ||
if (cachedRequest !== undefined) return cachedRequest as Promise<T>; | ||
|
||
const cachedValue = this.valueCache.get(key); | ||
if (cachedValue !== undefined) return JSON.parse(cachedValue).result as T; | ||
} | ||
|
||
async queueRequest<T>( | ||
key: string, | ||
send: ( | ||
...args: unknown[] | ||
) => Promise<{ | ||
response: { result: any } | { error: { message: string; code: number } }; | ||
raw: string | Buffer; | ||
}> | ||
): Promise<T> { | ||
const cached = this.getFromCache<T>(key); | ||
if (cached !== undefined) return cached; | ||
|
||
const promise = this.limiter.handle(send).then(({ response, raw }) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The I did this because we can't measure the size of a json object for the cache, but we can for a string and Buffer. |
||
if (this.abortSignal.aborted) return Promise.reject(new AbortError()); | ||
|
||
// check for null/undefined, as we can't trust that network responses will | ||
// be well-formed | ||
if (typeof response == "object") { | ||
if (hasOwn(response, "result")) { | ||
// cache non-error responses only | ||
this.valueCache.set(key, raw); | ||
|
||
return response.result as T; | ||
} else if (hasOwn(response, "error") && response.error != null) { | ||
const { error } = response as JsonRpcError; | ||
throw new CodedError(error.message, error.code); | ||
} | ||
} | ||
throw new Error(`${INVALID_RESPONSE}\`${JSON.stringify(response)}\``); | ||
}); | ||
this.requestCache.set(key, promise); | ||
return await promise; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
previously, requesting blocks that exist on the original chain, but not locally, would always just return our lcoal block's "latest" instead of
null
. this fixes it -- probably. :-)