parent
9df446a23d
commit
720f9a6a02
@ -0,0 +1,167 @@ |
|||||||
|
package state |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"log" |
||||||
|
"sync" |
||||||
|
"sync/atomic" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common" |
||||||
|
"github.com/ethereum/go-ethereum/rlp" |
||||||
|
"github.com/ethereum/go-ethereum/trie" |
||||||
|
"github.com/harmony-one/harmony/internal/utils" |
||||||
|
) |
||||||
|
|
||||||
|
type prefetchJob struct { |
||||||
|
accountAddr []byte |
||||||
|
account *Account |
||||||
|
start, end []byte |
||||||
|
} |
||||||
|
|
||||||
|
// Prefetch If redis is empty, the hit rate will be too low and the synchronization block speed will be slow
|
||||||
|
// this function will parallel load the latest block statedb to redis
|
||||||
|
// this function used by debug or first time to init tikv cluster
|
||||||
|
func (s *DB) Prefetch(parallel int) { |
||||||
|
wg := sync.WaitGroup{} |
||||||
|
|
||||||
|
jobChan := make(chan *prefetchJob, 10000) |
||||||
|
waitWorker := int64(parallel) |
||||||
|
|
||||||
|
// start parallel workers
|
||||||
|
for i := 0; i < parallel; i++ { |
||||||
|
go func() { |
||||||
|
defer wg.Done() |
||||||
|
for job := range jobChan { |
||||||
|
atomic.AddInt64(&waitWorker, -1) |
||||||
|
s.prefetchWorker(job, jobChan) |
||||||
|
atomic.AddInt64(&waitWorker, 1) |
||||||
|
} |
||||||
|
}() |
||||||
|
|
||||||
|
wg.Add(1) |
||||||
|
} |
||||||
|
|
||||||
|
// add first jobs
|
||||||
|
for i := 0; i < 255; i++ { |
||||||
|
start := []byte{byte(i)} |
||||||
|
end := []byte{byte(i + 1)} |
||||||
|
if i == parallel-1 { |
||||||
|
end = nil |
||||||
|
} else if i == 0 { |
||||||
|
start = nil |
||||||
|
} |
||||||
|
|
||||||
|
jobChan <- &prefetchJob{ |
||||||
|
start: start, |
||||||
|
end: end, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// wait all worker done
|
||||||
|
start := time.Now() |
||||||
|
log.Println("Prefetch start") |
||||||
|
var sleepCount int |
||||||
|
for sleepCount < 3 { |
||||||
|
time.Sleep(time.Second) |
||||||
|
waitWorkerCount := atomic.LoadInt64(&waitWorker) |
||||||
|
if waitWorkerCount >= int64(parallel) { |
||||||
|
sleepCount++ |
||||||
|
} else { |
||||||
|
sleepCount = 0 |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
close(jobChan) |
||||||
|
wg.Wait() |
||||||
|
end := time.Now() |
||||||
|
log.Println("Prefetch end, use", end.Sub(start)) |
||||||
|
} |
||||||
|
|
||||||
|
// prefetchWorker used to process one job
|
||||||
|
func (s *DB) prefetchWorker(job *prefetchJob, jobs chan *prefetchJob) { |
||||||
|
if job.account == nil { |
||||||
|
// scan one account
|
||||||
|
nodeIterator := s.trie.NodeIterator(job.start) |
||||||
|
it := trie.NewIterator(nodeIterator) |
||||||
|
|
||||||
|
for it.Next() { |
||||||
|
if job.end != nil && bytes.Compare(it.Key, job.end) >= 0 { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// build account data from main trie tree
|
||||||
|
var data Account |
||||||
|
if err := rlp.DecodeBytes(it.Value, &data); err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
addrBytes := s.trie.GetKey(it.Key) |
||||||
|
addr := common.BytesToAddress(addrBytes) |
||||||
|
obj := newObject(s, addr, data) |
||||||
|
if data.CodeHash != nil { |
||||||
|
obj.Code(s.db) |
||||||
|
} |
||||||
|
|
||||||
|
// build account trie tree
|
||||||
|
storageIt := trie.NewIterator(obj.getTrie(s.db).NodeIterator(nil)) |
||||||
|
storageJob := &prefetchJob{ |
||||||
|
accountAddr: addrBytes, |
||||||
|
account: &data, |
||||||
|
} |
||||||
|
|
||||||
|
// fetch data
|
||||||
|
s.prefetchAccountStorage(jobs, storageJob, storageIt) |
||||||
|
} |
||||||
|
} else { |
||||||
|
// scan main trie tree
|
||||||
|
obj := newObject(s, common.BytesToAddress(job.accountAddr), *job.account) |
||||||
|
storageIt := trie.NewIterator(obj.getTrie(s.db).NodeIterator(job.start)) |
||||||
|
|
||||||
|
// fetch data
|
||||||
|
s.prefetchAccountStorage(jobs, job, storageIt) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// prefetchAccountStorage used for fetch account storage
|
||||||
|
func (s *DB) prefetchAccountStorage(jobs chan *prefetchJob, job *prefetchJob, it *trie.Iterator) { |
||||||
|
start := time.Now() |
||||||
|
count := 0 |
||||||
|
|
||||||
|
for it.Next() { |
||||||
|
if job.end != nil && bytes.Compare(it.Key, job.end) >= 0 { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
count++ |
||||||
|
|
||||||
|
// if fetch one account job used more than 15s, then split the job
|
||||||
|
if count%10000 == 0 && time.Now().Sub(start) > 15*time.Second { |
||||||
|
middle := utils.BytesMiddle(it.Key, job.end) |
||||||
|
startJob := &prefetchJob{ |
||||||
|
accountAddr: job.accountAddr, |
||||||
|
account: job.account, |
||||||
|
start: it.Key, |
||||||
|
end: middle, |
||||||
|
} |
||||||
|
endJob := &prefetchJob{ |
||||||
|
accountAddr: job.accountAddr, |
||||||
|
account: job.account, |
||||||
|
start: middle, |
||||||
|
end: job.end, |
||||||
|
} |
||||||
|
|
||||||
|
select { |
||||||
|
case jobs <- endJob: |
||||||
|
select { |
||||||
|
case jobs <- startJob: |
||||||
|
return |
||||||
|
default: |
||||||
|
job.end = startJob.end |
||||||
|
start = time.Now() |
||||||
|
} |
||||||
|
default: |
||||||
|
log.Println("job full, continue") |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,80 @@ |
|||||||
|
package state |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"sync" |
||||||
|
"sync/atomic" |
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common" |
||||||
|
"github.com/ethereum/go-ethereum/rlp" |
||||||
|
"github.com/ethereum/go-ethereum/trie" |
||||||
|
"github.com/harmony-one/harmony/internal/shardchain/tikv_manage" |
||||||
|
) |
||||||
|
|
||||||
|
var secureKeyPrefix = []byte("secure-key-") |
||||||
|
|
||||||
|
// DiffAndCleanCache clean block tire data from redis, Used to reduce redis storage and increase hit rate
|
||||||
|
func (s *DB) DiffAndCleanCache(shardId uint32, to *DB) (int, error) { |
||||||
|
// create difference iterator
|
||||||
|
it, _ := trie.NewDifferenceIterator(to.trie.NodeIterator(nil), s.trie.NodeIterator(nil)) |
||||||
|
db, err := tikv_manage.GetDefaultTiKVFactory().NewCacheStateDB(shardId) |
||||||
|
if err != nil { |
||||||
|
return 0, err |
||||||
|
} |
||||||
|
|
||||||
|
batch := db.NewBatch() |
||||||
|
wg := &sync.WaitGroup{} |
||||||
|
count := uint64(0) |
||||||
|
for it.Next(true) { |
||||||
|
if !it.Leaf() { |
||||||
|
// delete it if trie leaf node
|
||||||
|
atomic.AddUint64(&count, 1) |
||||||
|
_ = batch.Delete(it.Hash().Bytes()) |
||||||
|
} else { |
||||||
|
|
||||||
|
// build account data
|
||||||
|
addrBytes := s.trie.GetKey(it.LeafKey()) |
||||||
|
addr := common.BytesToAddress(addrBytes) |
||||||
|
|
||||||
|
var fromAccount, toAccount Account |
||||||
|
if err := rlp.DecodeBytes(it.LeafBlob(), &fromAccount); err != nil { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
if toByte, err := to.trie.TryGet(addrBytes); err != nil { |
||||||
|
continue |
||||||
|
} else if err := rlp.DecodeBytes(toByte, &toAccount); err != nil { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
// if account not changed, skip
|
||||||
|
if bytes.Compare(fromAccount.Root.Bytes(), toAccount.Root.Bytes()) == 0 { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
// create account difference iterator
|
||||||
|
fromAccountTrie := newObject(s, addr, fromAccount).getTrie(s.db) |
||||||
|
toAccountTrie := newObject(to, addr, toAccount).getTrie(to.db) |
||||||
|
accountIt, _ := trie.NewDifferenceIterator(toAccountTrie.NodeIterator(nil), fromAccountTrie.NodeIterator(nil)) |
||||||
|
|
||||||
|
// parallel to delete data
|
||||||
|
wg.Add(1) |
||||||
|
go func() { |
||||||
|
defer wg.Done() |
||||||
|
for accountIt.Next(true) { |
||||||
|
atomic.AddUint64(&count, 1) |
||||||
|
|
||||||
|
if !accountIt.Leaf() { |
||||||
|
_ = batch.Delete(accountIt.Hash().Bytes()) |
||||||
|
} else { |
||||||
|
_ = batch.Delete(append(append([]byte{}, secureKeyPrefix...), accountIt.LeafKey()...)) |
||||||
|
} |
||||||
|
} |
||||||
|
}() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
wg.Wait() |
||||||
|
|
||||||
|
return int(count), db.ReplayCache(batch) |
||||||
|
} |
@ -0,0 +1,126 @@ |
|||||||
|
package shardchain |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"sync" |
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/core/rawdb" |
||||||
|
"github.com/harmony-one/harmony/internal/tikv" |
||||||
|
tikvCommon "github.com/harmony-one/harmony/internal/tikv/common" |
||||||
|
"github.com/harmony-one/harmony/internal/tikv/prefix" |
||||||
|
"github.com/harmony-one/harmony/internal/tikv/remote" |
||||||
|
"github.com/harmony-one/harmony/internal/tikv/statedb_cache" |
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/ethdb" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
LDBTiKVPrefix = "harmony_tikv" |
||||||
|
) |
||||||
|
|
||||||
|
type TiKvCacheConfig struct { |
||||||
|
StateDBCacheSizeInMB uint32 |
||||||
|
StateDBCachePersistencePath string |
||||||
|
StateDBRedisServerAddr string |
||||||
|
StateDBRedisLRUTimeInDay uint32 |
||||||
|
} |
||||||
|
|
||||||
|
// TiKvFactory is a memory-backed blockchain database factory.
|
||||||
|
type TiKvFactory struct { |
||||||
|
cacheDBMap sync.Map |
||||||
|
|
||||||
|
PDAddr []string |
||||||
|
Role string |
||||||
|
CacheConfig statedb_cache.StateDBCacheConfig |
||||||
|
} |
||||||
|
|
||||||
|
// getStateDB create statedb storage use tikv
|
||||||
|
func (f *TiKvFactory) getRemoteDB(shardID uint32) (*prefix.PrefixDatabase, error) { |
||||||
|
key := fmt.Sprintf("remote_%d_%s", shardID, f.Role) |
||||||
|
if db, ok := f.cacheDBMap.Load(key); ok { |
||||||
|
return db.(*prefix.PrefixDatabase), nil |
||||||
|
} else { |
||||||
|
prefixStr := []byte(fmt.Sprintf("%s_%d/", LDBTiKVPrefix, shardID)) |
||||||
|
remoteDatabase, err := remote.NewRemoteDatabase(f.PDAddr, f.Role == tikv.RoleReader) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
tmpDB := prefix.NewPrefixDatabase(prefixStr, remoteDatabase) |
||||||
|
if loadedDB, loaded := f.cacheDBMap.LoadOrStore(key, tmpDB); loaded { |
||||||
|
_ = tmpDB.Close() |
||||||
|
return loadedDB.(*prefix.PrefixDatabase), nil |
||||||
|
} |
||||||
|
|
||||||
|
return tmpDB, nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// getStateDB create statedb storage with local memory cahce
|
||||||
|
func (f *TiKvFactory) getStateDB(shardID uint32) (*statedb_cache.StateDBCacheDatabase, error) { |
||||||
|
key := fmt.Sprintf("state_db_%d_%s", shardID, f.Role) |
||||||
|
if db, ok := f.cacheDBMap.Load(key); ok { |
||||||
|
return db.(*statedb_cache.StateDBCacheDatabase), nil |
||||||
|
} else { |
||||||
|
db, err := f.getRemoteDB(shardID) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
tmpDB, err := statedb_cache.NewStateDBCacheDatabase(db, f.CacheConfig, f.Role == tikv.RoleReader) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
if loadedDB, loaded := f.cacheDBMap.LoadOrStore(key, tmpDB); loaded { |
||||||
|
_ = tmpDB.Close() |
||||||
|
return loadedDB.(*statedb_cache.StateDBCacheDatabase), nil |
||||||
|
} |
||||||
|
|
||||||
|
return tmpDB, nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// NewChainDB returns a new memDB for the blockchain for given shard.
|
||||||
|
func (f *TiKvFactory) NewChainDB(shardID uint32) (ethdb.Database, error) { |
||||||
|
var database ethdb.KeyValueStore |
||||||
|
|
||||||
|
db, err := f.getRemoteDB(shardID) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
database = tikvCommon.ToEthKeyValueStore(db) |
||||||
|
|
||||||
|
return rawdb.NewDatabase(database), nil |
||||||
|
} |
||||||
|
|
||||||
|
// NewStateDB create shard tikv database
|
||||||
|
func (f *TiKvFactory) NewStateDB(shardID uint32) (ethdb.Database, error) { |
||||||
|
var database ethdb.KeyValueStore |
||||||
|
|
||||||
|
cacheDatabase, err := f.getStateDB(shardID) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
database = tikvCommon.ToEthKeyValueStore(cacheDatabase) |
||||||
|
|
||||||
|
return rawdb.NewDatabase(database), nil |
||||||
|
} |
||||||
|
|
||||||
|
// NewCacheStateDB create shard statedb storage with memory cache
|
||||||
|
func (f *TiKvFactory) NewCacheStateDB(shardID uint32) (*statedb_cache.StateDBCacheDatabase, error) { |
||||||
|
return f.getStateDB(shardID) |
||||||
|
} |
||||||
|
|
||||||
|
// CloseAllDB close all tikv database
|
||||||
|
func (f *TiKvFactory) CloseAllDB() { |
||||||
|
f.cacheDBMap.Range(func(_, value interface{}) bool { |
||||||
|
if closer, ok := value.(io.Closer); ok { |
||||||
|
_ = closer.Close() |
||||||
|
} |
||||||
|
return true |
||||||
|
}) |
||||||
|
} |
@ -0,0 +1,28 @@ |
|||||||
|
package tikv_manage |
||||||
|
|
||||||
|
import ( |
||||||
|
"sync" |
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/ethdb" |
||||||
|
"github.com/harmony-one/harmony/internal/tikv/statedb_cache" |
||||||
|
) |
||||||
|
|
||||||
|
type TiKvFactory interface { |
||||||
|
NewChainDB(shardID uint32) (ethdb.Database, error) |
||||||
|
NewStateDB(shardID uint32) (ethdb.Database, error) |
||||||
|
NewCacheStateDB(shardID uint32) (*statedb_cache.StateDBCacheDatabase, error) |
||||||
|
CloseAllDB() |
||||||
|
} |
||||||
|
|
||||||
|
var tikvInit = sync.Once{} |
||||||
|
var tikvFactory TiKvFactory |
||||||
|
|
||||||
|
func SetDefaultTiKVFactory(factory TiKvFactory) { |
||||||
|
tikvInit.Do(func() { |
||||||
|
tikvFactory = factory |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func GetDefaultTiKVFactory() (factory TiKvFactory) { |
||||||
|
return tikvFactory |
||||||
|
} |
@ -0,0 +1,73 @@ |
|||||||
|
// from https://github.com/xtaci/smux/blob/master/alloc.go
|
||||||
|
package byte_alloc |
||||||
|
|
||||||
|
import ( |
||||||
|
"sync" |
||||||
|
) |
||||||
|
|
||||||
|
// magic number
|
||||||
|
var debruijinPos = [...]byte{0, 9, 1, 10, 13, 21, 2, 29, 11, 14, 16, 18, 22, 25, 3, 30, 8, 12, 20, 28, 15, 17, 24, 7, 19, 27, 23, 6, 26, 5, 4, 31} |
||||||
|
|
||||||
|
// Allocator for incoming frames, optimized to prevent overwriting after zeroing
|
||||||
|
type Allocator struct { |
||||||
|
buffers []sync.Pool |
||||||
|
} |
||||||
|
|
||||||
|
// NewAllocator initiates a []byte allocator for frames less than 1048576 bytes,
|
||||||
|
// the waste(memory fragmentation) of space allocation is guaranteed to be
|
||||||
|
// no more than 50%.
|
||||||
|
func NewAllocator() *Allocator { |
||||||
|
alloc := new(Allocator) |
||||||
|
alloc.buffers = make([]sync.Pool, 21) // 1B -> 1M
|
||||||
|
for k := range alloc.buffers { |
||||||
|
size := 1 << uint32(k) |
||||||
|
alloc.buffers[k].New = func() interface{} { |
||||||
|
return make([]byte, size) |
||||||
|
} |
||||||
|
} |
||||||
|
return alloc |
||||||
|
} |
||||||
|
|
||||||
|
// Get a []byte from pool with most appropriate cap
|
||||||
|
func (alloc *Allocator) Get(size int) []byte { |
||||||
|
if size <= 0 { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
if size > 1048576 { |
||||||
|
return make([]byte, size) |
||||||
|
} |
||||||
|
|
||||||
|
bits := msb(size) |
||||||
|
if size == 1<<bits { |
||||||
|
return alloc.buffers[bits].Get().([]byte)[:size] |
||||||
|
} else { |
||||||
|
return alloc.buffers[bits+1].Get().([]byte)[:size] |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Put returns a []byte to pool for future use,
|
||||||
|
// which the cap must be exactly 2^n
|
||||||
|
func (alloc *Allocator) Put(buf []byte) { |
||||||
|
bufCap := cap(buf) |
||||||
|
bits := msb(bufCap) |
||||||
|
if bufCap == 0 || bufCap > 65536 || bufCap != 1<<bits { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
alloc.buffers[bits].Put(buf) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// msb return the pos of most significiant bit
|
||||||
|
// Equivalent to: uint(math.Floor(math.Log2(float64(n))))
|
||||||
|
// http://supertech.csail.mit.edu/papers/debruijn.pdf
|
||||||
|
func msb(size int) byte { |
||||||
|
v := uint32(size) |
||||||
|
v |= v >> 1 |
||||||
|
v |= v >> 2 |
||||||
|
v |= v >> 4 |
||||||
|
v |= v >> 8 |
||||||
|
v |= v >> 16 |
||||||
|
return debruijinPos[(v*0x07C4ACDD)>>27] |
||||||
|
} |
@ -0,0 +1,15 @@ |
|||||||
|
package byte_alloc |
||||||
|
|
||||||
|
var defaultAllocator *Allocator |
||||||
|
|
||||||
|
func init() { |
||||||
|
defaultAllocator = NewAllocator() |
||||||
|
} |
||||||
|
|
||||||
|
func Get(size int) []byte { |
||||||
|
return defaultAllocator.Get(size) |
||||||
|
} |
||||||
|
|
||||||
|
func Put(buf []byte) { |
||||||
|
defaultAllocator.Put(buf) |
||||||
|
} |
@ -0,0 +1,10 @@ |
|||||||
|
package common |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
|
||||||
|
"github.com/syndtr/goleveldb/leveldb" |
||||||
|
) |
||||||
|
|
||||||
|
var ErrEmptyKey = errors.New("empty key is not supported") |
||||||
|
var ErrNotFound = leveldb.ErrNotFound |
@ -0,0 +1,20 @@ |
|||||||
|
package common |
||||||
|
|
||||||
|
import ( |
||||||
|
"unsafe" |
||||||
|
) |
||||||
|
|
||||||
|
// String converts byte slice to string.
|
||||||
|
func String(b []byte) string { |
||||||
|
return *(*string)(unsafe.Pointer(&b)) |
||||||
|
} |
||||||
|
|
||||||
|
// StringBytes converts string to byte slice.
|
||||||
|
func StringBytes(s string) []byte { |
||||||
|
return *(*[]byte)(unsafe.Pointer( |
||||||
|
&struct { |
||||||
|
string |
||||||
|
Cap int |
||||||
|
}{s, len(s)}, |
||||||
|
)) |
||||||
|
} |
@ -0,0 +1,41 @@ |
|||||||
|
package common |
||||||
|
|
||||||
|
import ( |
||||||
|
"io" |
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/ethdb" |
||||||
|
"github.com/syndtr/goleveldb/leveldb/util" |
||||||
|
) |
||||||
|
|
||||||
|
type TiKVStore interface { |
||||||
|
ethdb.KeyValueReader |
||||||
|
ethdb.KeyValueWriter |
||||||
|
ethdb.Batcher |
||||||
|
ethdb.Stater |
||||||
|
ethdb.Compacter |
||||||
|
io.Closer |
||||||
|
|
||||||
|
NewIterator(start, end []byte) ethdb.Iterator |
||||||
|
} |
||||||
|
|
||||||
|
// TiKVStoreWrapper simple wrapper to covert to ethdb.KeyValueStore
|
||||||
|
type TiKVStoreWrapper struct { |
||||||
|
TiKVStore |
||||||
|
} |
||||||
|
|
||||||
|
func (t *TiKVStoreWrapper) NewIterator() ethdb.Iterator { |
||||||
|
return t.TiKVStore.NewIterator(nil, nil) |
||||||
|
} |
||||||
|
|
||||||
|
func (t *TiKVStoreWrapper) NewIteratorWithStart(start []byte) ethdb.Iterator { |
||||||
|
return t.TiKVStore.NewIterator(start, nil) |
||||||
|
} |
||||||
|
|
||||||
|
func (t *TiKVStoreWrapper) NewIteratorWithPrefix(prefix []byte) ethdb.Iterator { |
||||||
|
bytesPrefix := util.BytesPrefix(prefix) |
||||||
|
return t.TiKVStore.NewIterator(bytesPrefix.Start, bytesPrefix.Limit) |
||||||
|
} |
||||||
|
|
||||||
|
func ToEthKeyValueStore(store TiKVStore) ethdb.KeyValueStore { |
||||||
|
return &TiKVStoreWrapper{TiKVStore: store} |
||||||
|
} |
@ -0,0 +1,6 @@ |
|||||||
|
package tikv |
||||||
|
|
||||||
|
const ( |
||||||
|
RoleReader = "Reader" |
||||||
|
RoleWriter = "Writer" |
||||||
|
) |
@ -0,0 +1,42 @@ |
|||||||
|
package prefix |
||||||
|
|
||||||
|
import "github.com/ethereum/go-ethereum/ethdb" |
||||||
|
|
||||||
|
type PrefixBatch struct { |
||||||
|
prefix []byte |
||||||
|
batch ethdb.Batch |
||||||
|
} |
||||||
|
|
||||||
|
func newPrefixBatch(prefix []byte, batch ethdb.Batch) *PrefixBatch { |
||||||
|
return &PrefixBatch{prefix: prefix, batch: batch} |
||||||
|
} |
||||||
|
|
||||||
|
// Put inserts the given value into the key-value data store.
|
||||||
|
func (p *PrefixBatch) Put(key []byte, value []byte) error { |
||||||
|
return p.batch.Put(append(append([]byte{}, p.prefix...), key...), value) |
||||||
|
} |
||||||
|
|
||||||
|
// Delete removes the key from the key-value data store.
|
||||||
|
func (p *PrefixBatch) Delete(key []byte) error { |
||||||
|
return p.batch.Delete(append(append([]byte{}, p.prefix...), key...)) |
||||||
|
} |
||||||
|
|
||||||
|
// ValueSize retrieves the amount of data queued up for writing.
|
||||||
|
func (p *PrefixBatch) ValueSize() int { |
||||||
|
return p.batch.ValueSize() |
||||||
|
} |
||||||
|
|
||||||
|
// Write flushes any accumulated data to disk.
|
||||||
|
func (p *PrefixBatch) Write() error { |
||||||
|
return p.batch.Write() |
||||||
|
} |
||||||
|
|
||||||
|
// Reset resets the batch for reuse.
|
||||||
|
func (p *PrefixBatch) Reset() { |
||||||
|
p.batch.Reset() |
||||||
|
} |
||||||
|
|
||||||
|
// Replay replays the batch contents.
|
||||||
|
func (p *PrefixBatch) Replay(w ethdb.KeyValueWriter) error { |
||||||
|
return p.batch.Replay(newPrefixBatchReplay(p.prefix, w)) |
||||||
|
} |
@ -0,0 +1,35 @@ |
|||||||
|
package prefix |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/ethdb" |
||||||
|
) |
||||||
|
|
||||||
|
type PrefixBatchReplay struct { |
||||||
|
prefix []byte |
||||||
|
prefixLen int |
||||||
|
w ethdb.KeyValueWriter |
||||||
|
} |
||||||
|
|
||||||
|
func newPrefixBatchReplay(prefix []byte, w ethdb.KeyValueWriter) *PrefixBatchReplay { |
||||||
|
return &PrefixBatchReplay{prefix: prefix, prefixLen: len(prefix), w: w} |
||||||
|
} |
||||||
|
|
||||||
|
// Put inserts the given value into the key-value data store.
|
||||||
|
func (p *PrefixBatchReplay) Put(key []byte, value []byte) error { |
||||||
|
if bytes.HasPrefix(key, p.prefix) { |
||||||
|
return p.w.Put(key[p.prefixLen:], value) |
||||||
|
} else { |
||||||
|
return p.w.Put(key, value) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Delete removes the key from the key-value data store.
|
||||||
|
func (p *PrefixBatchReplay) Delete(key []byte) error { |
||||||
|
if bytes.HasPrefix(key, p.prefix) { |
||||||
|
return p.w.Delete(key[p.prefixLen:]) |
||||||
|
} else { |
||||||
|
return p.w.Delete(key) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,109 @@ |
|||||||
|
package prefix |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/ethereum/go-ethereum/ethdb" |
||||||
|
"github.com/harmony-one/harmony/internal/tikv/byte_alloc" |
||||||
|
"github.com/harmony-one/harmony/internal/tikv/common" |
||||||
|
) |
||||||
|
|
||||||
|
// PrefixDatabase is a wrapper to split the storage with prefix
|
||||||
|
type PrefixDatabase struct { |
||||||
|
prefix []byte |
||||||
|
db common.TiKVStore |
||||||
|
keysPool *byte_alloc.Allocator |
||||||
|
} |
||||||
|
|
||||||
|
func NewPrefixDatabase(prefix []byte, db common.TiKVStore) *PrefixDatabase { |
||||||
|
return &PrefixDatabase{ |
||||||
|
prefix: prefix, |
||||||
|
db: db, |
||||||
|
keysPool: byte_alloc.NewAllocator(), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// makeKey use to create a key with prefix, keysPool can reduce gc pressure
|
||||||
|
func (p *PrefixDatabase) makeKey(keys []byte) []byte { |
||||||
|
prefixLen := len(p.prefix) |
||||||
|
byt := p.keysPool.Get(len(keys) + prefixLen) |
||||||
|
copy(byt, p.prefix) |
||||||
|
copy(byt[prefixLen:], keys) |
||||||
|
|
||||||
|
return byt |
||||||
|
} |
||||||
|
|
||||||
|
// Has retrieves if a key is present in the key-value data store.
|
||||||
|
func (p *PrefixDatabase) Has(key []byte) (bool, error) { |
||||||
|
return p.db.Has(p.makeKey(key)) |
||||||
|
} |
||||||
|
|
||||||
|
// Get retrieves the given key if it's present in the key-value data store.
|
||||||
|
func (p *PrefixDatabase) Get(key []byte) ([]byte, error) { |
||||||
|
return p.db.Get(p.makeKey(key)) |
||||||
|
} |
||||||
|
|
||||||
|
// Put inserts the given value into the key-value data store.
|
||||||
|
func (p *PrefixDatabase) Put(key []byte, value []byte) error { |
||||||
|
return p.db.Put(p.makeKey(key), value) |
||||||
|
} |
||||||
|
|
||||||
|
// Delete removes the key from the key-value data store.
|
||||||
|
func (p *PrefixDatabase) Delete(key []byte) error { |
||||||
|
return p.db.Delete(p.makeKey(key)) |
||||||
|
} |
||||||
|
|
||||||
|
// NewBatch creates a write-only database that buffers changes to its host db
|
||||||
|
// until a final write is called.
|
||||||
|
func (p *PrefixDatabase) NewBatch() ethdb.Batch { |
||||||
|
return newPrefixBatch(p.prefix, p.db.NewBatch()) |
||||||
|
} |
||||||
|
|
||||||
|
// buildLimitUsePrefix build the limit byte from start byte, useful generating from prefix works
|
||||||
|
func (p *PrefixDatabase) buildLimitUsePrefix() []byte { |
||||||
|
var limit []byte |
||||||
|
for i := len(p.prefix) - 1; i >= 0; i-- { |
||||||
|
c := p.prefix[i] |
||||||
|
if c < 0xff { |
||||||
|
limit = make([]byte, i+1) |
||||||
|
copy(limit, p.prefix) |
||||||
|
limit[i] = c + 1 |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return limit |
||||||
|
} |
||||||
|
|
||||||
|
// NewIterator creates a binary-alphabetical iterator over the start to end keyspace
|
||||||
|
// contained within the key-value database.
|
||||||
|
func (p *PrefixDatabase) NewIterator(start, end []byte) ethdb.Iterator { |
||||||
|
start = append(p.prefix, start...) |
||||||
|
|
||||||
|
if len(end) == 0 { |
||||||
|
end = p.buildLimitUsePrefix() |
||||||
|
} else { |
||||||
|
end = append(p.prefix, end...) |
||||||
|
} |
||||||
|
|
||||||
|
return newPrefixIterator(p.prefix, p.db.NewIterator(start, end)) |
||||||
|
} |
||||||
|
|
||||||
|
// Stat returns a particular internal stat of the database.
|
||||||
|
func (p *PrefixDatabase) Stat(property string) (string, error) { |
||||||
|
return p.db.Stat(property) |
||||||
|
} |
||||||
|
|
||||||
|
// Compact flattens the underlying data store for the given key range. In essence,
|
||||||
|
// deleted and overwritten versions are discarded, and the data is rearranged to
|
||||||
|
// reduce the cost of operations needed to access them.
|
||||||
|
//
|
||||||
|
// A nil start is treated as a key before all keys in the data store; a nil limit
|
||||||
|
// is treated as a key after all keys in the data store. If both is nil then it
|
||||||
|
// will compact entire data store.
|
||||||
|
func (p *PrefixDatabase) Compact(start []byte, limit []byte) error { |
||||||
|
return p.db.Compact(start, limit) |
||||||
|
} |
||||||
|
|
||||||
|
// Close the storage
|
||||||
|
func (p *PrefixDatabase) Close() error { |
||||||
|
return p.db.Close() |
||||||
|
} |
@ -0,0 +1,53 @@ |
|||||||
|
package prefix |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/ethereum/go-ethereum/ethdb" |
||||||
|
) |
||||||
|
|
||||||
|
type PrefixIterator struct { |
||||||
|
prefix []byte |
||||||
|
prefixLen int |
||||||
|
|
||||||
|
it ethdb.Iterator |
||||||
|
} |
||||||
|
|
||||||
|
func newPrefixIterator(prefix []byte, it ethdb.Iterator) *PrefixIterator { |
||||||
|
return &PrefixIterator{prefix: prefix, prefixLen: len(prefix), it: it} |
||||||
|
} |
||||||
|
|
||||||
|
// Next moves the iterator to the next key/value pair. It returns whether the
|
||||||
|
// iterator is exhausted.
|
||||||
|
func (i *PrefixIterator) Next() bool { |
||||||
|
return i.it.Next() |
||||||
|
} |
||||||
|
|
||||||
|
// Error returns any accumulated error. Exhausting all the key/value pairs
|
||||||
|
// is not considered to be an error.
|
||||||
|
func (i *PrefixIterator) Error() error { |
||||||
|
return i.it.Error() |
||||||
|
} |
||||||
|
|
||||||
|
// Key returns the key of the current key/value pair, or nil if done. The caller
|
||||||
|
// should not modify the contents of the returned slice, and its contents may
|
||||||
|
// change on the next call to Next.
|
||||||
|
func (i *PrefixIterator) Key() []byte { |
||||||
|
key := i.it.Key() |
||||||
|
if len(key) < len(i.prefix) { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
return key[i.prefixLen:] |
||||||
|
} |
||||||
|
|
||||||
|
// Value returns the value of the current key/value pair, or nil if done. The
|
||||||
|
// caller should not modify the contents of the returned slice, and its contents
|
||||||
|
// may change on the next call to Next.
|
||||||
|
func (i *PrefixIterator) Value() []byte { |
||||||
|
return i.it.Value() |
||||||
|
} |
||||||
|
|
||||||
|
// Release releases associated resources. Release should always succeed and can
|
||||||
|
// be called multiple times without causing error.
|
||||||
|
func (i *PrefixIterator) Release() { |
||||||
|
i.it.Release() |
||||||
|
} |
@ -0,0 +1,34 @@ |
|||||||
|
package redis_helper |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
|
||||||
|
"github.com/go-redis/redis/v8" |
||||||
|
) |
||||||
|
|
||||||
|
var redisInstance *redis.ClusterClient |
||||||
|
|
||||||
|
// Init used to init redis instance in tikv mode
|
||||||
|
func Init(serverAddr []string) error { |
||||||
|
option := &redis.ClusterOptions{ |
||||||
|
Addrs: serverAddr, |
||||||
|
PoolSize: 2, |
||||||
|
} |
||||||
|
|
||||||
|
rdb := redis.NewClusterClient(option) |
||||||
|
err := rdb.Ping(context.Background()).Err() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
redisInstance = rdb |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Close disconnect redis instance in tikv mode
|
||||||
|
func Close() error { |
||||||
|
if redisInstance != nil { |
||||||
|
return redisInstance.Close() |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,72 @@ |
|||||||
|
package redis_helper |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"crypto/rand" |
||||||
|
"encoding/base64" |
||||||
|
|
||||||
|
"github.com/go-redis/redis/v8" |
||||||
|
) |
||||||
|
|
||||||
|
type RedisPreempt struct { |
||||||
|
key string |
||||||
|
password string |
||||||
|
lockScript, unlockScript *redis.Script |
||||||
|
lastLockStatus bool |
||||||
|
} |
||||||
|
|
||||||
|
// CreatePreempt used to create a redis preempt instance
|
||||||
|
func CreatePreempt(key string) *RedisPreempt { |
||||||
|
p := &RedisPreempt{ |
||||||
|
key: key, |
||||||
|
} |
||||||
|
p.init() |
||||||
|
|
||||||
|
return p |
||||||
|
} |
||||||
|
|
||||||
|
// init redis preempt instance and some script
|
||||||
|
func (p *RedisPreempt) init() { |
||||||
|
byt := make([]byte, 18) |
||||||
|
_, _ = rand.Read(byt) |
||||||
|
p.password = base64.StdEncoding.EncodeToString(byt) |
||||||
|
p.lockScript = redis.NewScript(` |
||||||
|
if redis.call('get',KEYS[1]) == ARGV[1] then |
||||||
|
return redis.call('expire', KEYS[1], ARGV[2]) |
||||||
|
else |
||||||
|
return redis.call('set', KEYS[1], ARGV[1], 'ex', ARGV[2], 'nx') |
||||||
|
end |
||||||
|
`) |
||||||
|
p.unlockScript = redis.NewScript(` |
||||||
|
if redis.call('get',KEYS[1]) == ARGV[1] then |
||||||
|
return redis.call('del', KEYS[1]) |
||||||
|
else |
||||||
|
return 0 |
||||||
|
end |
||||||
|
`) |
||||||
|
} |
||||||
|
|
||||||
|
// TryLock attempt to lock the master for ttlSecond
|
||||||
|
func (p *RedisPreempt) TryLock(ttlSecond int) (ok bool, err error) { |
||||||
|
ok, err = p.lockScript.Run(context.Background(), redisInstance, []string{p.key}, p.password, ttlSecond).Bool() |
||||||
|
p.lastLockStatus = ok |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Unlock try to release the master permission
|
||||||
|
func (p *RedisPreempt) Unlock() (bool, error) { |
||||||
|
if p == nil { |
||||||
|
return false, nil |
||||||
|
} |
||||||
|
|
||||||
|
return p.unlockScript.Run(context.Background(), redisInstance, []string{p.key}, p.password).Bool() |
||||||
|
} |
||||||
|
|
||||||
|
// LastLockStatus get the last preempt status
|
||||||
|
func (p *RedisPreempt) LastLockStatus() bool { |
||||||
|
if p == nil { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
return p.lastLockStatus |
||||||
|
} |
@ -0,0 +1,130 @@ |
|||||||
|
package redis_helper |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/rlp" |
||||||
|
"github.com/harmony-one/harmony/core/types" |
||||||
|
"github.com/harmony-one/harmony/internal/utils" |
||||||
|
stakingTypes "github.com/harmony-one/harmony/staking/types" |
||||||
|
"github.com/pkg/errors" |
||||||
|
) |
||||||
|
|
||||||
|
// BlockUpdate block update event
|
||||||
|
type BlockUpdate struct { |
||||||
|
BlkNum uint64 |
||||||
|
Logs []*types.Log |
||||||
|
} |
||||||
|
|
||||||
|
// SubscribeShardUpdate subscribe block update event
|
||||||
|
func SubscribeShardUpdate(shardID uint32, cb func(blkNum uint64, logs []*types.Log)) { |
||||||
|
pubsub := redisInstance.Subscribe(context.Background(), fmt.Sprintf("shard_update_%d", shardID)) |
||||||
|
for message := range pubsub.Channel() { |
||||||
|
block := &BlockUpdate{} |
||||||
|
err := rlp.DecodeBytes([]byte(message.Payload), block) |
||||||
|
if err != nil { |
||||||
|
utils.Logger().Info().Err(err).Msg("redis subscribe shard update error") |
||||||
|
continue |
||||||
|
} |
||||||
|
cb(block.BlkNum, block.Logs) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// PublishShardUpdate publish block update event
|
||||||
|
func PublishShardUpdate(shardID uint32, blkNum uint64, logs []*types.Log) error { |
||||||
|
msg, err := rlp.EncodeToBytes(&BlockUpdate{ |
||||||
|
BlkNum: blkNum, |
||||||
|
Logs: logs, |
||||||
|
}) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return redisInstance.Publish(context.Background(), fmt.Sprintf("shard_update_%d", shardID), msg).Err() |
||||||
|
} |
||||||
|
|
||||||
|
//TxPoolUpdate tx pool update event
|
||||||
|
type TxPoolUpdate struct { |
||||||
|
typ string |
||||||
|
Local bool |
||||||
|
Tx types.PoolTransaction |
||||||
|
} |
||||||
|
|
||||||
|
// DecodeRLP decode struct from binary stream
|
||||||
|
func (t *TxPoolUpdate) DecodeRLP(stream *rlp.Stream) error { |
||||||
|
if err := stream.Decode(&t.typ); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if err := stream.Decode(&t.Local); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
switch t.typ { |
||||||
|
case "types.EthTransaction": |
||||||
|
var tmp = &types.EthTransaction{} |
||||||
|
if err := stream.Decode(tmp); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
t.Tx = tmp |
||||||
|
case "types.Transaction": |
||||||
|
var tmp = &types.Transaction{} |
||||||
|
if err := stream.Decode(tmp); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
t.Tx = tmp |
||||||
|
case "stakingTypes.StakingTransaction": |
||||||
|
var tmp = &stakingTypes.StakingTransaction{} |
||||||
|
if err := stream.Decode(tmp); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
t.Tx = tmp |
||||||
|
default: |
||||||
|
return errors.New("unknown txpool type") |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// EncodeRLP encode struct to binary stream
|
||||||
|
func (t *TxPoolUpdate) EncodeRLP(w io.Writer) error { |
||||||
|
switch t.Tx.(type) { |
||||||
|
case *types.EthTransaction: |
||||||
|
t.typ = "types.EthTransaction" |
||||||
|
case *types.Transaction: |
||||||
|
t.typ = "types.Transaction" |
||||||
|
case *stakingTypes.StakingTransaction: |
||||||
|
t.typ = "stakingTypes.StakingTransaction" |
||||||
|
} |
||||||
|
|
||||||
|
if err := rlp.Encode(w, t.typ); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if err := rlp.Encode(w, t.Local); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return rlp.Encode(w, t.Tx) |
||||||
|
} |
||||||
|
|
||||||
|
// SubscribeTxPoolUpdate subscribe tx pool update event
|
||||||
|
func SubscribeTxPoolUpdate(shardID uint32, cb func(tx types.PoolTransaction, local bool)) { |
||||||
|
pubsub := redisInstance.Subscribe(context.Background(), fmt.Sprintf("txpool_update_%d", shardID)) |
||||||
|
for message := range pubsub.Channel() { |
||||||
|
txu := &TxPoolUpdate{} |
||||||
|
err := rlp.DecodeBytes([]byte(message.Payload), &txu) |
||||||
|
if err != nil { |
||||||
|
utils.Logger().Info().Err(err).Msg("redis subscribe shard update error") |
||||||
|
continue |
||||||
|
} |
||||||
|
cb(txu.Tx, txu.Local) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// PublishTxPoolUpdate publish tx pool update event
|
||||||
|
func PublishTxPoolUpdate(shardID uint32, tx types.PoolTransaction, local bool) error { |
||||||
|
txu := &TxPoolUpdate{Local: local, Tx: tx} |
||||||
|
msg, err := rlp.EncodeToBytes(txu) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return redisInstance.Publish(context.Background(), fmt.Sprintf("txpool_update_%d", shardID), msg).Err() |
||||||
|
} |
@ -0,0 +1,119 @@ |
|||||||
|
package remote |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"context" |
||||||
|
"sync" |
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/ethdb" |
||||||
|
"github.com/harmony-one/harmony/internal/tikv/common" |
||||||
|
) |
||||||
|
|
||||||
|
type RemoteBatch struct { |
||||||
|
db *RemoteDatabase |
||||||
|
lock sync.Mutex |
||||||
|
|
||||||
|
size int |
||||||
|
batchWriteKey [][]byte |
||||||
|
batchWriteValue [][]byte |
||||||
|
batchDeleteKey [][]byte |
||||||
|
} |
||||||
|
|
||||||
|
func newRemoteBatch(db *RemoteDatabase) *RemoteBatch { |
||||||
|
return &RemoteBatch{db: db} |
||||||
|
} |
||||||
|
|
||||||
|
// Put inserts the given value into the key-value data store.
|
||||||
|
func (b *RemoteBatch) Put(key []byte, value []byte) error { |
||||||
|
if len(key) == 0 { |
||||||
|
return common.ErrEmptyKey |
||||||
|
} |
||||||
|
|
||||||
|
if len(value) == 0 { |
||||||
|
value = EmptyValueStub |
||||||
|
} |
||||||
|
|
||||||
|
b.lock.Lock() |
||||||
|
defer b.lock.Unlock() |
||||||
|
|
||||||
|
b.batchWriteKey = append(b.batchWriteKey, key) |
||||||
|
b.batchWriteValue = append(b.batchWriteValue, value) |
||||||
|
b.size += len(key) + len(value) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Delete removes the key from the key-value data store.
|
||||||
|
func (b *RemoteBatch) Delete(key []byte) error { |
||||||
|
b.lock.Lock() |
||||||
|
defer b.lock.Unlock() |
||||||
|
|
||||||
|
b.batchDeleteKey = append(b.batchDeleteKey, key) |
||||||
|
b.size += len(key) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// ValueSize retrieves the amount of data queued up for writing.
|
||||||
|
func (b *RemoteBatch) ValueSize() int { |
||||||
|
return b.size |
||||||
|
} |
||||||
|
|
||||||
|
// Write flushes any accumulated data to disk.
|
||||||
|
func (b *RemoteBatch) Write() error { |
||||||
|
b.lock.Lock() |
||||||
|
defer b.lock.Unlock() |
||||||
|
|
||||||
|
if len(b.batchWriteKey) > 0 { |
||||||
|
err := b.db.client.BatchPut(context.Background(), b.batchWriteKey, b.batchWriteValue) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if len(b.batchDeleteKey) > 0 { |
||||||
|
err := b.db.client.BatchDelete(context.Background(), b.batchDeleteKey) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Reset resets the batch for reuse.
|
||||||
|
func (b *RemoteBatch) Reset() { |
||||||
|
b.lock.Lock() |
||||||
|
defer b.lock.Unlock() |
||||||
|
|
||||||
|
b.batchWriteKey = b.batchWriteKey[:0] |
||||||
|
b.batchWriteValue = b.batchWriteValue[:0] |
||||||
|
b.batchDeleteKey = b.batchDeleteKey[:0] |
||||||
|
b.size = 0 |
||||||
|
} |
||||||
|
|
||||||
|
// Replay replays the batch contents.
|
||||||
|
func (b *RemoteBatch) Replay(w ethdb.KeyValueWriter) error { |
||||||
|
for i, key := range b.batchWriteKey { |
||||||
|
if bytes.Compare(b.batchWriteValue[i], EmptyValueStub) == 0 { |
||||||
|
err := w.Put(key, []byte{}) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} else { |
||||||
|
err := w.Put(key, b.batchWriteValue[i]) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if len(b.batchDeleteKey) > 0 { |
||||||
|
for _, key := range b.batchDeleteKey { |
||||||
|
err := w.Delete(key) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,139 @@ |
|||||||
|
package remote |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"context" |
||||||
|
"runtime/trace" |
||||||
|
"sync/atomic" |
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/ethdb" |
||||||
|
"github.com/harmony-one/harmony/internal/tikv/common" |
||||||
|
"github.com/tikv/client-go/v2/config" |
||||||
|
"github.com/tikv/client-go/v2/rawkv" |
||||||
|
) |
||||||
|
|
||||||
|
var EmptyValueStub = []byte("HarmonyTiKVEmptyValueStub") |
||||||
|
|
||||||
|
type RemoteDatabase struct { |
||||||
|
client *rawkv.Client |
||||||
|
readOnly bool |
||||||
|
isClose uint64 |
||||||
|
} |
||||||
|
|
||||||
|
func NewRemoteDatabase(pdAddr []string, readOnly bool) (*RemoteDatabase, error) { |
||||||
|
client, err := rawkv.NewClient(context.Background(), pdAddr, config.DefaultConfig().Security) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
db := &RemoteDatabase{ |
||||||
|
client: client, |
||||||
|
readOnly: readOnly, |
||||||
|
} |
||||||
|
|
||||||
|
return db, nil |
||||||
|
} |
||||||
|
|
||||||
|
// ReadOnly set storage to readonly mode
|
||||||
|
func (d *RemoteDatabase) ReadOnly() { |
||||||
|
d.readOnly = true |
||||||
|
} |
||||||
|
|
||||||
|
// Has retrieves if a key is present in the key-value data store.
|
||||||
|
func (d *RemoteDatabase) Has(key []byte) (bool, error) { |
||||||
|
data, err := d.Get(key) |
||||||
|
if err != nil { |
||||||
|
if err == common.ErrNotFound { |
||||||
|
return false, nil |
||||||
|
} |
||||||
|
return false, err |
||||||
|
} else { |
||||||
|
return len(data) != 0, nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Get retrieves the given key if it's present in the key-value data store.
|
||||||
|
func (d *RemoteDatabase) Get(key []byte) ([]byte, error) { |
||||||
|
if len(key) == 0 { |
||||||
|
return nil, common.ErrEmptyKey |
||||||
|
} |
||||||
|
|
||||||
|
region := trace.StartRegion(context.Background(), "tikv Get") |
||||||
|
defer region.End() |
||||||
|
|
||||||
|
get, err := d.client.Get(context.Background(), key) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
if len(get) == 0 { |
||||||
|
return nil, common.ErrNotFound |
||||||
|
} |
||||||
|
|
||||||
|
if len(get) == len(EmptyValueStub) && bytes.Compare(get, EmptyValueStub) == 0 { |
||||||
|
get = get[:0] |
||||||
|
} |
||||||
|
|
||||||
|
return get, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Put inserts the given value into the key-value data store.
|
||||||
|
func (d *RemoteDatabase) Put(key []byte, value []byte) error { |
||||||
|
if len(key) == 0 { |
||||||
|
return common.ErrEmptyKey |
||||||
|
} |
||||||
|
if d.readOnly { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
if len(value) == 0 { |
||||||
|
value = EmptyValueStub |
||||||
|
} |
||||||
|
|
||||||
|
return d.client.Put(context.Background(), key, value) |
||||||
|
} |
||||||
|
|
||||||
|
// Delete removes the key from the key-value data store.
|
||||||
|
func (d *RemoteDatabase) Delete(key []byte) error { |
||||||
|
if len(key) == 0 { |
||||||
|
return common.ErrEmptyKey |
||||||
|
} |
||||||
|
if d.readOnly { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
return d.client.Delete(context.Background(), key) |
||||||
|
} |
||||||
|
|
||||||
|
// NewBatch creates a write-only database that buffers changes to its host db
|
||||||
|
// until a final write is called.
|
||||||
|
func (d *RemoteDatabase) NewBatch() ethdb.Batch { |
||||||
|
if d.readOnly { |
||||||
|
return newNopRemoteBatch(d) |
||||||
|
} |
||||||
|
|
||||||
|
return newRemoteBatch(d) |
||||||
|
} |
||||||
|
|
||||||
|
func (d *RemoteDatabase) NewIterator(start, end []byte) ethdb.Iterator { |
||||||
|
return newRemoteIterator(d, start, end) |
||||||
|
} |
||||||
|
|
||||||
|
// Stat returns a particular internal stat of the database.
|
||||||
|
func (d *RemoteDatabase) Stat(property string) (string, error) { |
||||||
|
return "", common.ErrNotFound |
||||||
|
} |
||||||
|
|
||||||
|
// Compact tikv current not supprot manual compact
|
||||||
|
func (d *RemoteDatabase) Compact(start []byte, limit []byte) error { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Close disconnect the tikv
|
||||||
|
func (d *RemoteDatabase) Close() error { |
||||||
|
if atomic.CompareAndSwapUint64(&d.isClose, 0, 1) { |
||||||
|
return d.client.Close() |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,110 @@ |
|||||||
|
package remote |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"sync" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
iteratorOnce = 300 |
||||||
|
) |
||||||
|
|
||||||
|
type RemoteIterator struct { |
||||||
|
db *RemoteDatabase |
||||||
|
lock sync.Mutex |
||||||
|
|
||||||
|
limit []byte |
||||||
|
start []byte |
||||||
|
|
||||||
|
err error |
||||||
|
end bool |
||||||
|
pos int |
||||||
|
|
||||||
|
keys, values [][]byte |
||||||
|
currentKey, currentValue []byte |
||||||
|
} |
||||||
|
|
||||||
|
func newRemoteIterator(db *RemoteDatabase, start, limit []byte) *RemoteIterator { |
||||||
|
return &RemoteIterator{ |
||||||
|
db: db, |
||||||
|
start: start, |
||||||
|
limit: limit, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Next moves the iterator to the next key/value pair. It returns whether the
|
||||||
|
// iterator is exhausted.
|
||||||
|
func (i *RemoteIterator) Next() bool { |
||||||
|
if i.end { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
if i.keys == nil { |
||||||
|
if next := i.scanNext(); !next { |
||||||
|
return false |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
i.currentKey = i.keys[i.pos] |
||||||
|
i.currentValue = i.values[i.pos] |
||||||
|
i.pos++ |
||||||
|
|
||||||
|
if i.pos >= len(i.keys) { |
||||||
|
i.scanNext() |
||||||
|
} |
||||||
|
|
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
// scanNext real scan from tikv, and cache it
|
||||||
|
func (i *RemoteIterator) scanNext() bool { |
||||||
|
keys, values, err := i.db.client.Scan(context.Background(), i.start, i.limit, iteratorOnce) |
||||||
|
if err != nil { |
||||||
|
i.err = err |
||||||
|
i.end = true |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
if len(keys) == 0 { |
||||||
|
i.end = true |
||||||
|
return false |
||||||
|
} else { |
||||||
|
i.start = append(keys[len(keys)-1], 0) |
||||||
|
} |
||||||
|
|
||||||
|
i.pos = 0 |
||||||
|
i.keys = keys |
||||||
|
i.values = values |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
// Error returns any accumulated error. Exhausting all the key/value pairs
|
||||||
|
// is not considered to be an error.
|
||||||
|
func (i *RemoteIterator) Error() error { |
||||||
|
return i.err |
||||||
|
} |
||||||
|
|
||||||
|
// Key returns the key of the current key/value pair, or nil if done. The caller
|
||||||
|
// should not modify the contents of the returned slice, and its contents may
|
||||||
|
// change on the next call to Next.
|
||||||
|
func (i *RemoteIterator) Key() []byte { |
||||||
|
return i.currentKey |
||||||
|
} |
||||||
|
|
||||||
|
// Value returns the value of the current key/value pair, or nil if done. The
|
||||||
|
// caller should not modify the contents of the returned slice, and its contents
|
||||||
|
// may change on the next call to Next.
|
||||||
|
func (i *RemoteIterator) Value() []byte { |
||||||
|
return i.currentValue |
||||||
|
} |
||||||
|
|
||||||
|
// Release releases associated resources. Release should always succeed and can
|
||||||
|
// be called multiple times without causing error.
|
||||||
|
func (i *RemoteIterator) Release() { |
||||||
|
i.db = nil |
||||||
|
i.end = true |
||||||
|
i.keys = nil |
||||||
|
i.values = nil |
||||||
|
i.currentKey = nil |
||||||
|
i.currentValue = nil |
||||||
|
} |
@ -0,0 +1,36 @@ |
|||||||
|
package remote |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/ethereum/go-ethereum/ethdb" |
||||||
|
) |
||||||
|
|
||||||
|
// NopRemoteBatch on readonly mode, write operator will be reject
|
||||||
|
type NopRemoteBatch struct { |
||||||
|
} |
||||||
|
|
||||||
|
func newNopRemoteBatch(db *RemoteDatabase) *NopRemoteBatch { |
||||||
|
return &NopRemoteBatch{} |
||||||
|
} |
||||||
|
|
||||||
|
func (b *NopRemoteBatch) Put(key []byte, value []byte) error { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (b *NopRemoteBatch) Delete(key []byte) error { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (b *NopRemoteBatch) ValueSize() int { |
||||||
|
return 0 |
||||||
|
} |
||||||
|
|
||||||
|
func (b *NopRemoteBatch) Write() error { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (b *NopRemoteBatch) Reset() { |
||||||
|
} |
||||||
|
|
||||||
|
func (b *NopRemoteBatch) Replay(w ethdb.KeyValueWriter) error { |
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,45 @@ |
|||||||
|
package statedb_cache |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/ethereum/go-ethereum/ethdb" |
||||||
|
) |
||||||
|
|
||||||
|
type StateDBCacheBatch struct { |
||||||
|
db *StateDBCacheDatabase |
||||||
|
remoteBatch ethdb.Batch |
||||||
|
} |
||||||
|
|
||||||
|
func newStateDBCacheBatch(db *StateDBCacheDatabase, batch ethdb.Batch) *StateDBCacheBatch { |
||||||
|
return &StateDBCacheBatch{db: db, remoteBatch: batch} |
||||||
|
} |
||||||
|
|
||||||
|
func (b *StateDBCacheBatch) Put(key []byte, value []byte) error { |
||||||
|
return b.remoteBatch.Put(key, value) |
||||||
|
} |
||||||
|
|
||||||
|
func (b *StateDBCacheBatch) Delete(key []byte) error { |
||||||
|
return b.remoteBatch.Delete(key) |
||||||
|
} |
||||||
|
|
||||||
|
func (b *StateDBCacheBatch) ValueSize() int { |
||||||
|
// In ethdb, the size of each commit is too small, this controls the submission frequency
|
||||||
|
return b.remoteBatch.ValueSize() / 40 |
||||||
|
} |
||||||
|
|
||||||
|
func (b *StateDBCacheBatch) Write() (err error) { |
||||||
|
defer func() { |
||||||
|
if err == nil { |
||||||
|
_ = b.db.cacheWrite(b) |
||||||
|
} |
||||||
|
}() |
||||||
|
|
||||||
|
return b.remoteBatch.Write() |
||||||
|
} |
||||||
|
|
||||||
|
func (b *StateDBCacheBatch) Reset() { |
||||||
|
b.remoteBatch.Reset() |
||||||
|
} |
||||||
|
|
||||||
|
func (b *StateDBCacheBatch) Replay(w ethdb.KeyValueWriter) error { |
||||||
|
return b.remoteBatch.Replay(w) |
||||||
|
} |
@ -0,0 +1,373 @@ |
|||||||
|
package statedb_cache |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"log" |
||||||
|
"runtime" |
||||||
|
"sync/atomic" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/VictoriaMetrics/fastcache" |
||||||
|
"github.com/ethereum/go-ethereum/ethdb" |
||||||
|
redis "github.com/go-redis/redis/v8" |
||||||
|
"github.com/harmony-one/harmony/internal/tikv/common" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
maxMemoryEntrySize = 64 * 1024 |
||||||
|
maxPipelineEntriesCount = 10000 |
||||||
|
cacheMinGapInSecond = 600 |
||||||
|
) |
||||||
|
|
||||||
|
type cacheWrapper struct { |
||||||
|
*fastcache.Cache |
||||||
|
} |
||||||
|
|
||||||
|
// Put inserts the given value into the key-value data store.
|
||||||
|
func (c *cacheWrapper) Put(key []byte, value []byte) error { |
||||||
|
c.Cache.Set(key, value) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Delete removes the key from the key-value data store.
|
||||||
|
func (c *cacheWrapper) Delete(key []byte) error { |
||||||
|
c.Cache.Del(key) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
type redisPipelineWrapper struct { |
||||||
|
redis.Pipeliner |
||||||
|
|
||||||
|
expiredTime time.Duration |
||||||
|
} |
||||||
|
|
||||||
|
// Put inserts the given value into the key-value data store.
|
||||||
|
func (c *redisPipelineWrapper) Put(key []byte, value []byte) error { |
||||||
|
c.SetEX(context.Background(), string(key), value, c.expiredTime) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Delete removes the key from the key-value data store.
|
||||||
|
func (c *redisPipelineWrapper) Delete(key []byte) error { |
||||||
|
c.Del(context.Background(), string(key)) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
type StateDBCacheConfig struct { |
||||||
|
CacheSizeInMB uint32 |
||||||
|
CachePersistencePath string |
||||||
|
RedisServerAddr []string |
||||||
|
RedisLRUTimeInDay uint32 |
||||||
|
DebugHitRate bool |
||||||
|
} |
||||||
|
|
||||||
|
type StateDBCacheDatabase struct { |
||||||
|
config StateDBCacheConfig |
||||||
|
enableReadCache bool |
||||||
|
isClose uint64 |
||||||
|
remoteDB common.TiKVStore |
||||||
|
|
||||||
|
// l1cache (memory)
|
||||||
|
l1Cache *cacheWrapper |
||||||
|
|
||||||
|
// l2cache (redis)
|
||||||
|
l2Cache *redis.ClusterClient |
||||||
|
l2ReadOnly bool |
||||||
|
l2ExpiredTime time.Duration |
||||||
|
l2NextExpiredTime time.Time |
||||||
|
l2ExpiredRefresh chan string |
||||||
|
|
||||||
|
// stats
|
||||||
|
l1HitCount uint64 |
||||||
|
l2HitCount uint64 |
||||||
|
missCount uint64 |
||||||
|
notFoundOrErrorCount uint64 |
||||||
|
} |
||||||
|
|
||||||
|
func NewStateDBCacheDatabase(remoteDB common.TiKVStore, config StateDBCacheConfig, readOnly bool) (*StateDBCacheDatabase, error) { |
||||||
|
// create or load memory cache from file
|
||||||
|
cache := fastcache.LoadFromFileOrNew(config.CachePersistencePath, int(config.CacheSizeInMB)*1024*1024) |
||||||
|
|
||||||
|
// create options
|
||||||
|
option := &redis.ClusterOptions{ |
||||||
|
Addrs: config.RedisServerAddr, |
||||||
|
PoolSize: 32, |
||||||
|
IdleTimeout: 60 * time.Second, |
||||||
|
} |
||||||
|
|
||||||
|
if readOnly { |
||||||
|
option.PoolSize = 16 |
||||||
|
option.ReadOnly = true |
||||||
|
} |
||||||
|
|
||||||
|
// check redis connection
|
||||||
|
rdb := redis.NewClusterClient(option) |
||||||
|
err := rdb.Ping(context.Background()).Err() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
// reload redis cluster slots status
|
||||||
|
rdb.ReloadState(context.Background()) |
||||||
|
|
||||||
|
db := &StateDBCacheDatabase{ |
||||||
|
config: config, |
||||||
|
enableReadCache: true, |
||||||
|
remoteDB: remoteDB, |
||||||
|
l1Cache: &cacheWrapper{cache}, |
||||||
|
l2Cache: rdb, |
||||||
|
l2ReadOnly: readOnly, |
||||||
|
l2ExpiredTime: time.Duration(config.RedisLRUTimeInDay) * 24 * time.Hour, |
||||||
|
} |
||||||
|
|
||||||
|
if !readOnly { |
||||||
|
// Read a copy of the memory hit data into redis to improve the hit rate
|
||||||
|
// refresh read time to prevent recycling by lru
|
||||||
|
db.l2ExpiredRefresh = make(chan string, 100000) |
||||||
|
go db.startL2ExpiredRefresh() |
||||||
|
} |
||||||
|
|
||||||
|
// print debug info
|
||||||
|
if config.DebugHitRate { |
||||||
|
go func() { |
||||||
|
for range time.Tick(5 * time.Second) { |
||||||
|
db.cacheStatusPrint() |
||||||
|
} |
||||||
|
}() |
||||||
|
} |
||||||
|
|
||||||
|
return db, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Has retrieves if a key is present in the key-value data store.
|
||||||
|
func (c *StateDBCacheDatabase) Has(key []byte) (bool, error) { |
||||||
|
return c.remoteDB.Has(key) |
||||||
|
} |
||||||
|
|
||||||
|
// Get retrieves the given key if it's present in the key-value data store.
|
||||||
|
func (c *StateDBCacheDatabase) Get(key []byte) (ret []byte, err error) { |
||||||
|
if c.enableReadCache { |
||||||
|
var ok bool |
||||||
|
|
||||||
|
// first, get data from memory cache
|
||||||
|
keyStr := string(key) |
||||||
|
if ret, ok = c.l1Cache.HasGet(nil, key); ok { |
||||||
|
if !c.l2ReadOnly { |
||||||
|
select { |
||||||
|
// refresh read time to prevent recycling by lru
|
||||||
|
case c.l2ExpiredRefresh <- keyStr: |
||||||
|
default: |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
atomic.AddUint64(&c.l1HitCount, 1) |
||||||
|
return ret, nil |
||||||
|
} |
||||||
|
|
||||||
|
defer func() { |
||||||
|
if err == nil { |
||||||
|
if len(ret) < maxMemoryEntrySize { |
||||||
|
// set data to memory db if loaded from redis cache or leveldb
|
||||||
|
c.l1Cache.Set(key, ret) |
||||||
|
} |
||||||
|
} |
||||||
|
}() |
||||||
|
|
||||||
|
timeoutCtx, cancelFunc := context.WithTimeout(context.Background(), 5*time.Second) |
||||||
|
defer cancelFunc() |
||||||
|
|
||||||
|
// load data from redis cache
|
||||||
|
data := c.l2Cache.Get(timeoutCtx, keyStr) |
||||||
|
if data.Err() == nil { |
||||||
|
atomic.AddUint64(&c.l2HitCount, 1) |
||||||
|
return data.Bytes() |
||||||
|
} else if data.Err() != redis.Nil { |
||||||
|
log.Printf("[Get WARN]Redis Error: %v", data.Err()) |
||||||
|
} |
||||||
|
|
||||||
|
if !c.l2ReadOnly { |
||||||
|
defer func() { |
||||||
|
// set data to redis db if loaded from leveldb
|
||||||
|
if err == nil { |
||||||
|
c.l2Cache.SetEX(context.Background(), keyStr, ret, c.l2ExpiredTime) |
||||||
|
} |
||||||
|
}() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
ret, err = c.remoteDB.Get(key) |
||||||
|
if err != nil { |
||||||
|
atomic.AddUint64(&c.notFoundOrErrorCount, 1) |
||||||
|
} else { |
||||||
|
atomic.AddUint64(&c.missCount, 1) |
||||||
|
} |
||||||
|
|
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Put inserts the given value into the key-value data store.
|
||||||
|
func (c *StateDBCacheDatabase) Put(key []byte, value []byte) (err error) { |
||||||
|
if c.enableReadCache { |
||||||
|
defer func() { |
||||||
|
if err == nil { |
||||||
|
if len(value) < maxMemoryEntrySize { |
||||||
|
// memory db only accept the small data
|
||||||
|
c.l1Cache.Set(key, value) |
||||||
|
} |
||||||
|
if !c.l2ReadOnly { |
||||||
|
timeoutCtx, cancelFunc := context.WithTimeout(context.Background(), 5*time.Second) |
||||||
|
defer cancelFunc() |
||||||
|
|
||||||
|
// send the data to redis
|
||||||
|
res := c.l2Cache.SetEX(timeoutCtx, string(key), value, c.l2ExpiredTime) |
||||||
|
if res.Err() == nil { |
||||||
|
log.Printf("[Put WARN]Redis Error: %v", res.Err()) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}() |
||||||
|
} |
||||||
|
|
||||||
|
return c.remoteDB.Put(key, value) |
||||||
|
} |
||||||
|
|
||||||
|
// Delete removes the key from the key-value data store.
|
||||||
|
func (c *StateDBCacheDatabase) Delete(key []byte) (err error) { |
||||||
|
if c.enableReadCache { |
||||||
|
defer func() { |
||||||
|
if err == nil { |
||||||
|
c.l1Cache.Del(key) |
||||||
|
if !c.l2ReadOnly { |
||||||
|
c.l2Cache.Del(context.Background(), string(key)) |
||||||
|
} |
||||||
|
} |
||||||
|
}() |
||||||
|
} |
||||||
|
|
||||||
|
return c.remoteDB.Delete(key) |
||||||
|
} |
||||||
|
|
||||||
|
// NewBatch creates a write-only database that buffers changes to its host db
|
||||||
|
// until a final write is called.
|
||||||
|
func (c *StateDBCacheDatabase) NewBatch() ethdb.Batch { |
||||||
|
return newStateDBCacheBatch(c, c.remoteDB.NewBatch()) |
||||||
|
} |
||||||
|
|
||||||
|
// Stat returns a particular internal stat of the database.
|
||||||
|
func (c *StateDBCacheDatabase) Stat(property string) (string, error) { |
||||||
|
switch property { |
||||||
|
case "tikv.save.cache": |
||||||
|
_ = c.l1Cache.SaveToFileConcurrent(c.config.CachePersistencePath, runtime.NumCPU()) |
||||||
|
} |
||||||
|
return c.remoteDB.Stat(property) |
||||||
|
} |
||||||
|
|
||||||
|
func (c *StateDBCacheDatabase) Compact(start []byte, limit []byte) error { |
||||||
|
return c.remoteDB.Compact(start, limit) |
||||||
|
} |
||||||
|
|
||||||
|
func (c *StateDBCacheDatabase) NewIterator(start, end []byte) ethdb.Iterator { |
||||||
|
return c.remoteDB.NewIterator(start, end) |
||||||
|
} |
||||||
|
|
||||||
|
// Close disconnect the redis and save memory cache to file
|
||||||
|
func (c *StateDBCacheDatabase) Close() error { |
||||||
|
if atomic.CompareAndSwapUint64(&c.isClose, 0, 1) { |
||||||
|
err := c.l1Cache.SaveToFileConcurrent(c.config.CachePersistencePath, runtime.NumCPU()) |
||||||
|
if err != nil { |
||||||
|
log.Printf("save file to '%s' error: %v", c.config.CachePersistencePath, err) |
||||||
|
} |
||||||
|
|
||||||
|
if !c.l2ReadOnly { |
||||||
|
close(c.l2ExpiredRefresh) |
||||||
|
time.Sleep(time.Second) |
||||||
|
|
||||||
|
for range c.l2ExpiredRefresh { |
||||||
|
// nop, clear chan
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
_ = c.l2Cache.Close() |
||||||
|
|
||||||
|
return c.remoteDB.Close() |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// cacheWrite write batch to cache
|
||||||
|
func (c *StateDBCacheDatabase) cacheWrite(b ethdb.Batch) error { |
||||||
|
if !c.l2ReadOnly { |
||||||
|
pipeline := c.l2Cache.Pipeline() |
||||||
|
|
||||||
|
err := b.Replay(&redisPipelineWrapper{Pipeliner: pipeline, expiredTime: c.l2ExpiredTime}) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
_, err = pipeline.Exec(context.Background()) |
||||||
|
if err != nil { |
||||||
|
log.Printf("[BatchWrite WARN]Redis Error: %v", err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return b.Replay(c.l1Cache) |
||||||
|
} |
||||||
|
|
||||||
|
func (c *StateDBCacheDatabase) refreshL2ExpiredTime() { |
||||||
|
unix := time.Now().Add(c.l2ExpiredTime).Unix() |
||||||
|
unix = unix - (unix % cacheMinGapInSecond) + cacheMinGapInSecond |
||||||
|
c.l2NextExpiredTime = time.Unix(unix, 0) |
||||||
|
} |
||||||
|
|
||||||
|
// startL2ExpiredRefresh batch refresh redis cache
|
||||||
|
func (c *StateDBCacheDatabase) startL2ExpiredRefresh() { |
||||||
|
ticker := time.NewTicker(time.Second) |
||||||
|
defer ticker.Stop() |
||||||
|
|
||||||
|
pipeline := c.l2Cache.Pipeline() |
||||||
|
lastWrite := time.Now() |
||||||
|
|
||||||
|
for { |
||||||
|
select { |
||||||
|
case key, ok := <-c.l2ExpiredRefresh: |
||||||
|
if !ok { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
pipeline.ExpireAt(context.Background(), key, c.l2NextExpiredTime) |
||||||
|
if pipeline.Len() > maxPipelineEntriesCount { |
||||||
|
_, _ = pipeline.Exec(context.Background()) |
||||||
|
lastWrite = time.Now() |
||||||
|
} |
||||||
|
case now := <-ticker.C: |
||||||
|
c.refreshL2ExpiredTime() |
||||||
|
|
||||||
|
if pipeline.Len() > 0 && now.Sub(lastWrite) > time.Second { |
||||||
|
_, _ = pipeline.Exec(context.Background()) |
||||||
|
lastWrite = time.Now() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// print stats to stdout
|
||||||
|
func (c *StateDBCacheDatabase) cacheStatusPrint() { |
||||||
|
var state = &fastcache.Stats{} |
||||||
|
state.Reset() |
||||||
|
c.l1Cache.UpdateStats(state) |
||||||
|
|
||||||
|
stats := c.l2Cache.PoolStats() |
||||||
|
log.Printf("redis: TotalConns: %d, IdleConns: %d, StaleConns: %d", stats.TotalConns, stats.IdleConns, stats.StaleConns) |
||||||
|
total := float64(c.l1HitCount + c.l2HitCount + c.missCount + c.notFoundOrErrorCount) |
||||||
|
|
||||||
|
log.Printf( |
||||||
|
"total: l1Hit: %d(%.2f%%), l2Hit: %d(%.2f%%), miss: %d(%.2f%%), notFoundOrErrorCount: %d, fastcache: GetCalls: %d, SetCalls: %d, Misses: %d, EntriesCount: %d, BytesSize: %d", |
||||||
|
c.l1HitCount, float64(c.l1HitCount)/total*100, c.l2HitCount, float64(c.l2HitCount)/total*100, c.missCount, float64(c.missCount)/total*100, c.notFoundOrErrorCount, |
||||||
|
state.GetCalls, state.SetCalls, state.Misses, state.EntriesCount, state.BytesSize, |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
func (c *StateDBCacheDatabase) ReplayCache(batch ethdb.Batch) error { |
||||||
|
return c.cacheWrite(batch) |
||||||
|
} |
Loading…
Reference in new issue