infra/proxyd/cache.go

194 lines
4.7 KiB
Go

package proxyd
import (
"context"
"encoding/json"
"strings"
"time"
"github.com/ethereum/go-ethereum/rpc"
"github.com/redis/go-redis/v9"
"github.com/golang/snappy"
lru "github.com/hashicorp/golang-lru"
)
type Cache interface {
Get(ctx context.Context, key string) (string, error)
Put(ctx context.Context, key string, value string) error
}
const (
// assuming an average RPCRes size of 3 KB
memoryCacheLimit = 4096
)
type cache struct {
lru *lru.Cache
}
func newMemoryCache() *cache {
rep, _ := lru.New(memoryCacheLimit)
return &cache{rep}
}
func (c *cache) Get(ctx context.Context, key string) (string, error) {
if val, ok := c.lru.Get(key); ok {
return val.(string), nil
}
return "", nil
}
func (c *cache) Put(ctx context.Context, key string, value string) error {
c.lru.Add(key, value)
return nil
}
type redisCache struct {
redisClient *redis.Client
redisReadClient *redis.Client
prefix string
ttl time.Duration
}
func newRedisCache(redisClient *redis.Client, redisReadClient *redis.Client, prefix string, ttl time.Duration) *redisCache {
return &redisCache{redisClient, redisReadClient, prefix, ttl}
}
func (c *redisCache) namespaced(key string) string {
if c.prefix == "" {
return key
}
return strings.Join([]string{c.prefix, key}, ":")
}
func (c *redisCache) Get(ctx context.Context, key string) (string, error) {
start := time.Now()
val, err := c.redisReadClient.Get(ctx, c.namespaced(key)).Result()
redisCacheDurationSumm.WithLabelValues("GET").Observe(float64(time.Since(start).Milliseconds()))
if err == redis.Nil {
return "", nil
} else if err != nil {
RecordRedisError("CacheGet")
return "", err
}
return val, nil
}
func (c *redisCache) Put(ctx context.Context, key string, value string) error {
start := time.Now()
err := c.redisClient.SetEx(ctx, c.namespaced(key), value, c.ttl).Err()
redisCacheDurationSumm.WithLabelValues("SETEX").Observe(float64(time.Since(start).Milliseconds()))
if err != nil {
RecordRedisError("CacheSet")
}
return err
}
type cacheWithCompression struct {
cache Cache
}
func newCacheWithCompression(cache Cache) *cacheWithCompression {
return &cacheWithCompression{cache}
}
func (c *cacheWithCompression) Get(ctx context.Context, key string) (string, error) {
encodedVal, err := c.cache.Get(ctx, key)
if err != nil {
return "", err
}
if encodedVal == "" {
return "", nil
}
val, err := snappy.Decode(nil, []byte(encodedVal))
if err != nil {
return "", err
}
return string(val), nil
}
func (c *cacheWithCompression) Put(ctx context.Context, key string, value string) error {
encodedVal := snappy.Encode(nil, []byte(value))
return c.cache.Put(ctx, key, string(encodedVal))
}
type RPCCache interface {
GetRPC(ctx context.Context, req *RPCReq) (*RPCRes, error)
PutRPC(ctx context.Context, req *RPCReq, res *RPCRes) error
}
type rpcCache struct {
cache Cache
handlers map[string]RPCMethodHandler
}
func newRPCCache(cache Cache) RPCCache {
staticHandler := &StaticMethodHandler{cache: cache}
debugGetRawReceiptsHandler := &StaticMethodHandler{cache: cache,
filterGet: func(req *RPCReq) bool {
// cache only if the request is for a block hash
var p []rpc.BlockNumberOrHash
err := json.Unmarshal(req.Params, &p)
if err != nil {
return false
}
if len(p) != 1 {
return false
}
return p[0].BlockHash != nil
},
filterPut: func(req *RPCReq, res *RPCRes) bool {
// don't cache if response contains 0 receipts
rawReceipts, ok := res.Result.([]interface{})
if !ok {
return false
}
return len(rawReceipts) > 0
},
}
handlers := map[string]RPCMethodHandler{
"eth_chainId": staticHandler,
"net_version": staticHandler,
"eth_getBlockTransactionCountByHash": staticHandler,
"eth_getUncleCountByBlockHash": staticHandler,
"eth_getBlockByHash": staticHandler,
"eth_getTransactionByBlockHashAndIndex": staticHandler,
"eth_getUncleByBlockHashAndIndex": staticHandler,
"debug_getRawReceipts": debugGetRawReceiptsHandler,
}
return &rpcCache{
cache: cache,
handlers: handlers,
}
}
func (c *rpcCache) GetRPC(ctx context.Context, req *RPCReq) (*RPCRes, error) {
handler := c.handlers[req.Method]
if handler == nil {
return nil, nil
}
res, err := handler.GetRPCMethod(ctx, req)
if err != nil {
RecordCacheError(req.Method)
return nil, err
}
if res == nil {
RecordCacheMiss(req.Method)
} else {
RecordCacheHit(req.Method)
}
return res, nil
}
func (c *rpcCache) PutRPC(ctx context.Context, req *RPCReq, res *RPCRes) error {
handler := c.handlers[req.Method]
if handler == nil {
return nil
}
return handler.PutRPCMethod(ctx, req, res)
}