Skip to content

Commit

Permalink
Basic implementation of Caching Middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
evgeniy-scherbina committed Oct 11, 2023
1 parent 69b86f0 commit d4c21ed
Show file tree
Hide file tree
Showing 19 changed files with 1,262 additions and 26 deletions.
3 changes: 3 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ METRIC_PARTITIONING_ROUTINE_DELAY_FIRST_RUN_SECONDS=10
METRIC_PARTITIONINING_PREFILL_PERIOD_DAYS=7
# Used by `ready` script to ensure metric partitions have been created.
MINIMUM_REQUIRED_PARTITIONS=30
REDIS_ENDPOINT_URL=redis:6379
REDIS_PASSWORD=
CHAIN_ID=local-chain

##### Database Config
POSTGRES_PASSWORD=password
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ kava-proxy-service
cover.html

# Dependency directories (remove the comment below to include it)
# vendor/
vendor/

# ignore editor files
.vscode/
.idea/
16 changes: 16 additions & 0 deletions clients/cache/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package cache

import (
"context"
"errors"
"time"
)

var ErrNotFound = errors.New("value not found in the cache")

type Cache interface {
Set(ctx context.Context, key string, data []byte, expiration time.Duration) error
Get(ctx context.Context, key string) ([]byte, error)
Delete(ctx context.Context, key string) error
Healthcheck(ctx context.Context) error
}
101 changes: 101 additions & 0 deletions clients/cache/inmemory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package cache

import (
"context"
"sync"
"time"
)

// InMemoryCache is an in-memory implementation of the Cache interface.
type InMemoryCache struct {
data map[string]cacheItem
mutex sync.RWMutex
}

// Ensure InMemoryCache implements the Cache interface.
var _ Cache = (*InMemoryCache)(nil)

// cacheItem represents an item stored in the cache.
type cacheItem struct {
data []byte
expiration time.Time
}

// NewInMemoryCache creates a new instance of InMemoryCache.
func NewInMemoryCache() *InMemoryCache {
return &InMemoryCache{
data: make(map[string]cacheItem),
}
}

// Set sets the value of a key in the cache.
func (c *InMemoryCache) Set(
ctx context.Context,
key string,
data []byte,
expiration time.Duration,
) error {
c.mutex.Lock()
defer c.mutex.Unlock()

expiry := time.Now().Add(expiration)

if expiration == 0 {
// 100 years in the future to prevent expiry
expiry = time.Now().AddDate(100, 0, 0)
}

c.data[key] = cacheItem{
data: data,
expiration: expiry,
}

return nil
}

// Get retrieves the value of a key from the cache.
func (c *InMemoryCache) Get(ctx context.Context, key string) ([]byte, error) {
c.mutex.RLock()
defer c.mutex.RUnlock()

item, ok := c.data[key]
if !ok || time.Now().After(item.expiration) {
// Not a real ttl but just replicates it for fetching
delete(c.data, key)

return nil, ErrNotFound
}

return item.data, nil
}

// GetAll returns all the non-expired data in the cache.
func (c *InMemoryCache) GetAll(ctx context.Context) map[string][]byte {
c.mutex.RLock()
defer c.mutex.RUnlock()

result := make(map[string][]byte)

for key, item := range c.data {
if time.Now().After(item.expiration) {
delete(c.data, key)
} else {
result[key] = item.data
}
}

return result
}

// Delete removes a key from the cache.
func (c *InMemoryCache) Delete(ctx context.Context, key string) error {
c.mutex.Lock()
defer c.mutex.Unlock()

delete(c.data, key)
return nil
}

func (c *InMemoryCache) Healthcheck(ctx context.Context) error {
return nil
}
114 changes: 114 additions & 0 deletions clients/cache/redis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package cache

import (
"context"
"fmt"
"time"

"github.com/kava-labs/kava-proxy-service/logging"
"github.com/redis/go-redis/v9"
)

type RedisConfig struct {
Address string
Password string
DB int
}

// RedisCache is an implementation of Cache that uses Redis as the caching backend.
type RedisCache struct {
client *redis.Client
*logging.ServiceLogger
}

var _ Cache = (*RedisCache)(nil)

func NewRedisCache(
cfg *RedisConfig,
logger *logging.ServiceLogger,
) (*RedisCache, error) {
client := redis.NewClient(&redis.Options{
Addr: cfg.Address,
Password: cfg.Password,
DB: cfg.DB,
})

return &RedisCache{
client: client,
ServiceLogger: logger,
}, nil
}

// Set sets the value for the given key in the cache with the given expiration.
func (rc *RedisCache) Set(
ctx context.Context,
key string,
value []byte,
expiration time.Duration,
) error {
rc.Logger.Trace().
Str("key", key).
Str("value", string(value)).
Dur("expiration", expiration).
Msg("setting value in redis")

return rc.client.Set(ctx, key, value, expiration).Err()
}

