Skip to content

Commit b6bdd69

Browse files
authored
core/filtermaps: fix deadlock in filtermap callback (#31708)
This PR fixes a deadlock situation is deleteTailEpoch that might arise when range delete is running in iterator based fallback mode (either using leveldb database or the hashdb state storage scheme). In this case a stopCb callback is called periodically that does check events, including matcher sync requests, in which case it tries to acquire indexLock for read access, while deleteTailEpoch already held it for write access. This pull request removes the indexLock acquiring in `FilterMapsMatcherBackend.synced` as this function is only called in the indexLoop. Fixes #31700
1 parent b62756d commit b6bdd69

File tree

3 files changed

+50
-24
lines changed

3 files changed

+50
-24
lines changed

core/filtermaps/filtermaps.go

+33-17
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,17 @@ type FilterMaps struct {
8585
// fields written by the indexer and read by matcher backend. Indexer can
8686
// read them without a lock and write them under indexLock write lock.
8787
// Matcher backend can read them under indexLock read lock.
88-
indexLock sync.RWMutex
89-
indexedRange filterMapsRange
90-
cleanedEpochsBefore uint32 // all unindexed data cleaned before this point
91-
indexedView *ChainView // always consistent with the log index
92-
hasTempRange bool
88+
indexLock sync.RWMutex
89+
indexedRange filterMapsRange
90+
indexedView *ChainView // always consistent with the log index
91+
hasTempRange bool
92+
93+
// cleanedEpochsBefore indicates that all unindexed data before this point
94+
// has been cleaned.
95+
//
96+
// This field is only accessed and modified within tryUnindexTail, so no
97+
// explicit locking is required.
98+
cleanedEpochsBefore uint32
9399

94100
// also accessed by indexer and matcher backend but no locking needed.
95101
filterMapCache *lru.Cache[uint32, filterMap]
@@ -248,15 +254,16 @@ func NewFilterMaps(db ethdb.KeyValueStore, initView *ChainView, historyCutoff, f
248254
},
249255
// deleting last unindexed epoch might have been interrupted by shutdown
250256
cleanedEpochsBefore: max(rs.MapsFirst>>params.logMapsPerEpoch, 1) - 1,
251-
historyCutoff: historyCutoff,
252-
finalBlock: finalBlock,
253-
matcherSyncCh: make(chan *FilterMapsMatcherBackend),
254-
matchers: make(map[*FilterMapsMatcherBackend]struct{}),
255-
filterMapCache: lru.NewCache[uint32, filterMap](cachedFilterMaps),
256-
lastBlockCache: lru.NewCache[uint32, lastBlockOfMap](cachedLastBlocks),
257-
lvPointerCache: lru.NewCache[uint64, uint64](cachedLvPointers),
258-
baseRowsCache: lru.NewCache[uint64, [][]uint32](cachedBaseRows),
259-
renderSnapshots: lru.NewCache[uint64, *renderedMap](cachedRenderSnapshots),
257+
258+
historyCutoff: historyCutoff,
259+
finalBlock: finalBlock,
260+
matcherSyncCh: make(chan *FilterMapsMatcherBackend),
261+
matchers: make(map[*FilterMapsMatcherBackend]struct{}),
262+
filterMapCache: lru.NewCache[uint32, filterMap](cachedFilterMaps),
263+
lastBlockCache: lru.NewCache[uint32, lastBlockOfMap](cachedLastBlocks),
264+
lvPointerCache: lru.NewCache[uint64, uint64](cachedLvPointers),
265+
baseRowsCache: lru.NewCache[uint64, [][]uint32](cachedBaseRows),
266+
renderSnapshots: lru.NewCache[uint64, *renderedMap](cachedRenderSnapshots),
260267
}
261268

262269
// Set initial indexer target.
@@ -444,6 +451,7 @@ func (f *FilterMaps) safeDeleteWithLogs(deleteFn func(db ethdb.KeyValueStore, ha
444451

445452
// setRange updates the indexed chain view and covered range and also adds the
446453
// changes to the given batch.
454+
//
447455
// Note that this function assumes that the index write lock is being held.
448456
func (f *FilterMaps) setRange(batch ethdb.KeyValueWriter, newView *ChainView, newRange filterMapsRange, isTempRange bool) {
449457
f.indexedView = newView
@@ -477,6 +485,7 @@ func (f *FilterMaps) setRange(batch ethdb.KeyValueWriter, newView *ChainView, ne
477485
// Note that this function assumes that the log index structure is consistent
478486
// with the canonical chain at the point where the given log value index points.
479487
// If this is not the case then an invalid result or an error may be returned.
488+
//
480489
// Note that this function assumes that the indexer read lock is being held when
481490
// called from outside the indexerLoop goroutine.
482491
func (f *FilterMaps) getLogByLvIndex(lvIndex uint64) (*types.Log, error) {
@@ -655,6 +664,7 @@ func (f *FilterMaps) mapRowIndex(mapIndex, rowIndex uint32) uint64 {
655664
// getBlockLvPointer returns the starting log value index where the log values
656665
// generated by the given block are located. If blockNumber is beyond the current
657666
// head then the first unoccupied log value index is returned.
667+
//
658668
// Note that this function assumes that the indexer read lock is being held when
659669
// called from outside the indexerLoop goroutine.
660670
func (f *FilterMaps) getBlockLvPointer(blockNumber uint64) (uint64, error) {
@@ -762,7 +772,7 @@ func (f *FilterMaps) deleteTailEpoch(epoch uint32) (bool, error) {
762772
return false, errors.New("invalid tail epoch number")
763773
}
764774
// remove index data
765-
if err := f.safeDeleteWithLogs(func(db ethdb.KeyValueStore, hashScheme bool, stopCb func(bool) bool) error {
775+
deleteFn := func(db ethdb.KeyValueStore, hashScheme bool, stopCb func(bool) bool) error {
766776
first := f.mapRowIndex(firstMap, 0)
767777
count := f.mapRowIndex(firstMap+f.mapsPerEpoch, 0) - first
768778
if err := rawdb.DeleteFilterMapRows(f.db, common.NewRange(first, count), hashScheme, stopCb); err != nil {
@@ -786,10 +796,13 @@ func (f *FilterMaps) deleteTailEpoch(epoch uint32) (bool, error) {
786796
f.lvPointerCache.Remove(blockNumber)
787797
}
788798
return nil
789-
}, fmt.Sprintf("Deleting tail epoch #%d", epoch), func() bool {
799+
}
800+
action := fmt.Sprintf("Deleting tail epoch #%d", epoch)
801+
stopFn := func() bool {
790802
f.processEvents()
791803
return f.stop || !f.targetHeadIndexed()
792-
}); err == nil {
804+
}
805+
if err := f.safeDeleteWithLogs(deleteFn, action, stopFn); err == nil {
793806
// everything removed; mark as cleaned and report success
794807
if f.cleanedEpochsBefore == epoch {
795808
f.cleanedEpochsBefore = epoch + 1
@@ -808,6 +821,9 @@ func (f *FilterMaps) deleteTailEpoch(epoch uint32) (bool, error) {
808821
}
809822

810823
// exportCheckpoints exports epoch checkpoints in the format used by checkpoints.go.
824+
//
825+
// Note: acquiring the indexLock read lock is unnecessary here, as this function
826+
// is always called within the indexLoop.
811827
func (f *FilterMaps) exportCheckpoints() {
812828
finalLvPtr, err := f.getBlockLvPointer(f.finalBlock + 1)
813829
if err != nil {

core/filtermaps/indexer.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ func (f *FilterMaps) indexerLoop() {
4343
log.Info("Started log indexer")
4444

4545
for !f.stop {
46+
// Note: acquiring the indexLock read lock is unnecessary here,
47+
// as the `indexedRange` is accessed within the indexerLoop.
4648
if !f.indexedRange.initialized {
4749
if f.targetView.HeadNumber() == 0 {
4850
// initialize when chain head is available
@@ -105,7 +107,7 @@ type targetUpdate struct {
105107
historyCutoff, finalBlock uint64
106108
}
107109

108-
// SetTargetView sets a new target chain view for the indexer to render.
110+
// SetTarget sets a new target chain view for the indexer to render.
109111
// Note that SetTargetView never blocks.
110112
func (f *FilterMaps) SetTarget(targetView *ChainView, historyCutoff, finalBlock uint64) {
111113
if targetView == nil {
@@ -178,6 +180,8 @@ func (f *FilterMaps) processSingleEvent(blocking bool) bool {
178180
if f.stop {
179181
return false
180182
}
183+
// Note: acquiring the indexLock read lock is unnecessary here,
184+
// as this function is always called within the indexLoop.
181185
if !f.hasTempRange {
182186
for _, mb := range f.matcherSyncRequests {
183187
mb.synced()

core/filtermaps/matcher_backend.go

+12-6
Original file line numberDiff line numberDiff line change
@@ -111,17 +111,17 @@ func (fm *FilterMapsMatcherBackend) GetLogByLvIndex(ctx context.Context, lvIndex
111111
// synced signals to the matcher that has triggered a synchronisation that it
112112
// has been finished and the log index is consistent with the chain head passed
113113
// as a parameter.
114+
//
114115
// Note that if the log index head was far behind the chain head then it might not
115116
// be synced up to the given head in a single step. Still, the latest chain head
116117
// should be passed as a parameter and the existing log index should be consistent
117118
// with that chain.
119+
//
120+
// Note: acquiring the indexLock read lock is unnecessary here, as this function
121+
// is always called within the indexLoop.
118122
func (fm *FilterMapsMatcherBackend) synced() {
119-
fm.f.indexLock.RLock()
120123
fm.f.matchersLock.Lock()
121-
defer func() {
122-
fm.f.matchersLock.Unlock()
123-
fm.f.indexLock.RUnlock()
124-
}()
124+
defer fm.f.matchersLock.Unlock()
125125

126126
indexedBlocks := fm.f.indexedRange.blocks
127127
if !fm.f.indexedRange.headIndexed && !indexedBlocks.IsEmpty() {
@@ -154,6 +154,8 @@ func (fm *FilterMapsMatcherBackend) SyncLogIndex(ctx context.Context) (SyncRange
154154
case <-ctx.Done():
155155
return SyncRange{}, ctx.Err()
156156
case <-fm.f.disabledCh:
157+
// Note: acquiring the indexLock read lock is unnecessary here,
158+
// as the indexer has already been terminated.
157159
return SyncRange{IndexedView: fm.f.indexedView}, nil
158160
}
159161
select {
@@ -162,6 +164,8 @@ func (fm *FilterMapsMatcherBackend) SyncLogIndex(ctx context.Context) (SyncRange
162164
case <-ctx.Done():
163165
return SyncRange{}, ctx.Err()
164166
case <-fm.f.disabledCh:
167+
// Note: acquiring the indexLock read lock is unnecessary here,
168+
// as the indexer has already been terminated.
165169
return SyncRange{IndexedView: fm.f.indexedView}, nil
166170
}
167171
}
@@ -170,7 +174,9 @@ func (fm *FilterMapsMatcherBackend) SyncLogIndex(ctx context.Context) (SyncRange
170174
// valid range with the current indexed range. This function should be called
171175
// whenever a part of the log index has been removed, before adding new blocks
172176
// to it.
173-
// Note that this function assumes that the index read lock is being held.
177+
//
178+
// Note: acquiring the indexLock read lock is unnecessary here, as this function
179+
// is always called within the indexLoop.
174180
func (f *FilterMaps) updateMatchersValidRange() {
175181
f.matchersLock.Lock()
176182
defer f.matchersLock.Unlock()

0 commit comments

Comments
 (0)