Skip to content
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

Implement expirable LRU cache and config settings for EVM calls #223

Merged
merged 2 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 30 additions & 13 deletions config/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -410,24 +410,41 @@ node:
# # Exposed RPC endpoint of virtual filter service for client request
# serviceRpcUrl: http://127.0.0.1:42537

# # Global constraints
# constraints:
# # Log filter constraint
# logfilter:
# # Maximum count of block hashes
# # Request control Configuration
# requestControl:
# # Log filter limits for handling 'getLogs' requests
# logFilter:
# # Maximum number of block hashes allowed in a filter
# maxBlockHashCount: 32
# # Maximum count of address
# # Maximum number of addresses allowed in a filter
# maxAddressCount: 32
# # Maximum count of topics
# # Maximum number of topics allowed in a filter
# maxTopicCount: 32
# # Maximum epoch range for the log filter split to the full node
# # Maximum epoch range to split log filters for full nodes
# maxSplitEpochRange: 1000
# # Maximum block range for the log filter split to the full node
# # Maximum block range to split log filters for full nodes
# maxSplitBlockRange: 1000
# # RPC handler
# rpc:
# # Maximum number of bytes for the response body of getLogs requests
# maxGetLogsResponseBytes: 10 * 1024 * 1024
#
# # Resource usage constraints
# resourceLimits:
# # Maximum response size for 'getLogs' requests (default 10MB)
# maxGetLogsResponseSize: 10485760
#
# # ETH Cache settings
# ethCache:
# # Cache expiration time duration for 'net_version' requests
# netVersionExpiration: 1m
# # Cache expiration time duration for 'eth_clientVersion' requests
# clientVersionExpiration: 1m
# # Cache expiration time duration for 'eth_chainId' requests
# chainIdExpiration: 8760h
# # Cache expiration time duration for 'eth_blockNumber'equests
# blockNumberExpiration: 1s
# # Cache expiration time duration for 'eth_gasPrice'
# priceExpiration: 3s
# # LRU Cache size and expiration time duration for 'eth_call'
# callCacheExpiration: 1s
# callCacheSize: 128

# # Go performance profiling
# pprof:
Expand Down
6 changes: 3 additions & 3 deletions node/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,15 @@ func (p *clientProvider) GetRouteGroup(key string) (grp Group, ok bool) {
}