// Get gets the value for the given key in the cache.
func (rc *RedisCache) Get(
ctx context.Context,
key string,
) ([]byte, error) {
rc.Logger.Trace().
Str("key", key).
Msg("getting value from redis")

val, err := rc.client.Get(ctx, key).Bytes()
if err == redis.Nil {
rc.Logger.Trace().
Str("key", key).
Msgf("value not found in redis")
return nil, ErrNotFound
}
if err != nil {
rc.Logger.Error().
Str("key", key).
Err(err).
Msg("error during getting value from redis")
return nil, err
}

rc.Logger.Trace().
Str("key", key).
Str("value", string(val)).
Msg("successfully got value from redis")

return val, nil
}

// Delete deletes the value for the given key in the cache.
func (rc *RedisCache) Delete(ctx context.Context, key string) error {
rc.Logger.Trace().
Str("key", key).
Msg("deleting value from redis")

return rc.client.Del(ctx, key).Err()
}

func (rc *RedisCache) Healthcheck(ctx context.Context) error {
rc.Logger.Trace().Msg("redis healthcheck was called")

// Check if we can connect to Redis
_, err := rc.client.Ping(ctx).Result()
if err != nil {
rc.Logger.Error().
Err(err).
Msg("can't ping redis")
return fmt.Errorf("error connecting to Redis: %v", err)
}

rc.Logger.Trace().Msg("redis healthcheck was successful")

return nil
}
13 changes: 13 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ type Config struct {
MetricPartitioningRoutineInterval time.Duration
MetricPartitioningRoutineDelayFirstRun time.Duration
MetricPartitioningPrefillPeriodDays int
RedisEndpointURL string
RedisPassword string
CacheTTL time.Duration
ChainID string
}

const (
Expand Down Expand Up @@ -79,6 +83,11 @@ const (
DEFAULT_DATABASE_READ_TIMEOUT_SECONDS = 60
DATABASE_WRITE_TIMEOUT_SECONDS_ENVIRONMENT_KEY = "DATABASE_WRITE_TIMEOUT_SECONDS"
DEFAULT_DATABASE_WRITE_TIMEOUT_SECONDS = 10
REDIS_ENDPOINT_URL_ENVIRONMENT_KEY = "REDIS_ENDPOINT_URL"
REDIS_PASSWORD_ENVIRONMENT_KEY = "REDIS_PASSWORD"
CACHE_TTL_ENVIRONMENT_KEY = "CACHE_TTL"
DEFAULT_CACHE_TTL_SECONDS = 600
CHAIN_ID_ENVIRONMENT_KEY = "CHAIN_ID"
)

// EnvOrDefault fetches an environment variable value, or if not set returns the fallback value
Expand Down Expand Up @@ -204,5 +213,9 @@ func ReadConfig() Config {
MetricPartitioningRoutineInterval: time.Duration(time.Duration(EnvOrDefaultInt(METRIC_PARTITIONING_ROUTINE_INTERVAL_SECONDS_ENVIRONMENT_KEY, DEFAULT_METRIC_PARTITIONING_ROUTINE_INTERVAL_SECONDS)) * time.Second),
MetricPartitioningRoutineDelayFirstRun: time.Duration(time.Duration(EnvOrDefaultInt(METRIC_PARTITIONING_ROUTINE_DELAY_FIRST_RUN_SECONDS_ENVIRONMENT_KEY, DEFAULT_METRIC_PARTITIONING_ROUTINE_DELAY_FIRST_RUN_SECONDS)) * time.Second),
MetricPartitioningPrefillPeriodDays: EnvOrDefaultInt(METRIC_PARTITIONING_PREFILL_PERIOD_DAYS_ENVIRONMENT_KEY, DEFAULT_METRIC_PARTITIONING_PREFILL_PERIOD_DAYS),
RedisEndpointURL: os.Getenv(REDIS_ENDPOINT_URL_ENVIRONMENT_KEY),
RedisPassword: os.Getenv(REDIS_PASSWORD_ENVIRONMENT_KEY),
CacheTTL: time.Duration(EnvOrDefaultInt(CACHE_TTL_ENVIRONMENT_KEY, DEFAULT_CACHE_TTL_SECONDS)) * time.Second,
ChainID: os.Getenv(CHAIN_ID_ENVIRONMENT_KEY),
}
}
19 changes: 12 additions & 7 deletions decode/evm_rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@ import (
"errors"
"fmt"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"

cosmosmath "cosmossdk.io/math"
"github.com/ethereum/go-ethereum/common"
ethctypes "github.com/ethereum/go-ethereum/core/types"
)

