From f1e98b3bcf7d38b083086a942be73710803cffbe Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria <6909403+Uxio0@users.noreply.github.com> Date: Wed, 15 Apr 2026 16:42:40 +0200 Subject: [PATCH] Invalidate per-contract ABI cache after metadata update get_contract_abi and get_contract_abi_selectors_with_functions were cached indefinitely with alru_cache(maxsize=2048), so updating a contract's ABI in the database (e.g. after a proxy implementation refresh) had no effect until the process restarted. Two changes: - Add ttl=600 to both caches as a safety net so stale entries expire automatically within 10 minutes even if explicit invalidation is missed. - Add DataDecoderService.invalidate_contract_abi_cache() and call it from ContractMetadataService.process_contract_metadata() immediately after a new ABI is linked, so decodings in subsequent requests use the fresh ABI without waiting for the TTL. --- app/services/contract_metadata_service.py | 10 ++++++++++ app/services/data_decoder.py | 22 ++++++++++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/app/services/contract_metadata_service.py b/app/services/contract_metadata_service.py index aca3e8b..642e9e0 100644 --- a/app/services/contract_metadata_service.py +++ b/app/services/contract_metadata_service.py @@ -1,3 +1,4 @@ +# SPDX-License-Identifier: FSL-1.1-MIT import enum import logging from dataclasses import dataclass @@ -22,6 +23,7 @@ from app.config import settings from app.datasources.cache.redis import get_redis from app.datasources.db.models import Abi, AbiSource, Contract +from app.services.data_decoder import get_data_decoder_service logger = logging.getLogger(__name__) @@ -199,6 +201,14 @@ async def process_contract_metadata( contract_metadata.metadata.implementation ) + # Evict stale per-contract ABI cache entries so the decoder + # picks up the newly linked ABI on the next request. + decoder = await get_data_decoder_service() + decoder.invalidate_contract_abi_cache( + contract_metadata.address, + contract_metadata.chain_id, + ) + contract.fetch_retries += 1 await contract.update() return bool(contract_metadata.metadata) diff --git a/app/services/data_decoder.py b/app/services/data_decoder.py index 015e1d8..e517ea7 100644 --- a/app/services/data_decoder.py +++ b/app/services/data_decoder.py @@ -175,7 +175,7 @@ async def get_supported_abis(self) -> AsyncIterator[ABI]: async def get_multisend_abis(self) -> AsyncIterator[ABI]: yield get_multi_send_contract(self.dummy_w3).abi - @alru_cache(maxsize=2048) + @alru_cache(maxsize=2048, ttl=600) async def get_contract_abi( self, address: Address, @@ -190,7 +190,7 @@ async def get_contract_abi( """ return await Contract.get_abi_by_contract_address(HexBytes(address), chain_id) - @alru_cache(maxsize=2048) + @alru_cache(maxsize=2048, ttl=600) async def get_contract_abi_selectors_with_functions( self, address: Address, chain_id: int | None ) -> dict[bytes, ABIFunction] | None: @@ -498,6 +498,24 @@ async def get_decoding_accuracy( return DecodingAccuracyEnum.PARTIAL_MATCH return DecodingAccuracyEnum.ONLY_FUNCTION_MATCH + def invalidate_contract_abi_cache( + self, address: Address | ChecksumAddress, chain_id: int | None + ) -> None: + """ + Evict a specific contract from both per-contract ABI caches. + + Call this whenever a contract's ABI is updated in the database so + that the next decode request re-fetches the latest ABI rather than + serving stale data from the in-memory cache. + + :param address: Contract address + :param chain_id: Chain id for the contract + """ + self.get_contract_abi.cache_invalidate(self, address, chain_id) + self.get_contract_abi_selectors_with_functions.cache_invalidate( + self, address, chain_id + ) + async def add_abi(self, abi: ABI) -> bool: """ Add a new abi without rebuilding the entire decoder