diff --git a/.env b/.env index fc6c8e2..2474685 100644 --- a/.env +++ b/.env @@ -89,16 +89,18 @@ 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 -# RedisEndpointURL is an url of redis +# CACHE_ENABLED specifies if cache should be enabled. By default cache is disabled. +CACHE_ENABLED=true +# REDIS_ENDPOINT_URL is an url of redis REDIS_ENDPOINT_URL=redis:6379 REDIS_PASSWORD= -# TTL for cached evm requests -# TTL should be specified in seconds +# CACHE_TTL is a TTL for cached evm requests +# CACHE_TTL should be specified in seconds CACHE_TTL=600 -# CachePrefix is used as prefix for any key in the cache, key has such structure: +# CACHE_PREFIX is used as prefix for any key in the cache, key has such structure: # :evm-request::sha256: # Possible values are testnet, mainnet, etc... -# CachePrefix must not contain colon symbol +# CACHE_PREFIX must not contain colon symbol CACHE_PREFIX=local-chain ##### Database Config diff --git a/config/config.go b/config/config.go index 256a54a..8260ef3 100644 --- a/config/config.go +++ b/config/config.go @@ -37,6 +37,7 @@ type Config struct { MetricPartitioningRoutineInterval time.Duration MetricPartitioningRoutineDelayFirstRun time.Duration MetricPartitioningPrefillPeriodDays int + CacheEnabled bool RedisEndpointURL string RedisPassword string CacheTTL time.Duration @@ -83,6 +84,7 @@ const ( DEFAULT_DATABASE_READ_TIMEOUT_SECONDS = 60 DATABASE_WRITE_TIMEOUT_SECONDS_ENVIRONMENT_KEY = "DATABASE_WRITE_TIMEOUT_SECONDS" DEFAULT_DATABASE_WRITE_TIMEOUT_SECONDS = 10 + CACHE_ENABLED_ENVIRONMENT_KEY = "CACHE_ENABLED" REDIS_ENDPOINT_URL_ENVIRONMENT_KEY = "REDIS_ENDPOINT_URL" REDIS_PASSWORD_ENVIRONMENT_KEY = "REDIS_PASSWORD" CACHE_TTL_ENVIRONMENT_KEY = "CACHE_TTL" @@ -212,6 +214,7 @@ 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), + CacheEnabled: EnvOrDefaultBool(CACHE_ENABLED_ENVIRONMENT_KEY, false), RedisEndpointURL: os.Getenv(REDIS_ENDPOINT_URL_ENVIRONMENT_KEY), RedisPassword: os.Getenv(REDIS_PASSWORD_ENVIRONMENT_KEY), CacheTTL: time.Duration(EnvOrDefaultInt(CACHE_TTL_ENVIRONMENT_KEY, 0)) * time.Second, diff --git a/main_test.go b/main_test.go index 40a96c3..9415fbc 100644 --- a/main_test.go +++ b/main_test.go @@ -483,7 +483,7 @@ func TestE2ETestProxyTracksBlockNumberForMethodsWithBlockHashParam(t *testing.T) } } -func TestE2eTestCachingMdwWithBlockNumberParam(t *testing.T) { +func TestE2ETestCachingMdwWithBlockNumberParam(t *testing.T) { // create api and database clients client, err := ethclient.Dial(proxyServiceURL) if err != nil { @@ -513,7 +513,7 @@ func TestE2eTestCachingMdwWithBlockNumberParam(t *testing.T) { } { t.Run(tc.desc, func(t *testing.T) { // test cache MISS and cache HIT scenarios for specified method - // check corresponding values in cachemdw.CacheMissHeaderValue HTTP header + // check corresponding values in cachemdw.CacheHeaderKey HTTP header // check that cached and non-cached responses are equal // eth_getBlockByNumber - cache MISS @@ -557,7 +557,7 @@ func TestE2eTestCachingMdwWithBlockNumberParam(t *testing.T) { cleanUpRedis(t, redisClient) } -func TestE2eTestCachingMdwWithBlockNumberParam_EmptyResult(t *testing.T) { +func TestE2ETestCachingMdwWithBlockNumberParam_EmptyResult(t *testing.T) { testRandomAddressHex := "0x6767114FFAA17C6439D7AEA480738B982CE63A02" testAddress := common.HexToAddress(testRandomAddressHex) @@ -590,7 +590,7 @@ func TestE2eTestCachingMdwWithBlockNumberParam_EmptyResult(t *testing.T) { } { t.Run(tc.desc, func(t *testing.T) { // both calls should lead to cache MISS scenario, because empty results aren't cached - // check corresponding values in cachemdw.CacheMissHeaderValue HTTP header + // check corresponding values in cachemdw.CacheHeaderKey HTTP header // check that responses are equal // eth_getBlockByNumber - cache MISS diff --git a/service/cachemdw/cache.go b/service/cachemdw/cache.go index 8114420..9dd8f57 100644 --- a/service/cachemdw/cache.go +++ b/service/cachemdw/cache.go @@ -20,7 +20,8 @@ type ServiceCache struct { cacheTTL time.Duration decodedRequestContextKey any // cachePrefix is used as prefix for any key in the cache - cachePrefix string + cachePrefix string + cacheEnabled bool *logging.ServiceLogger } @@ -31,6 +32,7 @@ func NewServiceCache( cacheTTL time.Duration, decodedRequestContextKey any, cachePrefix string, + cacheEnabled bool, logger *logging.ServiceLogger, ) *ServiceCache { return &ServiceCache{ @@ -39,6 +41,7 @@ func NewServiceCache( cacheTTL: cacheTTL, decodedRequestContextKey: decodedRequestContextKey, cachePrefix: cachePrefix, + cacheEnabled: cacheEnabled, ServiceLogger: logger, } } diff --git a/service/cachemdw/cache_test.go b/service/cachemdw/cache_test.go index 3aef28e..7121a32 100644 --- a/service/cachemdw/cache_test.go +++ b/service/cachemdw/cache_test.go @@ -78,7 +78,15 @@ func TestUnitTestCacheQueryResponse(t *testing.T) { cacheTTL := time.Hour ctxb := context.Background() - serviceCache := cachemdw.NewServiceCache(inMemoryCache, blockGetter, cacheTTL, service.DecodedRequestContextKey, defaultCachePrefixString, &logger) + serviceCache := cachemdw.NewServiceCache( + inMemoryCache, + blockGetter, + cacheTTL, + service.DecodedRequestContextKey, + defaultCachePrefixString, + true, + &logger, + ) req := mkEVMRPCRequestEnvelope(defaultBlockNumber) resp, err := serviceCache.GetCachedQueryResponse(ctxb, req) diff --git a/service/cachemdw/caching_middleware.go b/service/cachemdw/caching_middleware.go index ec41d9f..547b3a1 100644 --- a/service/cachemdw/caching_middleware.go +++ b/service/cachemdw/caching_middleware.go @@ -19,6 +19,12 @@ func (c *ServiceCache) CachingMiddleware( next http.Handler, ) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + // if cache is not enabled - do nothing and forward to next middleware + if !c.cacheEnabled { + next.ServeHTTP(w, r) + return + } + // if we can't get decoded request then forward to next middleware req := r.Context().Value(c.decodedRequestContextKey) decodedReq, ok := (req).(*decode.EVMRPCRequestEnvelope) diff --git a/service/cachemdw/is_cached_middleware.go b/service/cachemdw/is_cached_middleware.go index 3cbc557..6936556 100644 --- a/service/cachemdw/is_cached_middleware.go +++ b/service/cachemdw/is_cached_middleware.go @@ -28,6 +28,12 @@ func (c *ServiceCache) IsCachedMiddleware( next http.Handler, ) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + // if cache is not enabled - do nothing and forward to next middleware + if !c.cacheEnabled { + next.ServeHTTP(w, r) + return + } + uncachedContext := context.WithValue(r.Context(), CachedContextKey, false) cachedContext := context.WithValue(r.Context(), CachedContextKey, true) diff --git a/service/cachemdw/middleware_test.go b/service/cachemdw/middleware_test.go index 03e8eb3..a819a50 100644 --- a/service/cachemdw/middleware_test.go +++ b/service/cachemdw/middleware_test.go @@ -16,7 +16,7 @@ import ( "github.com/kava-labs/kava-proxy-service/service/cachemdw" ) -func TestE2ETestServiceCacheMiddleware(t *testing.T) { +func TestUnitTestServiceCacheMiddleware(t *testing.T) { logger, err := logging.New("TRACE") require.NoError(t, err) @@ -24,7 +24,15 @@ func TestE2ETestServiceCacheMiddleware(t *testing.T) { blockGetter := NewMockEVMBlockGetter() cacheTTL := time.Duration(0) // TTL: no expiry - serviceCache := cachemdw.NewServiceCache(inMemoryCache, blockGetter, cacheTTL, service.DecodedRequestContextKey, defaultCachePrefixString, &logger) + serviceCache := cachemdw.NewServiceCache( + inMemoryCache, + blockGetter, + cacheTTL, + service.DecodedRequestContextKey, + defaultCachePrefixString, + true, + &logger, + ) emptyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) cachingMdw := serviceCache.CachingMiddleware(emptyHandler) @@ -49,6 +57,9 @@ func TestE2ETestServiceCacheMiddleware(t *testing.T) { }) isCachedMdw := serviceCache.IsCachedMiddleware(proxyHandler) + // test cache MISS and cache HIT scenarios for specified method + // check corresponding values in cachemdw.CacheHeaderKey HTTP header + t.Run("cache miss", func(t *testing.T) { req := createTestHttpRequest( t, @@ -81,6 +92,10 @@ func TestE2ETestServiceCacheMiddleware(t *testing.T) { require.Equal(t, http.StatusOK, resp.Code) require.JSONEq(t, testEVMQueries[TestRequestEthBlockByNumberSpecific].ResponseBody, resp.Body.String()) require.Equal(t, cachemdw.CacheHitHeaderValue, resp.Header().Get(cachemdw.CacheHeaderKey)) + + cacheItems := inMemoryCache.GetAll(context.Background()) + require.Len(t, cacheItems, 1) + require.Contains(t, cacheItems, "1:evm-request:eth_getBlockByNumber:sha256:bf79de57723b25b85391513b470ea6989e7c44dd9afc0c270ee961c9f12f578d") }) } diff --git a/service/middleware.go b/service/middleware.go index 82ae226..f332b06 100644 --- a/service/middleware.go +++ b/service/middleware.go @@ -233,9 +233,9 @@ func createProxyRequestMiddleware(next http.Handler, config config.Config, servi response := r.Context().Value(cachemdw.ResponseContextKey) typedResponse, ok := response.([]byte) - // if request is cached and response is present in context - serve the request from the cache + // if cache is enabled, request is cached and response is present in context - serve the request from the cache // otherwise proxy to the actual backend - if isCached && ok { + if config.CacheEnabled && isCached && ok { serviceLogger.Logger.Trace(). Str("method", r.Method). Str("url", r.URL.String()). @@ -271,20 +271,23 @@ func createProxyRequestMiddleware(next http.Handler, config config.Config, servi // extract the original hostname the request was sent to requestHostnameContext := context.WithValue(originRoundtripLatencyContext, RequestHostnameContextKey, r.Host) - var bodyCopy bytes.Buffer - tee := io.TeeReader(lrw.body, &bodyCopy) - // read all body from reader into bodyBytes, and copy into bodyCopy - bodyBytes, err := io.ReadAll(tee) - if err != nil { - serviceLogger.Error().Err(err).Msg("can't read lrw.body") - } + enrichedContext := requestHostnameContext - // replace empty body reader with fresh copy - lrw.body = &bodyCopy - // set body in context - responseContext := context.WithValue(requestHostnameContext, cachemdw.ResponseContextKey, bodyBytes) + // if cache is enabled, update enrichedContext with cachemdw.ResponseContextKey -> bodyBytes key-value pair + if config.CacheEnabled { + var bodyCopy bytes.Buffer + tee := io.TeeReader(lrw.body, &bodyCopy) + // read all body from reader into bodyBytes, and copy into bodyCopy + bodyBytes, err := io.ReadAll(tee) + if err != nil { + serviceLogger.Error().Err(err).Msg("can't read lrw.body") + } - enrichedContext := responseContext + // replace empty body reader with fresh copy + lrw.body = &bodyCopy + // set body in context + enrichedContext = context.WithValue(enrichedContext, cachemdw.ResponseContextKey, bodyBytes) + } // parse the remote address of the request for use below remoteAddressParts := strings.Split(r.RemoteAddr, ":") diff --git a/service/service.go b/service/service.go index a607c67..dbefc22 100644 --- a/service/service.go +++ b/service/service.go @@ -213,6 +213,7 @@ func createServiceCache( config.CacheTTL, DecodedRequestContextKey, config.CachePrefix, + config.CacheEnabled, logger, )