Skip to content

Commit

Permalink
(nonce) refactor nonce delta reseting
Browse files Browse the repository at this point in the history
  • Loading branch information
joewagner committed Feb 29, 2024
1 parent ee53654 commit 51fffa1
Show file tree
Hide file tree
Showing 3 changed files with 49 additions and 39 deletions.
62 changes: 36 additions & 26 deletions packages/nonce/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,13 @@ export class NonceManager extends ethers.Signer {
readonly memStore = redis;

_lock: string | undefined;
_initialPromise: Promise<number> | null;

constructor(signer: ethers.Signer) {
super();
if (typeof signer.provider === "undefined") {
throw new Error("NonceManager requires a provider at instantiation");
}

this._initialPromise = null;
this.signer = signer;
this.provider = signer.provider;
}
Expand All @@ -57,21 +55,16 @@ export class NonceManager extends ethers.Signer {
blockTag?: ethers.providers.BlockTag,
): Promise<number> {
if (blockTag === "pending") {
if (!this._initialPromise) {
this._initialPromise = this.signer.getTransactionCount("pending");
}

await this._acquireLock();

const currentCount = await this.signer.getTransactionCount("pending");
// this returns null if the key doesn't exist
const deltaCount = await this.memStore.get(
`delta:${await this.getAddress()}`,
);
await this._releaseLock();

// TODO: this `_initialPromise` may no longer serve any purpose. It's
// here because that is how ethers implements NonceManager.
const initial = await this._initialPromise;
return initial + (typeof deltaCount === "number" ? deltaCount : 0);
return currentCount + (typeof deltaCount === "number" ? deltaCount : 0);
}

return await this.signer.getTransactionCount(blockTag);
Expand All @@ -80,13 +73,8 @@ export class NonceManager extends ethers.Signer {
async setTransactionCount(
transactionCount: ethers.BigNumberish | Promise<ethers.BigNumberish>,
): Promise<void> {
this._initialPromise = Promise.resolve(transactionCount).then((nonce) => {
return ethers.BigNumber.from(nonce).toNumber();
});

// aquire the lock that protects all nonce managers using the same redis db
await this._acquireLock();
await this.memStore.set(`delta:${await this.getAddress()}`, 0);
await this._resetDelta();
await this._releaseLock();
}

Expand Down Expand Up @@ -124,6 +112,14 @@ export class NonceManager extends ethers.Signer {
}

const tx = await this.signer.sendTransaction(transaction);

this.provider
.getTransactionReceipt(tx.hash)
.then(async () => {
await this._resetDelta();
})
.catch((err) => console.log("Error reseting delta:", err));

return tx;
}

Expand All @@ -145,12 +141,6 @@ export class NonceManager extends ethers.Signer {
}

async _acquireLock() {
// make sure we don't try to aquire multiple times
if (this._lock) {
throw new Error("cannot aquire a lock simultaneously in one instance");
}
this._lock = Math.random().toString();

// If the lock was not acquired, we want to retry using increasing backoff
// combined with "jitter". For a nice overview of the reasoning here, read
// https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter
Expand All @@ -159,20 +149,28 @@ export class NonceManager extends ethers.Signer {
const backoffRate = 50; // ms
const maxWait = 2000; // ms
let trys = 0;
const doTry = async function () {
// NOTE: we need arrow functions to keep context of `this`
const doTry = async () => {
trys += 1;
await new Promise(function (resolve, reject) {
await new Promise((resolve, reject) => {
const wait = Math.random() * Math.min(maxWait, trys * backoffRate);

// `setTimeout`'s function should return void so we don't swallow a
// Promise, hence using `catch` instead of `await`ing.
setTimeout(function () {
setTimeout(() => {
// capture resolve & reject in scope
(async function () {
(async () => {
// make sure we can't aquire simultaneously in this instance
if (this._lock) {
return resolve(await doTry());
}

this._lock = Math.random().toString();
// returns null or "OK"
const res = await acquire();

if (res === null) {
this._lock = undefined;
return resolve(await doTry());
}

Expand Down Expand Up @@ -203,4 +201,16 @@ export class NonceManager extends ethers.Signer {
);
this._lock = undefined;
}

async _resetDelta() {
await this.memStore.eval(
`if redis.call("get",KEYS[1]) ~= ARGV[1] then
return redis.call("set",KEYS[1], 0)
else
return 0
end`,
[`delta:${await this.getAddress()}`],
[0],
);
}
}
2 changes: 1 addition & 1 deletion packages/nonce/test/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ helpers.overrideDefaults("localhost", { baseUrl: TEST_VALIDATOR_URL });
const lt = new LocalTableland({
validator: path.resolve(_dirname, "validator"),
registryPort: TEST_REGISTRY_PORT,
silent: true,
silent: false,
});

before(async function () {
Expand Down
24 changes: 12 additions & 12 deletions packages/web/tables_421614.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"migrations": "migrations_421614_453",
"deployments": "deployments_421614_454",
"environments": "environments_421614_455",
"project_tables": "project_tables_421614_456",
"projects": "projects_421614_457",
"tables": "tables_421614_458",
"team_invites": "team_invites_421614_459",
"team_memberships": "team_memberships_421614_460",
"team_projects": "team_projects_421614_461",
"teams": "teams_421614_462",
"users": "users_421614_463"
}
"migrations": "migrations_421614_453",
"deployments": "deployments_421614_454",
"environments": "environments_421614_455",
"project_tables": "project_tables_421614_456",
"projects": "projects_421614_457",
"tables": "tables_421614_458",
"team_invites": "team_invites_421614_459",
"team_memberships": "team_memberships_421614_460",
"team_projects": "team_projects_421614_461",
"teams": "teams_421614_462",
"users": "users_421614_463"
}

0 comments on commit 51fffa1

Please sign in to comment.