mirror of
https://github.com/status-im/eth-rpc-proxy.git
synced 2026-08-31 11:41:08 +00:00
290 lines
11 KiB
Lua
290 lines
11 KiB
Lua
local json = require("cjson")
|
|
local cache_rules = require("cache.cache_rules")
|
|
local mlcache = require("resty.mlcache")
|
|
local keydb_l3_cache = require("cache.keydb_l3_cache")
|
|
|
|
local _M = {}
|
|
|
|
-- Initialize mlcache instances for different cache types
|
|
local cache_instances = {}
|
|
|
|
-- Create normalized hash for cache key generation
|
|
local function normalized_hash(decoded_body)
|
|
if not decoded_body or not decoded_body.method then
|
|
return nil
|
|
end
|
|
|
|
local method = decoded_body.method or ""
|
|
local jsonrpc = decoded_body.jsonrpc or "2.0"
|
|
local params_hash = ""
|
|
|
|
-- Generate hash for params if they exist
|
|
if decoded_body.params then
|
|
local params_json = json.encode(decoded_body.params)
|
|
params_hash = ngx.md5(params_json)
|
|
end
|
|
|
|
return method .. ":" .. jsonrpc .. ":" .. params_hash
|
|
end
|
|
|
|
-- Fix the id in cached response to match the current request
|
|
local function fix_response_id(cached_response, request_id)
|
|
if not cached_response or not request_id then
|
|
return cached_response
|
|
end
|
|
|
|
local success, response_data = pcall(json.decode, cached_response)
|
|
if not success or not response_data then
|
|
return cached_response
|
|
end
|
|
|
|
-- If id already matches, return original response to preserve formatting
|
|
if response_data.id == request_id then
|
|
return cached_response
|
|
end
|
|
|
|
-- Update the id to match the current request
|
|
response_data.id = request_id
|
|
|
|
local success_encode, fixed_response = pcall(json.encode, response_data)
|
|
if not success_encode then
|
|
return cached_response
|
|
end
|
|
|
|
return fixed_response
|
|
end
|
|
|
|
-- Function to reset cache instances (for testing)
|
|
function _M.reset_cache_instances()
|
|
cache_instances = {}
|
|
end
|
|
|
|
-- L3 cache callback function for mlcache
|
|
local function l3_callback(cache_key)
|
|
-- Check if L3 cache is enabled
|
|
if not keydb_l3_cache.enabled() then
|
|
ngx.log(ngx.ERR, "L3 cache disabled, skipping for key: ", cache_key)
|
|
return nil
|
|
end
|
|
|
|
local data, err = keydb_l3_cache.get(cache_key)
|
|
if err and err ~= "cache miss" then
|
|
ngx.log(ngx.ERR, "L3 cache error for key ", cache_key, ": ", err)
|
|
return nil
|
|
end
|
|
|
|
-- Log L3 cache activity only when L3 cache is enabled
|
|
local stats_dict = ngx.shared.cache_stats
|
|
if data then
|
|
ngx.log(ngx.ERR, "L3 cache hit for key: ", cache_key)
|
|
stats_dict:incr("l3_cache_hits", 1, 0)
|
|
else
|
|
ngx.log(ngx.ERR, "L3 cache miss for key: ", cache_key)
|
|
stats_dict:incr("l3_cache_misses", 1, 0)
|
|
end
|
|
|
|
return data
|
|
end
|
|
|
|
-- Helper function to get or create mlcache instance
|
|
local function get_cache_instance(cache_type)
|
|
if not cache_instances[cache_type] then
|
|
local shm_name = "rpc_cache"
|
|
if cache_type == "short" then
|
|
shm_name = "rpc_cache_short"
|
|
elseif cache_type == "minimal" then
|
|
shm_name = "rpc_cache_minimal"
|
|
end
|
|
|
|
local cache, err = mlcache.new("mlcache_" .. cache_type, shm_name, {
|
|
lru_size = 1000, -- L1 LRU cache size
|
|
ttl = 3600, -- Default TTL (will be overridden)
|
|
neg_ttl = 30, -- Negative caching TTL
|
|
resurrect_ttl = 30, -- TTL for stale values during refresh
|
|
ipc_shm = "mlcache_ipc", -- IPC shared memory for multi-worker sync
|
|
resty_lock_opts = {
|
|
exptime = 30, -- Lock expiration time
|
|
timeout = 5 -- Lock acquisition timeout
|
|
}
|
|
})
|
|
|
|
if not cache then
|
|
ngx.log(ngx.ERR, "Failed to create mlcache instance for ", cache_type, ": ", err)
|
|
return nil
|
|
end
|
|
|
|
cache_instances[cache_type] = cache
|
|
end
|
|
|
|
return cache_instances[cache_type]
|
|
end
|
|
|
|
-- Helper function to decode JSON once and cache result
|
|
local function get_decoded_body(body_data)
|
|
if not body_data then return nil end
|
|
|
|
-- If already decoded (table), return as is
|
|
if type(body_data) == "table" then
|
|
return body_data
|
|
end
|
|
|
|
local ok, body_json = pcall(json.decode, body_data)
|
|
if not ok or not body_json or not body_json.method then
|
|
return nil
|
|
end
|
|
|
|
return body_json
|
|
end
|
|
|
|
-- Unified cache function that handles all cache operations
|
|
-- Returns cache information along with decoded request body to avoid duplicate JSON parsing
|
|
function _M.check_cache(chain, network, body_data)
|
|
ngx.log(ngx.INFO, "[CACHE_DEBUG] check_cache called - chain: ", chain, ", network: ", network, ", body_length: ", body_data and #body_data or "nil")
|
|
|
|
local decoded_body = get_decoded_body(body_data)
|
|
if not decoded_body then
|
|
ngx.log(ngx.WARN, "[CACHE_DEBUG] Failed to decode body data or missing method")
|
|
return {
|
|
cache_type = nil,
|
|
cache_key = nil,
|
|
ttl = nil,
|
|
cached_response = nil,
|
|
decoded_body = nil
|
|
}
|
|
end
|
|
|
|
ngx.log(ngx.INFO, "[CACHE_DEBUG] Decoded body method: ", decoded_body.method)
|
|
|
|
local cache_info = cache_rules.get_cache_info(chain, network, decoded_body)
|
|
ngx.log(ngx.INFO, "[CACHE_DEBUG] Cache rules result - cache_type: ", cache_info and cache_info.cache_type or "nil",
|
|
", ttl: ", cache_info and cache_info.ttl or "nil")
|
|
|
|
if not cache_info or cache_info.cache_type == "none" or cache_info.ttl == 0 then
|
|
ngx.log(ngx.INFO, "[CACHE_DEBUG] Cache disabled or TTL is 0, not using cache")
|
|
return {
|
|
cache_type = nil,
|
|
cache_key = nil,
|
|
ttl = nil,
|
|
cached_response = nil,
|
|
decoded_body = decoded_body
|
|
}
|
|
end
|
|
|
|
local cache_type = cache_info.cache_type
|
|
local ttl = cache_info.ttl
|
|
|
|
local cache_instance = get_cache_instance(cache_type)
|
|
|
|
if not cache_instance then
|
|
ngx.log(ngx.ERR, "[CACHE_DEBUG] Failed to get cache instance for type: ", cache_type)
|
|
return {
|
|
cache_type = nil,
|
|
cache_key = nil,
|
|
ttl = nil,
|
|
cached_response = nil,
|
|
decoded_body = decoded_body
|
|
}
|
|
end
|
|
|
|
-- Create normalized cache key (without id field for consistent caching)
|
|
local hash_part = normalized_hash(decoded_body)
|
|
if not hash_part then
|
|
ngx.log(ngx.ERR, "[CACHE_DEBUG] Failed to generate normalized hash")
|
|
return {
|
|
cache_type = nil,
|
|
cache_key = nil,
|
|
ttl = nil,
|
|
cached_response = nil,
|
|
decoded_body = decoded_body
|
|
}
|
|
end
|
|
|
|
local cache_key = chain .. ":" .. network .. ":" .. hash_part
|
|
ngx.log(ngx.INFO, "[CACHE_DEBUG] Generated cache key: ", cache_key, " for cache_type: ", cache_type, " with TTL: ", ttl)
|
|
|
|
-- Use L3 callback when getting from cache
|
|
local cached_response, err, hit_level = cache_instance:get(cache_key, { ttl = ttl }, l3_callback, cache_key)
|
|
ngx.log(ngx.INFO, "[CACHE_DEBUG] Cache lookup result - hit_level: ", hit_level or "nil",
|
|
", has_response: ", cached_response and "yes" or "no", ", error: ", err or "none")
|
|
local stats_dict = ngx.shared.cache_stats
|
|
|
|
if cached_response and cached_response ~= ngx.null then
|
|
-- Fix the id in cached response to match current request
|
|
cached_response = fix_response_id(cached_response, decoded_body.id)
|
|
|
|
-- Increment cache hit counter based on hit level
|
|
if hit_level == 1 then
|
|
stats_dict:incr("l1_cache_hits_" .. cache_type, 1, 0)
|
|
ngx.log(ngx.INFO, "[CACHE_DEBUG] L1 Cache HIT for key: ", cache_key)
|
|
elseif hit_level == 2 then
|
|
stats_dict:incr("l2_cache_hits_" .. cache_type, 1, 0)
|
|
ngx.log(ngx.INFO, "[CACHE_DEBUG] L2 Cache HIT for key: ", cache_key)
|
|
end
|
|
stats_dict:incr("cache_hits_" .. cache_type, 1, 0)
|
|
stats_dict:incr("cache_hits_total", 1, 0)
|
|
ngx.log(ngx.INFO, "[CACHE_DEBUG] Cache HIT - returning cached response for key: ", cache_key)
|
|
else
|
|
-- Increment cache miss counter
|
|
stats_dict:incr("cache_misses_" .. cache_type, 1, 0)
|
|
stats_dict:incr("cache_misses_total", 1, 0)
|
|
cached_response = nil
|
|
ngx.log(ngx.INFO, "[CACHE_DEBUG] Cache MISS for key: ", cache_key)
|
|
end
|
|
|
|
-- Increment total requests counter
|
|
stats_dict:incr("total_requests_" .. cache_type, 1, 0)
|
|
stats_dict:incr("total_requests_all", 1, 0)
|
|
|
|
return {
|
|
cache_type = cache_type,
|
|
cache_key = cache_key,
|
|
ttl = ttl,
|
|
cached_response = cached_response,
|
|
decoded_body = decoded_body,
|
|
cache_instance = cache_instance
|
|
}
|
|
end
|
|
|
|
-- Save function that uses cache_info and also saves to L3 cache
|
|
function _M.save_to_cache(cache_info, response_body)
|
|
ngx.log(ngx.INFO, "[CACHE_DEBUG] save_to_cache called - cache_type: ", cache_info.cache_type or "nil",
|
|
", cache_key: ", cache_info.cache_key or "nil", ", ttl: ", cache_info.ttl or "nil")
|
|
|
|
if not cache_info.cache_type or not cache_info.cache_key or not cache_info.ttl or not cache_info.cache_instance then
|
|
ngx.log(ngx.WARN, "[CACHE_DEBUG] Missing cache info parameters, cannot save to cache")
|
|
return false
|
|
end
|
|
|
|
-- Save to L1/L2 cache (mlcache)
|
|
local success, err = cache_info.cache_instance:set(cache_info.cache_key, { ttl = cache_info.ttl }, response_body)
|
|
if not success then
|
|
ngx.log(ngx.ERR, "[CACHE_DEBUG] Failed to cache response to L1/L2 (", cache_info.cache_type, "): ", err)
|
|
else
|
|
ngx.log(ngx.INFO, "[CACHE_DEBUG] Successfully saved to L1/L2 cache - key: ", cache_info.cache_key,
|
|
", cache_type: ", cache_info.cache_type)
|
|
end
|
|
|
|
local l3_success = true -- Default to true if L3 is disabled
|
|
|
|
-- Save to L3 cache (KeyDB) only if enabled
|
|
if keydb_l3_cache.enabled() then
|
|
ngx.log(ngx.INFO, "[CACHE_DEBUG] L3 cache is enabled, attempting to save to KeyDB")
|
|
l3_success, l3_err = keydb_l3_cache.set(cache_info.cache_key, response_body, cache_info.ttl)
|
|
if not l3_success then
|
|
ngx.log(ngx.ERR, "[CACHE_DEBUG] Failed to cache response to L3 (", cache_info.cache_type, "): ", l3_err)
|
|
else
|
|
ngx.log(ngx.INFO, "[CACHE_DEBUG] Successfully saved to L3 cache - key: ", cache_info.cache_key)
|
|
end
|
|
else
|
|
ngx.log(ngx.INFO, "[CACHE_DEBUG] L3 cache disabled, skipping save for key: ", cache_info.cache_key)
|
|
end
|
|
|
|
local final_result = success or l3_success
|
|
ngx.log(ngx.INFO, "[CACHE_DEBUG] Cache save result - L1/L2_success: ", success and "true" or "false",
|
|
", L3_success: ", l3_success and "true" or "false", ", final_result: ", final_result and "true" or "false")
|
|
|
|
-- Return true if at least one cache succeeded
|
|
return final_result
|
|
end
|
|
|
|
return _M |