-
Notifications
You must be signed in to change notification settings - Fork 717
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
CCQ/Node/EVM: Cache timestamps (#3491)
* NODE/EVM: Cache timestamps * Code review rework * Code review rework
- Loading branch information
1 parent
0a89e23
commit 3ba2a46
Showing
14 changed files
with
641 additions
and
17 deletions.
There are no files selected for viewing
This file contains 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 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 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 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,161 @@ | ||
package evm | ||
|
||
import ( | ||
"sort" | ||
"sync" | ||
|
||
"go.uber.org/zap" | ||
) | ||
|
||
const ( | ||
BTS_MAX_BLOCKS = 10000 | ||
) | ||
|
||
type ( | ||
BlocksByTimestamp struct { | ||
// cache is ordered by timestamp, blockNum. There may be multiple entries for the same timestamp, but not the same block. | ||
cache Blocks | ||
|
||
// maxCacheSize is used to trim the cache. | ||
maxCacheSize int | ||
|
||
// mutex is used to protect the cache. | ||
mutex sync.Mutex | ||
} | ||
|
||
Blocks []Block | ||
|
||
Block struct { | ||
Timestamp uint64 | ||
BlockNum uint64 | ||
} | ||
) | ||
|
||
// NewBlocksByTimestamp creates an empty cache of blocks by timestamp. | ||
func NewBlocksByTimestamp(maxCacheSize int) *BlocksByTimestamp { | ||
return &BlocksByTimestamp{ | ||
cache: Blocks{}, | ||
maxCacheSize: maxCacheSize, | ||
} | ||
} | ||
|
||
// AddLatest adds a block to the end of the cache. This is meant to be used in the normal scenario when a new latest block is received. If the specified | ||
// timestamp or block number is less than the latest in the cache (most likely a rollback), the cache will be truncated and the new value inserted. | ||
func (bts *BlocksByTimestamp) AddLatest(logger *zap.Logger, timestamp uint64, blockNum uint64) { | ||
bts.mutex.Lock() | ||
defer bts.mutex.Unlock() | ||
l := len(bts.cache) | ||
if l > 0 && (blockNum <= bts.cache[l-1].BlockNum || timestamp < bts.cache[l-1].Timestamp) { | ||
// The cache is in order of both timestamp and block number. Search backwards until we find the entry where the block number is less than the one | ||
// passed in and the timestamp is less than or equal to the one passed in. We then truncate everything after that before adding the new one. | ||
idx := l - 1 | ||
for ; idx >= 0; idx-- { | ||
if bts.cache[idx].BlockNum < blockNum && bts.cache[idx].Timestamp <= timestamp { | ||
break | ||
} | ||
} | ||
|
||
logger.Warn("rollback detected in timestamp cache", | ||
zap.Uint64("oldLatestBlockNum", bts.cache[l-1].BlockNum), | ||
zap.Uint64("oldLatestTimestamp", bts.cache[l-1].Timestamp), | ||
zap.Uint64("newLatestBlockNum", blockNum), | ||
zap.Uint64("newLatestTimestamp", timestamp), | ||
) | ||
bts.cache = bts.cache[:idx+1] | ||
} | ||
|
||
bts.cache = append(bts.cache, Block{Timestamp: timestamp, BlockNum: blockNum}) | ||
|
||
if len(bts.cache) > bts.maxCacheSize { | ||
bts.cache = bts.cache[1:] | ||
} | ||
logger.Debug("cache updated", zap.Int("len", len(bts.cache)), zap.Uint64("lastTimestamp", timestamp), zap.Uint64("lastBlockNum", blockNum)) | ||
} | ||
|
||
// AddBatch adds a batch of blocks to the cache. This is meant to be used for backfilling the cache. It makes sure there are no duplicate blocks and regenerates the cache in the correct order by timestamp. | ||
func (bts *BlocksByTimestamp) AddBatch(blocks Blocks) { | ||
bts.mutex.Lock() | ||
defer bts.mutex.Unlock() | ||
|
||
// First build a map of all the existing blocks so we can avoid duplicates. | ||
blockMap := make(map[uint64]uint64) | ||
for _, block := range bts.cache { | ||
blockMap[block.BlockNum] = block.Timestamp | ||
} | ||
|
||
// Now add the new blocks to the map, overwriting any duplicates. (Maybe there was a reorg. . .) | ||
for _, block := range blocks { | ||
blockMap[block.BlockNum] = block.Timestamp | ||
} | ||
|
||
// Now put everything into the cache in random order. | ||
cache := Blocks{} | ||
for blockNum, timestamp := range blockMap { | ||
cache = append(cache, Block{Timestamp: timestamp, BlockNum: blockNum}) | ||
} | ||
|
||
// Sort the cache into timestamp order. | ||
sort.SliceStable(cache, func(i, j int) bool { | ||
return cache[i].Cmp(cache[j]) < 0 | ||
}) | ||
|
||
if len(cache) > bts.maxCacheSize { | ||
// Trim the cache. | ||
trimIdx := len(cache) - bts.maxCacheSize | ||
cache = cache[trimIdx:] | ||
} | ||
|
||
bts.cache = cache | ||
} | ||
|
||
// LookUp searches the cache for the specified timestamp and returns the blocks surrounding that timestamp. It also returns true if the results are complete or false if they are not. | ||
// The following rules apply: | ||
// - If timestamp is less than the first timestamp in the cache, it returns (0, <theFirstBlockInTheCache>, false) | ||
// - If timestamp is greater than or equal to the last timestamp in the cache, it returns (<theLastBlockInTheCache>, 0, false) | ||
// - If timestamp exactly matches one in the cache, it returns (<theLastBlockForThatTimestamp>, <theFirstBlockForTheNextTimestamp>, true) | ||
// - If timestamp is not in the cache, but there are blocks around it, it returns (<theLastBlockForThePreviousTimestamp>, <theFirstBlockForTheNextTimestamp>, false) | ||
func (bts *BlocksByTimestamp) LookUp(timestamp uint64) (uint64, uint64, bool) { | ||
bts.mutex.Lock() | ||
defer bts.mutex.Unlock() | ||
|
||
if len(bts.cache) == 0 { // Empty cache. | ||
return 0, 0, false | ||
} | ||
|
||
if timestamp < bts.cache[0].Timestamp { // Before the start of the cache. | ||
return 0, bts.cache[0].BlockNum, false | ||
} | ||
|
||
if timestamp >= bts.cache[len(bts.cache)-1].Timestamp { // After the end of the cache (including matching the final timestamp). | ||
return bts.cache[len(bts.cache)-1].BlockNum, 0, false | ||
} | ||
|
||
// The search returns the first entry where the timestamp is greater than requested. | ||
idx := bts.cache.SearchForTimestamp(timestamp) | ||
|
||
// If the two blocks are adjacent, then we found what we are looking for. | ||
found := bts.cache[idx-1].BlockNum+1 == bts.cache[idx].BlockNum | ||
return bts.cache[idx-1].BlockNum, bts.cache[idx].BlockNum, found | ||
} | ||
|
||
func (blocks Blocks) SearchForTimestamp(timestamp uint64) int { | ||
return sort.Search(len(blocks), func(i int) bool { return blocks[i].Timestamp > timestamp }) | ||
} | ||
|
||
// Cmp compares two blocks, returning the usual -1, 0, +1. | ||
func (lhs Block) Cmp(rhs Block) int { | ||
if lhs.Timestamp < rhs.Timestamp { | ||
return -1 | ||
} | ||
if lhs.Timestamp > rhs.Timestamp { | ||
return 1 | ||
} | ||
if lhs.BlockNum < rhs.BlockNum { | ||
return -1 | ||
} | ||
if lhs.BlockNum > rhs.BlockNum { | ||
return 1 | ||
} | ||
|
||
return 0 | ||
} |
Oops, something went wrong.