-
Notifications
You must be signed in to change notification settings - Fork 93
feat: PoC replace a tx in the tx pool #4616
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
natanasow
wants to merge
9
commits into
feat/nonce-ordering-with-locks
Choose a base branch
from
poc-replace-tx-in-tx-pool
base: feat/nonce-ordering-with-locks
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+625
−21
Draft
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
5a88a01
feat: create LockService class + relevant interfaces (#4605)
simzzz 046823c
chore: add local lock strategy
natanasow 9f64cd0
chore: fix comment
natanasow 11e6bf5
chore: remove unused var
natanasow 7fc9246
chore: fix comment
natanasow e534446
chore: add test
natanasow c04db62
chore: eslint fix
natanasow 765a06f
chore: add poc
natanasow cc024c3
chore: fix transaction service
natanasow File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
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.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
168 changes: 168 additions & 0 deletions
168
packages/relay/src/lib/services/lockService/LocalLockStrategy.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,168 @@ | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| import { Mutex } from 'async-mutex'; | ||
| import { randomUUID } from 'crypto'; | ||
| import { LRUCache } from 'lru-cache'; | ||
| import { Logger } from 'pino'; | ||
|
|
||
| /** | ||
| * Represents the internal state for a lock associated with a given address. | ||
| */ | ||
| interface LockState { | ||
| mutex: Mutex; | ||
| sessionKey: string | null; | ||
| acquiredAt: number | null; | ||
| maxLockTime: NodeJS.Timeout | null; | ||
| } | ||
|
|
||
| /** | ||
| * Implements a local, in-memory locking strategy. | ||
| * | ||
| * Each unique "address" gets its own mutex to ensure only one session can hold | ||
| * the lock at a time. Locks are auto-expiring and stored in an LRU cache. | ||
| */ | ||
| export class LocalLockStrategy { | ||
| /** | ||
| * Maximum number of lock entries stored in memory. | ||
| * Prevents unbounded memory growth. | ||
| */ | ||
| public static LOCAL_LOCK_MAX_ENTRIES: number = 1_000; // Max 1000 addresses | ||
|
|
||
| /** | ||
| * Time-to-live for each lock entry in the cache (in milliseconds). | ||
| */ | ||
| public static LOCAL_LOCK_TTL: number = 300_000; // 5 minutes | ||
|
|
||
| /** | ||
| * Seconds for auto-release if lock not manually released | ||
| */ | ||
| public static LOCAL_LOCK_MAX_LOCK_TIME: number = 30_000; // 30 secs | ||
|
|
||
| /** | ||
| * LRU cache of lock states, keyed by address. | ||
| */ | ||
| private localLockStates = new LRUCache<string, LockState>({ | ||
| max: LocalLockStrategy.LOCAL_LOCK_MAX_ENTRIES, | ||
| ttl: LocalLockStrategy.LOCAL_LOCK_TTL, | ||
| }); | ||
|
|
||
| /** | ||
| * Logger. | ||
| * | ||
| * @private | ||
| */ | ||
| private readonly logger: Logger; | ||
|
|
||
| /** | ||
| * Creates a new LocalLockStrategy instance. | ||
| * | ||
| * @param logger - The logger | ||
| */ | ||
| constructor(logger: Logger) { | ||
| this.logger = logger; | ||
| } | ||
|
|
||
| /** | ||
| * Acquire a lock for a specific address. | ||
| * Waits until the lock is available (blocking if another session holds it). | ||
| * | ||
| * @param address - The key representing the resource to lock | ||
| * @returns A session key identifying the current lock owner | ||
| */ | ||
| async acquireLock(address: string): Promise<string> { | ||
| const sessionKey = randomUUID(); | ||
| const state = this.getOrCreateState(address); | ||
|
|
||
| // Acquire the mutex (this will block until available) | ||
| await state.mutex.acquire(); | ||
|
|
||
| // Record lock ownership metadata | ||
| state.sessionKey = sessionKey; | ||
| state.acquiredAt = Date.now(); | ||
|
|
||
| // Start a 30-second timer to auto-release if lock not manually released | ||
| state.maxLockTime = setTimeout(() => { | ||
| this.forceReleaseExpiredLock(address, sessionKey); | ||
| }, LocalLockStrategy.LOCAL_LOCK_MAX_LOCK_TIME); | ||
|
|
||
| return sessionKey; | ||
| } | ||
|
|
||
| /** | ||
| * Release a previously acquired lock, if the session key matches the current owner. | ||
| * | ||
| * @param address - The locked resource key | ||
| * @param sessionKey - The session key of the lock holder | ||
| */ | ||
| async releaseLock(address: string, sessionKey: string): Promise<void> { | ||
| const state = this.localLockStates.get(address); | ||
|
|
||
| // Ensure only the lock owner can release | ||
| if (state?.sessionKey !== sessionKey) { | ||
| return; // Not the owner — safely ignore | ||
| } | ||
|
|
||
| // Perform cleanup and release | ||
| await this.doRelease(state); | ||
| } | ||
|
|
||
| /** | ||
| * Retrieve an existing lock state for the given address, or create a new one if it doesn't exist. | ||
| * | ||
| * @param address - Unique identifier for the lock | ||
| * @returns The LockState object associated with the address | ||
| */ | ||
| private getOrCreateState(address: string): LockState { | ||
| if (!this.localLockStates.has(address)) { | ||
| this.localLockStates.set(address, { | ||
| mutex: new Mutex(), | ||
| sessionKey: null, | ||
| acquiredAt: null, | ||
| maxLockTime: null, | ||
| }); | ||
| } | ||
|
|
||
| return this.localLockStates.get(address)!; | ||
| } | ||
|
|
||
| /** | ||
| * Internal helper to perform cleanup and release the mutex. | ||
| * | ||
| * @param state - The LockState instance to reset and release | ||
| */ | ||
| private async doRelease(state: LockState): Promise<void> { | ||
| // Clear timeout first | ||
| clearTimeout(state.maxLockTime!); | ||
|
|
||
| // Reset state | ||
| state.sessionKey = null; | ||
| state.maxLockTime = null; | ||
| state.acquiredAt = null; | ||
|
|
||
| // Release the mutex lock | ||
| state.mutex.release(); | ||
| } | ||
|
|
||
| /** | ||
| * Forcefully release a lock that has exceeded its maximum execution time. | ||
| * Used by the timeout set during `acquireLock`. | ||
| * | ||
| * @param address - The resource key associated with the lock | ||
| * @param sessionKey - The session key to verify ownership before releasing | ||
| */ | ||
| private async forceReleaseExpiredLock(address: string, sessionKey: string): Promise<void> { | ||
| const state = this.localLockStates.get(address); | ||
|
|
||
| // Ensure the session still owns the lock before force-releasing | ||
| if (!state || state.sessionKey !== sessionKey) { | ||
| return; // Already released or lock reassigned | ||
| } | ||
|
|
||
| if (this.logger.isLevelEnabled('debug')) { | ||
| const holdTime = Date.now() - state.acquiredAt!; | ||
| this.logger.debug(`Force releasing expired local lock for address ${address} held for ${holdTime}ms.`); | ||
| } | ||
|
|
||
| await this.doRelease(state); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
FYI: here we should release the lock if validation fails