fix(sequencer): deduplicate forced transactions in pool - #3807
Conversation
Signed-off-by: nadeemb53 <nadeemb53@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
Suppressed comments (2)
linea-besu/plugins/linea-sequencer/sequencer/src/main/java/lineth/sequencer/forced/LineaForcedTransactionPool.java:379
recordStatusmutatesstatusCacheandpendingTransactionNumbers, and runs concurrently withaddForcedTransactions(RPC-driven) andprocessForBlock/onBlockAdded(Besu event-driven). Without a shared lock,addForcedTransactionscan observestatusCacheempty and enqueue a tx whilerecordStatusis in-flight, reintroducing duplicates.
To make the deduplication robust, guard status recording with the same lock as addForcedTransactions (for example by synchronizing this method) so the cache+set updates cannot interleave with enqueueing.
statusCache.put(ftx.forcedTransactionNumber(), status);
pendingTransactionNumbers.remove(ftx.forcedTransactionNumber());
inclusionResultCounters.get(result).incrementAndGet();
linea-besu/plugins/linea-sequencer/sequencer/src/main/java/lineth/sequencer/forced/LineaForcedTransactionPool.java:156
- The deduplication relies on
pendingTransactionNumbersandstatusCache, butstatusCachecan be updated concurrently byonBlockAdded/recordStatuswhile this loop is running. That creates an interleaving wherestatusCachebecomes non-empty after thegetIfPresentcheck but beforependingQueue.addLast(tx), allowing a forcedTxNumber that already has a final status recorded to be enqueued again.
Consider guarding the add/check/enqueue sequence with the same lock used when recording statuses (e.g., synchronized on the instance or a dedicated lock) so the pending-set and status-cache checks are atomic w.r.t. final status recording.
This issue also appears on line 377 of the same file.
final long forcedTransactionNumber = tx.forcedTransactionNumber();
if (!pendingTransactionNumbers.add(forcedTransactionNumber)) {
continue;
}
if (statusCache.getIfPresent(forcedTransactionNumber) != null) {
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Signed-off-by: nadeemb53 <nadeemb53@gmail.com>
7f9e939 to
22dae13
Compare
| 7_200; | ||
|
|
||
| private final Deque<ForcedTransaction> pendingQueue = new ConcurrentLinkedDeque<>(); | ||
| private final Set<Long> pendingTransactionNumbers = ConcurrentHashMap.newKeySet(); |
There was a problem hiding this comment.
I feel like the this becomes bloated. What if we use an ConcurrentSkipListMap here instead of the pendingQueue?
There was a problem hiding this comment.
my understanding is that the set keeps the transaction number reserved while the transaction moves from the queue into the status cache. With only a map, removing the entry before recording the status creates a window where a retry can add it again, so we’d need extra locking. That doesn’t end up being simpler.
There was a problem hiding this comment.
What if we avoid this window by reordering the operations? Same as you've done with the pendingTransactionNumbers, removal from the queue after the status is updated
Also, a validation whether a transaction is within the statusCache already with INCLUDED status could also help with the duplicates. And I think we don't care about the resubmission with other, initially rejected statuses
8c6d8ae to
22dae13
Compare
Signed-off-by: nadeemb53 <nadeemb53@gmail.com>
Fixes #3776.
Deduplicates pending and completed forced transactions so coordinator redelivery cannot leave stale queue entries that overwrite inclusion status.