func (p *clientProvider) cacheLoad(key string) (Group, bool) {
v, expired, found := p.routeKeyCache.GetNoExp(key)
v, expired, found := p.routeKeyCache.GetWithoutExp(key)
if found && !expired { // cache hit
return v.(Group), true
}

p.mu.Lock()
defer p.mu.Unlock()

v, expired, found = p.routeKeyCache.GetNoExp(key)
v, expired, found = p.routeKeyCache.GetWithoutExp(key)
if found && !expired { // double check
return v.(Group), true
}
Expand All @@ -109,7 +109,7 @@ func (p *clientProvider) populateCache(token string) (grp Group, ok bool) {

// for db error, we cache an empty group for the key by which no expiry cache value existed
// so that db pressure can be mitigrated by reducing too many subsequential queries.
if _, _, found := p.routeKeyCache.GetNoExp(token); !found {
if _, _, found := p.routeKeyCache.GetWithoutExp(token); !found {
p.routeKeyCache.Add(token, grp)
}

Expand Down
96 changes: 89 additions & 7 deletions rpc/cache/cache_eth.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,49 @@
package cache

import (
"crypto/md5"
"encoding/hex"
"encoding/json"
"math/big"
"time"

"github.com/Conflux-Chain/confura/node"
"github.com/Conflux-Chain/confura/util/rpc"
"github.com/Conflux-Chain/go-conflux-util/viper"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/mcuadros/go-defaults"
"github.com/openweb3/web3go"
"github.com/openweb3/web3go/types"
"github.com/sirupsen/logrus"
)

var EthDefault = NewEth()
var (
EthDefault *EthCache = newEthCache(newEthCacheConfig())
)

type EthCacheConfig struct {
NetVersionExpiration time.Duration `default:"1m"`
ClientVersionExpiration time.Duration `default:"1m"`
ChainIdExpiration time.Duration `default:"8760h"`
BlockNumberExpiration time.Duration `default:"1s"`
PriceExpiration time.Duration `default:"3s"`
CallCacheExpiration time.Duration `default:"1s"`
CallCacheSize int `default:"128"`
}

// newEthCacheConfig returns a EthCacheConfig with default values.
func newEthCacheConfig() EthCacheConfig {
var cfg EthCacheConfig
defaults.SetDefaults(&cfg)
return cfg
}

func MustInitFromViper() {
config := newEthCacheConfig()
viper.MustUnmarshalKey("requestControl.ethCache", &config)

EthDefault = newEthCache(config)
}

// EthCache memory cache for some evm space RPC methods
type EthCache struct {
Expand All @@ -19,15 +52,17 @@ type EthCache struct {
chainIdCache *expiryCache
priceCache *expiryCache
blockNumberCache *nodeExpiryCaches
callCache *keyExpiryLruCaches
}

func NewEth() *EthCache {
func newEthCache(cfg EthCacheConfig) *EthCache {
return &EthCache{
netVersionCache: newExpiryCache(time.Minute),
clientVersionCache: newExpiryCache(time.Minute),
chainIdCache: newExpiryCache(time.Hour * 24 * 365 * 100),
priceCache: newExpiryCache(3 * time.Second),
blockNumberCache: newNodeExpiryCaches(time.Second),
netVersionCache: newExpiryCache(cfg.NetVersionExpiration),
clientVersionCache: newExpiryCache(cfg.ClientVersionExpiration),
chainIdCache: newExpiryCache(cfg.ChainIdExpiration),
priceCache: newExpiryCache(cfg.PriceExpiration),
blockNumberCache: newNodeExpiryCaches(cfg.BlockNumberExpiration),
callCache: newKeyExpiryLruCaches(cfg.CallCacheExpiration, cfg.CallCacheSize),
}
}

Expand Down Expand Up @@ -92,3 +127,50 @@ func (cache *EthCache) GetBlockNumber(client *node.Web3goClient) (*hexutil.Big,

return (*hexutil.Big)(val.(*big.Int)), nil
}

func (cache *EthCache) Call(client *node.Web3goClient, callRequest types.CallRequest, blockNum *types.BlockNumberOrHash) ([]byte, error) {
nodeName := rpc.Url2NodeName(client.URL)

cacheKey, err := generateCallCacheKey(nodeName, callRequest, blockNum)
if err != nil {
// This should rarely happen, but if it does, we don't want to fail the entire request due to cache error.
// The error is logged and the request is forwarded to the node directly.
logrus.WithFields(logrus.Fields{
"nodeName": nodeName,
"callReq": callRequest,
"blockNum": blockNum,
}).WithError(err).Error("Failed to generate cache key for `eth_call`")
return client.Eth.Call(callRequest, blockNum)
}

val, err := cache.callCache.getOrUpdate(cacheKey, func() (interface{}, error) {
return client.Eth.Call(callRequest, blockNum)
})
if err != nil {
return nil, err
}

return val.([]byte), nil
}

func generateCallCacheKey(nodeName string, callRequest types.CallRequest, blockNum *types.BlockNumberOrHash) (string, error) {
// Create a map of parameters to be serialized
params := map[string]interface{}{
"nodeName": nodeName,
"callRequest": callRequest,
"blockNum": blockNum,
}

// Serialize the parameters to JSON
jsonBytes, err := json.Marshal(params)
if err != nil {
return "", err
}

// Generate MD5 hash
hash := md5.New()
hash.Write(jsonBytes)

// Convert hash to a hexadecimal string
return hex.EncodeToString(hash.Sum(nil)), nil
}
21 changes: 21 additions & 0 deletions rpc/cache/expiry_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,24 @@ func (caches *nodeExpiryCaches) getOrUpdate(node string, updateFunc func() (inte

return val.(*expiryCache).getOrUpdate(updateFunc)
}

// keyExpiryLruCaches caches value with specified expiration time and size using LRU eviction policy.
type keyExpiryLruCaches struct {
key2Caches *util.ExpirableLruCache // cache key => expiryCache
ttl time.Duration
}

func newKeyExpiryLruCaches(ttl time.Duration, size int) *keyExpiryLruCaches {
return &keyExpiryLruCaches{
ttl: ttl,
key2Caches: util.NewExpirableLruCache(size, ttl),
}
}

func (caches *keyExpiryLruCaches) getOrUpdate(cacheKey string, updateFunc func() (interface{}, error)) (interface{}, error) {
val, _ := caches.key2Caches.GetOrUpdate(cacheKey, func() (interface{}, error) {
return newExpiryCache(caches.ttl), nil
})

return val.(*expiryCache).getOrUpdate(updateFunc)
}
10 changes: 5 additions & 5 deletions rpc/handler/cfx_logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,16 @@ var (
errEventLogsTooStale = errors.New("event logs are too stale (already pruned)")
)

func Init() {
var constraint struct {
func MustInitFromViper() {
var resrcLimit struct {
MaxGetLogsResponseBytes uint64 `default:"10485760"` // default 10MB
}
viper.MustUnmarshalKey("constraints.rpc", &constraint)
viper.MustUnmarshalKey("requestControl.resourceLimits", &resrcLimit)

maxGetLogsResponseBytes = constraint.MaxGetLogsResponseBytes
maxGetLogsResponseBytes = resrcLimit.MaxGetLogsResponseBytes
errResponseBodySizeTooLarge = fmt.Errorf(
"result body size is too large with more than %d bytes, please narrow down your filter condition",
constraint.MaxGetLogsResponseBytes,
resrcLimit.MaxGetLogsResponseBytes,
)
}

Expand Down
3 changes: 2 additions & 1 deletion rpc/handler/eth_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"math/big"

"github.com/Conflux-Chain/confura/node"
"github.com/Conflux-Chain/confura/rpc/cache"
"github.com/Conflux-Chain/confura/util/metrics"
"github.com/ethereum/go-ethereum/common"
"github.com/openweb3/web3go/types"
Expand Down Expand Up @@ -104,7 +105,7 @@ func (h *EthStateHandler) Call(
blockNum *types.BlockNumberOrHash,
) ([]byte, error) {
result, err, usefs := h.doRequest(ctx, w3c, func(w3c *node.Web3goClient) (interface{}, error) {
return w3c.Eth.Call(callRequest, blockNum)
return cache.EthDefault.Call(w3c, callRequest, blockNum)
})

metrics.Registry.RPC.Percentage("eth_call", "fullState").Mark(usefs)
Expand Down
6 changes: 5 additions & 1 deletion rpc/server_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"net/http"

"github.com/Conflux-Chain/confura/node"
"github.com/Conflux-Chain/confura/rpc/cache"
"github.com/Conflux-Chain/confura/rpc/handler"
"github.com/Conflux-Chain/confura/util/rate"
"github.com/Conflux-Chain/confura/util/rpc/handlers"
Expand All @@ -20,8 +21,11 @@ const (
)

func MustInit() {
// init cache
cache.MustInitFromViper()

// init handler
handler.Init()
handler.MustInitFromViper()

// init metrics
initMetrics()
Expand Down
2 changes: 1 addition & 1 deletion store/log_filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func initLogFilter() {
MaxSplitBlockRange uint64 `default:"1000"`
}

viper.MustUnmarshalKey("constraints.logfilter", &lfc)
viper.MustUnmarshalKey("requestControl.logfilter", &lfc)

MaxLogBlockHashesSize = lfc.MaxBlockHashCount
MaxLogFilterAddrCount = lfc.MaxAddressCount
Expand Down
Loading
Loading