Add user stats caching and invalidation functionality#807
Conversation
tenkus47
commented
Jun 24, 2026
- Introduced caching for user statistics with a new timeout configuration.
- Implemented functions to set, get, and invalidate user stats cache in the daily log cache service.
- Updated daily log service to utilize the new caching mechanism for user stats.
- Modified accumulator service to invalidate user stats cache upon updates.
- Enhanced tests to cover new caching behavior and ensure proper functionality of user stats retrieval and invalidation.
- Introduced caching for user statistics with a new timeout configuration. - Implemented functions to set, get, and invalidate user stats cache in the daily log cache service. - Updated daily log service to utilize the new caching mechanism for user stats. - Modified accumulator service to invalidate user stats cache upon updates. - Enhanced tests to cover new caching behavior and ensure proper functionality of user stats retrieval and invalidation.
Confidence Score: 3/5Needs one fix before merging: cache invalidation in The cache invalidation in
Reviews (2): Last reviewed commit: "Remove rate limiting functionality and r..." | Re-trigger Greptile |
| async def get_user_stats_cache(user_id: UUID) -> Optional[UserStatsResponse]: | ||
| cache_key = _build_user_stats_cache_key(user_id=user_id) | ||
| cache_data = await get_cache_data(hash_key=cache_key) | ||
| if cache_data and isinstance(cache_data, dict): | ||
| return UserStatsResponse(**cache_data) | ||
| return None |
There was a problem hiding this comment.
UserStatsResponse(**cache_data) will raise a Pydantic ValidationError if the cached dict doesn't conform to the current model schema — for example, after a deployment that adds a new required field while Redis still holds stale entries from the previous schema. The exception is not caught here, so it propagates through get_user_stats_service and surfaces as a 500 for every user whose stats are cached until all entries expire (up to 5 minutes). Returning None on validation failure causes a graceful cache miss and fresh DB computation instead.
| async def get_user_stats_cache(user_id: UUID) -> Optional[UserStatsResponse]: | |
| cache_key = _build_user_stats_cache_key(user_id=user_id) | |
| cache_data = await get_cache_data(hash_key=cache_key) | |
| if cache_data and isinstance(cache_data, dict): | |
| return UserStatsResponse(**cache_data) | |
| return None | |
| async def get_user_stats_cache(user_id: UUID) -> Optional[UserStatsResponse]: | |
| cache_key = _build_user_stats_cache_key(user_id=user_id) | |
| cache_data = await get_cache_data(hash_key=cache_key) | |
| if cache_data and isinstance(cache_data, dict): | |
| try: | |
| return UserStatsResponse(**cache_data) | |
| except Exception: | |
| logging.warning("Failed to deserialize user stats cache; treating as cache miss", exc_info=True) | |
| return None | |
| return None |
| def schedule_invalidate_user_stats_cache(user_id: UUID) -> None: | ||
| """Invalidate stats cache from sync callers without blocking.""" | ||
| try: | ||
| loop = asyncio.get_running_loop() | ||
| loop.create_task(invalidate_user_stats_cache(user_id)) | ||
| except RuntimeError: | ||
| asyncio.run(invalidate_user_stats_cache(user_id)) |
There was a problem hiding this comment.
loop.create_task() returns a Task but the reference is immediately discarded. When the garbage collector eventually collects the task and it raised an exception, Python emits a "Task exception was never retrieved" warning rather than surfacing the error. Storing the returned task (or attaching a done-callback) ensures unhandled exceptions are at minimum logged.
| def schedule_invalidate_user_stats_cache(user_id: UUID) -> None: | |
| """Invalidate stats cache from sync callers without blocking.""" | |
| try: | |
| loop = asyncio.get_running_loop() | |
| loop.create_task(invalidate_user_stats_cache(user_id)) | |
| except RuntimeError: | |
| asyncio.run(invalidate_user_stats_cache(user_id)) | |
| def schedule_invalidate_user_stats_cache(user_id: UUID) -> None: | |
| """Invalidate stats cache from sync callers without blocking.""" | |
| try: | |
| loop = asyncio.get_running_loop() | |
| task = loop.create_task(invalidate_user_stats_cache(user_id)) | |
| task.add_done_callback( | |
| lambda t: logging.error("Failed to invalidate user stats cache", exc_info=t.exception()) | |
| if not t.cancelled() and t.exception() is not None | |
| else None | |
| ) | |
| except RuntimeError: | |
| asyncio.run(invalidate_user_stats_cache(user_id)) |
- Deleted the rate limiting middleware and its associated configuration from the application. - Removed references to rate limiting in the configuration file and application code. - Eliminated related tests and fixtures to ensure a clean removal of rate limiting features. - Updated dependencies by removing the `slowapi` package from the project.