Skip to content

Commit

Permalink
chore: RPC limits & standard plans (#16)
Browse files Browse the repository at this point in the history
* feat: add rpc provider limit

* feat: catch rpc provider rate limit on runtime

* chore: display rpc and duration usage warnings

* tests: new rpc limits

* chore: add nodejs v>16 requirement

* feat: add task creation link on deploy

* chore: update storage wording

* chore: bump version

* chore: bump sdk version
  • Loading branch information
goums authored Feb 3, 2023
1 parent e079bd8 commit 15e0a42
Show file tree
Hide file tree
Showing 12 changed files with 119 additions and 58 deletions.
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
engine-strict=true
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ yarn test src/web3Functions/storage/index.ts --show-logs

You will see your updated key/values:
```
Web3Function Storage updated:
Simulated Web3Function Storage update:
✓ lastBlockNumber: '8321923'
```

Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@gelatonetwork/web3-functions-sdk",
"version": "0.3.1",
"version": "0.4.1",
"description": "Gelato Automate Web3 Functions sdk",
"url": "https://github.com/gelatodigital/web3-functions-sdk",
"main": "dist/lib/index.js",
Expand Down Expand Up @@ -57,6 +57,9 @@
"fetch": "ts-node src/bin/index.ts fetch",
"schema": "ts-node src/bin/index.ts schema"
},
"engines": {
"node": ">=16.0.0"
},
"keywords": [],
"author": "",
"license": "ISC",
Expand Down
28 changes: 19 additions & 9 deletions src/lib/Web3Function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,7 @@ export class Web3Function {
};