// EVMBlockGetter defines an interface which can be implemented by any client capable of getting ethereum block by hash
type EVMBlockGetter interface {
// BlockByHash returns ethereum block by hash
BlockByHash(ctx context.Context, hash common.Hash) (*ethctypes.Block, error)
}

// Errors that might result from decoding parts or the whole of
// an EVM RPC request
var (
Expand Down Expand Up @@ -138,7 +143,7 @@ func DecodeEVMRPCRequest(body []byte) (*EVMRPCRequestEnvelope, error) {
// - the request is a valid evm rpc request
// - the method for the request supports specifying a block number
// - the provided block number is a valid tag or number
func (r *EVMRPCRequestEnvelope) ExtractBlockNumberFromEVMRPCRequest(ctx context.Context, evmClient *ethclient.Client) (int64, error) {
func (r *EVMRPCRequestEnvelope) ExtractBlockNumberFromEVMRPCRequest(ctx context.Context, blockGetter EVMBlockGetter) (int64, error) {
// only attempt to extract block number from a valid ethereum api request
if r.Method == "" {
return 0, ErrInvalidEthAPIRequest
Expand Down Expand Up @@ -173,12 +178,12 @@ func (r *EVMRPCRequestEnvelope) ExtractBlockNumberFromEVMRPCRequest(ctx context.
return parseBlockNumberFromParams(r.Method, r.Params)
}

return lookupBlockNumberFromHashParam(ctx, evmClient, r.Method, r.Params)
return lookupBlockNumberFromHashParam(ctx, blockGetter, r.Method, r.Params)
}

// Generic method to lookup the block number
// based on the hash value in a set of params
func lookupBlockNumberFromHashParam(ctx context.Context, evmClient *ethclient.Client, methodName string, params []interface{}) (int64, error) {
func lookupBlockNumberFromHashParam(ctx context.Context, blockGetter EVMBlockGetter, methodName string, params []interface{}) (int64, error) {
paramIndex, exists := MethodNameToBlockHashParamIndex[methodName]

if !exists {
Expand All @@ -191,7 +196,7 @@ func lookupBlockNumberFromHashParam(ctx context.Context, evmClient *ethclient.Cl
return 0, fmt.Errorf(fmt.Sprintf("error decoding block hash param from params %+v at index %d", params, paramIndex))
}

block, err := evmClient.BlockByHash(ctx, common.HexToHash(blockHash))
block, err := blockGetter.BlockByHash(ctx, common.HexToHash(blockHash))

if err != nil {
return 0, err
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
cosmossdk.io/math v1.0.0
github.com/ethereum/go-ethereum v1.11.2
github.com/google/uuid v1.3.0
github.com/redis/go-redis/v9 v9.2.1
github.com/rs/zerolog v1.29.0
github.com/stretchr/testify v1.8.2
github.com/uptrace/bun v1.1.12
Expand All @@ -18,9 +19,11 @@ require (
require (
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/deckarep/golang-set/v2 v2.1.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fatih/color v1.14.1 // indirect
github.com/go-ole/go-ole v1.2.1 // indirect
github.com/go-stack/stack v1.8.1 // indirect
Expand Down
7 changes: 7 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 h1:fLjPD/aNc3UIO
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/VictoriaMetrics/fastcache v1.6.0 h1:C/3Oi3EiBCqufydp1neRZkqcwmEiuRT9c3fqvvgKm5o=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k=
github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cockroachdb/errors v1.9.1 h1:yFVvsI0VxmRShfawbt/laCIDy/mtTqqnvoNgiy5bEV8=
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE=
github.com/cockroachdb/pebble v0.0.0-20230209160836-829675f94811 h1:ytcWPaNPhNoGMWEhDvS3zToKcDpRsLuRolQJBVGdozk=
Expand All @@ -24,6 +27,8 @@ github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw=
github.com/ethereum/go-ethereum v1.11.2 h1:z/luyejbevDCAMUUiu0rc80dxJxOnpoG58k5o0tSawc=
github.com/ethereum/go-ethereum v1.11.2/go.mod h1:DuefStAgaxoaYGLR0FueVcVbehmn5n9QUcVrMCuOvuc=
Expand Down Expand Up @@ -78,6 +83,8 @@ github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvq
github.com/prometheus/common v0.39.0 h1:oOyhkDq05hPZKItWVBkJ6g6AtGxi+fy7F4JvUV8uhsI=
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
github.com/prometheus/tsdb v0.7.1 h1:YZcsG11NqnK4czYLrWd9mpEuAJIHVQLwdrleYfszMAA=
github.com/redis/go-redis/v9 v9.2.1 h1:WlYJg71ODF0dVspZZCpYmoF1+U1Jjk9Rwd7pq6QmlCg=
github.com/redis/go-redis/v9 v9.2.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
Expand Down
Loading

0 comments on commit d4c21ed

Please sign in to comment.