try {
const { result, ctxData } = await this._run(
event.data.context
);
const { result, ctxData } = await this._run(event.data.context);

const lastStorageHash = objectHash(storage.storage, {
algorithm: "md5",
Expand Down Expand Up @@ -73,9 +71,7 @@ export class Web3Function {
break;
}
default:
Web3Function._log(
`Unrecognized parent process event: ${event.action}`
);
Web3Function._log(`Unrecognized parent process event: ${event.action}`);
throw new Error(`Unrecognized parent process event: ${event.action}`);
}
}
Expand All @@ -89,9 +85,7 @@ export class Web3Function {
...ctxData.gelatoArgs,
gasPrice: BigNumber.from(ctxData.gelatoArgs.gasPrice),
},
provider: new ethers.providers.StaticJsonRpcProvider(
ctxData.rpcProviderUrl
),
provider: this._initProvider(ctxData.rpcProviderUrl),
userArgs: ctxData.userArgs,
secrets: {
get: async (key: string) => {
Expand Down Expand Up @@ -150,4 +144,20 @@ export class Web3Function {
private static _log(message: string) {
if (Web3Function._debug) console.log(`Web3Function: ${message}`);
}

private _initProvider(
providerUrl: string | undefined
): ethers.providers.StaticJsonRpcProvider {
const provider = new ethers.providers.StaticJsonRpcProvider(providerUrl);
// Listen to response to check for rate limit error
provider.on("debug", (data) => {
if (data.action === "response" && data.error) {
if (/Request limit exceeded/.test(data.error.message)) {
console.error("Web3FunctionError: RPC requests limit exceeded");
this._exit(250);
}
}
});
return provider;
}
}
3 changes: 2 additions & 1 deletion src/lib/binaries/benchmark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ export default async function benchmark() {
const start = performance.now();
const memory = buildRes.schema.memory;
const timeout = buildRes.schema.timeout * 1000;
const options = { runtime, showLogs, memory, timeout };
const rpcLimit = 100;
const options = { runtime, showLogs, memory, timeout, rpcLimit };
const script = buildRes.filePath;
const provider = new ethers.providers.StaticJsonRpcProvider(
process.env.PROVIDER_URL
Expand Down
7 changes: 6 additions & 1 deletion src/lib/binaries/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,10 @@ const web3FunctionSrcPath = process.argv[3] ?? "./src/web3Functions/index.ts";

export default async function deploy() {
const cid = await Web3FunctionBuilder.deploy(web3FunctionSrcPath);
console.log(` ${OK} Web3Function deployed to ipfs. CID: ${cid}`);
console.log(` ${OK} Web3Function deployed to ipfs.`);
console.log(` ${OK} CID: ${cid}`);
console.log(
`\nTo create a task that runs your Web3 Function every minute, visit:`
);
console.log(`> https://beta.app.gelato.network/new-task?cid=${cid}`);
}
45 changes: 34 additions & 11 deletions src/lib/binaries/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,13 @@ if (process.argv.length > 2) {
});
}

const STD_TIMEOUT = 10;
const STD_RPC_LIMIT = 10;
const MAX_RPC_LIMIT = 100;

const OK = colors.green("✓");
const KO = colors.red("✗");
const WARN = colors.yellow("⚠");
export default async function test() {
// Build Web3Function
console.log(`Web3Function building...`);
Expand Down Expand Up @@ -82,7 +87,8 @@ export default async function test() {
const runner = new Web3FunctionRunner(debug);
const memory = buildRes.schema.memory;
const timeout = buildRes.schema.timeout * 1000;
const options = { runtime, showLogs, memory, timeout };
const rpcLimit = MAX_RPC_LIMIT;
const options = { runtime, showLogs, memory, rpcLimit, timeout };
const script = buildRes.filePath;
const provider = new ethers.providers.StaticJsonRpcProvider(
process.env.PROVIDER_URL
Expand Down Expand Up @@ -111,7 +117,7 @@ export default async function test() {

// Show storage update
if (res.storage?.state === "updated") {
console.log(`\nWeb3Function Storage updated:`);
console.log(`\nSimulated Web3Function Storage update:`);
Object.entries(res.storage.storage).forEach(([key, value]) =>
console.log(` ${OK} ${key}: ${colors.green(`'${value}'`)}`)
);
Expand All @@ -127,15 +133,32 @@ export default async function test() {

// Show runtime stats
console.log(`\nWeb3Function Runtime stats:`);
const durationStatus = res.duration < 0.9 * buildRes.schema.timeout ? OK : KO;
console.log(` ${durationStatus} Duration: ${res.duration.toFixed(2)}s`);
if (res.duration > 0.9 * buildRes.schema.timeout) {
console.log(` ${KO} Duration: ${res.duration.toFixed(2)}s`);
} else if (res.duration > STD_TIMEOUT) {
console.log(
` ${WARN} Duration: ${res.duration.toFixed(
2
)}s (Runtime is above Standard plan limit: ${STD_TIMEOUT}s!)`
);
} else {
console.log(` ${OK} Duration: ${res.duration.toFixed(2)}s`);
}
const memoryStatus = res.memory < 0.9 * memory ? OK : KO;
console.log(` ${memoryStatus} Memory: ${res.memory.toFixed(2)}mb`);
const rpcCallsStatus =
res.rpcCalls.throttled > 0.1 * res.rpcCalls.total ? KO : OK;
console.log(
` ${rpcCallsStatus} Rpc calls: ${res.rpcCalls.total} ${
res.rpcCalls.throttled > 0 ? `(${res.rpcCalls.throttled} throttled)` : ""
}`
);
if (res.rpcCalls.throttled > 0) {
console.log(
` ${KO} Rpc calls: ${
res.rpcCalls.total
} ${`(${res.rpcCalls.throttled} throttled - Please reduce your rpc usage!)`}`
);
} else if (res.rpcCalls.total > STD_RPC_LIMIT) {
console.log(
` ${WARN} Rpc calls: ${
res.rpcCalls.total
} ${`(RPC usage is above Standard plan limit: ${STD_RPC_LIMIT}!)`}`
);
} else {
console.log(` ${OK} Rpc calls: ${res.rpcCalls.total}`);
}
}
28 changes: 4 additions & 24 deletions src/lib/provider/Web3FunctionProxyProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,6 @@ import crypto from "crypto";
import { ethers } from "ethers";
import { ethErrors, serializeError } from "eth-rpc-errors";

const delay = (t: number) => new Promise((resolve) => setTimeout(resolve, t));

const SOFT_LIMIT = 5; // 5 rpc calls / second
const HARD_LIMIT = 10; // 10 rpc calls / second
const MAX_TIME_DIFFERENCE = 1_000;

export class Web3FunctionProxyProvider {
private _debug: boolean;
private _host: string;
Expand All @@ -23,45 +17,31 @@ export class Web3FunctionProxyProvider {
private _isStopped = false;
private _nbRpcCalls = 0;
private _nbThrottledRpcCalls = 0;
private _throttlingRequests = 0;
private _lastIntervalStarted = new Date();
private _limit: number;
private _whitelistedMethods = ["eth_chainId", "net_version"];

constructor(
host: string,
port: number,
provider: ethers.providers.StaticJsonRpcProvider,
limit: number,
debug = true
) {
this._host = host;
this._port = port;
this._provider = provider;
this._debug = debug;
this._limit = limit;
this._mountPath = crypto.randomUUID();
this._proxyUrl = `${this._host}:${this._port}/${this._mountPath}/`;
}

protected async _checkRateLimit() {
const now = new Date();
const timeSinceLastIntervalStarted =
now.getTime() - this._lastIntervalStarted.getTime();
if (timeSinceLastIntervalStarted > MAX_TIME_DIFFERENCE) {
this._lastIntervalStarted = now;
this._throttlingRequests = 1;
} else {
this._throttlingRequests++;
}
this._log(`throttlingRequests: ${this._throttlingRequests}`);

if (this._throttlingRequests > HARD_LIMIT) {
if (this._nbRpcCalls > this._limit) {
// Reject requests when reaching hard limit
this._log(`Too many requests, blocking rpc call`);
this._nbThrottledRpcCalls++;
throw ethErrors.rpc.limitExceeded();
} else if (this._throttlingRequests > SOFT_LIMIT) {
// Slow down requests when reaching soft limit
this._log(`Too many requests, slowing down`);
await delay(Math.floor(MAX_TIME_DIFFERENCE / 2));
}
}

Expand Down
9 changes: 7 additions & 2 deletions src/lib/runtime/Web3FunctionRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ export class Web3FunctionRunner {
: "http://host.docker.internal",
proxyProviderPort,
provider,
options.rpcLimit,
this._debug
);
await this._proxyProvider.start();
Expand Down Expand Up @@ -288,10 +289,14 @@ export class Web3FunctionRunner {
if (!isResolved)
if (signal === 0) {
reject(new Error(`Web3Function exited without returning result`));
} else {
} else if (signal === 250) {
reject(
new Error(`Web3Function sandbox exited with code=${signal}`)
new Error(
`Web3Function exited with code=${signal} (RPC requests limit exceeded)`
)
);
} else {
reject(new Error(`Web3Function exited with code=${signal}`));
}
});
});
Expand Down
1 change: 1 addition & 0 deletions src/lib/runtime/types/Web3FunctionRunnerPayload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Web3FunctionContextData } from "../../types/Web3FunctionContext";
export interface Web3FunctionRunnerOptions {
memory: number;
timeout: number;
rpcLimit: number;
runtime: "thread" | "docker";
showLogs: boolean;
serverPort?: number;
Expand Down
17 changes: 9 additions & 8 deletions src/web3Functions/fails/rpc-provider-limit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,15 @@ Web3Function.onRun(async (context: Web3FunctionContext) => {
assert.match(value.toString(), /\d+/);

// Test hard rate limits
try {
const promises: Promise<any>[] = [];
for (let i = 0; i < 100; i++) promises.push(oracle.lastUpdated());
const values = await Promise.all(promises);
console.log(`Call results:`, values);
} catch (err) {
failure = err.message;
console.log("Throttling RPC calls error:", err.message);
for (let j = 0; j < 20; j++) {
try {
const promises: Promise<any>[] = [];
for (let i = 0; i < 10; i++) promises.push(oracle.lastUpdated());
const values = await Promise.all(promises);
} catch (err) {
failure = err.message;
console.log("Throttling RPC calls error:", err.message);
}
}
assert.match(failure, /\"code\":-32005/);
assert.match(failure, /Request limit exceeded/);
Expand Down
31 changes: 31 additions & 0 deletions src/web3Functions/fails/standard-plan-warning.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {
Web3Function,
Web3FunctionContext,
} from "@gelatonetwork/web3-functions-sdk";
import ky from "ky";
import { Contract, ethers } from "ethers";
const delay = (time: number) => new Promise((res) => setTimeout(res, time));

const ORACLE_ABI = [
"function lastUpdated() external view returns(uint256)",
"function updatePrice(uint256)",
];

Web3Function.onRun(async (context: Web3FunctionContext) => {
const { provider } = context;

// Test soft rate limits
const oracleAddress = "0x6a3c82330164822A8a39C7C0224D20DB35DD030a";
const oracle = new Contract(oracleAddress, ORACLE_ABI, provider);
try {
const promises: Promise<any>[] = [];
for (let i = 0; i < 20; i++) promises.push(oracle.lastUpdated());
await Promise.race(promises);
} catch (err) {
console.log("Throttling RPC calls error:", err.message);
}

await delay(9000);

return { canExec: false, message: "RPC providers tests ok!" };
});

0 comments on commit 15e0a42

Please sign in to comment.