-
-
Notifications
You must be signed in to change notification settings - Fork 42
feat: valkey #1179
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: valkey #1179
Conversation
- Introduced a comprehensive caching layer that includes both in-memory and Valkey (Redis-compatible) backends. - Added CacheService for managing Valkey connections, along with AsyncCacheBackend, InMemoryBackend, and TTLCache for efficient data storage and retrieval. - Implemented GuildConfigCacheManager and JailStatusCache for managing guild configurations and jail statuses, respectively, with support for asynchronous operations. - Enhanced cache management with TTL functionality to automatically expire entries, improving performance and resource utilization.
… documentation - Revised caching best practices to include support for Valkey backend, multi-guild safety, and detailed architecture of the caching system. - Enhanced descriptions of cache managers and their async operations, emphasizing the importance of proper cache invalidation and configuration. - Updated environment variable documentation to include Valkey configuration options, ensuring clarity on required settings for optimal caching performance.
- Updated .env.example to include new Valkey environment variables for host, port, database, and password. - Enhanced config.json.example by adding BOT_INTENTS to support additional bot functionalities, improving configuration clarity for users.
- Implemented an optional health check for Valkey (cache) in the database health command. - Updated health check logic to report the status of the Valkey connection when configured. - Enhanced documentation and comments to reflect the inclusion of Valkey in health checks.
- Added CacheSetupService to manage Valkey cache connections during bot initialization. - Integrated CacheSetupService into BotSetupOrchestrator for seamless setup. - Updated PermissionSetupService to utilize cache backend for database coordination. - Enhanced logging for cache connection status and backend wiring.
- Added CacheService to the Tux bot for managing Valkey cache connections. - Implemented cache connection closure logic in the bot's shutdown process, enhancing resource management. - Improved logging for cache connection status during shutdown to aid in debugging.
…ission management - Replaced in-memory cache with a backend cache for command permission fallbacks, improving scalability and performance. - Updated cache invalidation and retrieval logic to utilize the new cache backend, ensuring consistent behavior across command permissions. - Enhanced logging for cache operations to aid in debugging and monitoring.
- Added support for retrieving and storing guild prefixes using a cache backend, improving performance and scalability. - Updated prefix retrieval and setting methods to utilize the cache, ensuring consistent behavior across guilds. - Enhanced cache invalidation logic to remove prefixes from the cache when necessary, maintaining data integrity.
- Enhanced DatabaseCoordinator to support an optional cache backend for permission controllers, improving performance and scalability. - Updated GuildConfigController and Permission controllers to utilize the cache backend for caching and invalidating configurations and permissions. - Refactored cache logic to ensure consistent behavior across different controllers, maintaining data integrity and improving response times.
- Modified JailStatusCache calls in the jail and unjail modules to use async methods for cache invalidation, ensuring proper handling of asynchronous operations. - Cleaned up import statements to consistently use the new cache import path across modules. - Updated validation checks in utility functions for improved clarity and type safety.
…upport - Changed `get` and `invalidate` methods in `CommunicationService` to be asynchronous, ensuring proper handling of cache operations. - Updated import statements to reflect the correct cache manager path, maintaining consistency across the module.
- Updated settings.py to include new Valkey configuration parameters: VALKEY_HOST, VALKEY_PORT, VALKEY_DB, VALKEY_PASSWORD, and VALKEY_URL. - Modified documentation to reflect the use of Valkey in environment variable settings. - Implemented a computed property for constructing the Valkey URL from its components, enhancing flexibility in configuration.
- Deleted the TTLCache class and its associated methods from the cache module, streamlining the caching logic. - Updated the shared module to reflect the removal of TTLCache, ensuring the `__all__` variable is now an empty list. - This change simplifies the caching mechanism and prepares for future enhancements in cache management.
- Introduced unit tests for various cache components, including InMemoryBackend, ValkeyBackend, and CacheService, ensuring robust coverage of caching operations. - Implemented tests for cache health checks and shared cache management, validating the behavior of cache interactions and configurations. - Enhanced performance tests for GuildConfigCacheManager and JailStatusCache, ensuring efficient operation under load. - Established a structured testing framework for cache-related functionalities, improving maintainability and reliability of the caching system.
- Added Valkey as an optional cache backend in the Additional tools section. - Updated directory structure to reflect the new cache layer. - Included instructions for starting Valkey and configuring the environment variable for shared cache usage. - Clarified the caching mechanism, detailing the fallback to in-memory cache when Valkey is not set.
- Included Valkey version 6.1.1 in pyproject.toml and updated uv.lock to reflect the new package. - This addition supports the integration of Valkey for caching functionalities in the project.
- Introduced a new service for Valkey in the Docker Compose file, enabling caching functionalities. - Configured health checks, logging options, and volume management for persistent data storage. - Updated the volumes section to include a dedicated volume for Valkey data.
Reviewer's GuideIntroduces a new cache layer with optional Valkey (Redis-compatible) backend and wires it through bot setup, permission controllers, prefix manager, guild config/jail status managers, and health checks, replacing the old tux.shared.cache usage and updating docs/tests to match. Sequence diagram for get_command_permission with Valkey backendsequenceDiagram
actor User
participant Cog as CommandCog
participant PermSystem as PermissionSystem
participant Backend as AsyncCacheBackend
participant DBCoord as DatabaseCoordinator
participant CmdCtrl as PermissionCommandController
participant DB as Database
User->>Cog: invoke command
Cog->>PermSystem: get_command_permission(guild_id, command_name)
PermSystem->>Backend: get(PERM_FALLBACK_KEY_PREFIX + guild_id + command_name)
alt cache hit
Backend-->>PermSystem: cached_raw
PermSystem->>PermSystem: unwrap_optional_perm(cached_raw)
PermSystem-->>Cog: PermissionCommand | None
Cog-->>User: authorize or deny
else cache miss
PermSystem->>PermSystem: compute command_names_to_check
PermSystem->>DBCoord: command_permissions.get_command_permission(guild_id, command_name) or batch
DBCoord->>CmdCtrl: get_command_permission(...)
CmdCtrl->>DB: SELECT PermissionCommand WHERE guild_id, command_name
DB-->>CmdCtrl: PermissionCommand | None
CmdCtrl-->>DBCoord: result
DBCoord-->>PermSystem: result
PermSystem->>Backend: set(PERM_FALLBACK_KEY_PREFIX + guild_id + command_name, wrap_optional_perm(result), ttl_sec=PERM_FALLBACK_TTL)
PermSystem-->>Cog: result
Cog-->>User: authorize or deny
end
Sequence diagram for JailStatusCache.get_or_fetch with shared backendsequenceDiagram
actor Moderator
participant Cog as JailCog
participant JailCache as JailStatusCache
participant Backend as AsyncCacheBackend
participant Service as ModerationService
participant DB as Database
Moderator->>Cog: check is_jailed(guild_id, user_id)
Cog->>JailCache: get_or_fetch(guild_id, user_id, fetch_func)
JailCache->>Backend: get(jail_status:guild_id:user_id)
alt cached status exists
Backend-->>JailCache: bool
JailCache-->>Cog: bool
Cog-->>Moderator: result
else cache miss
Backend-->>JailCache: None
JailCache->>JailCache: acquire per-user lock
JailCache->>Backend: get(jail_status:guild_id:user_id)
alt filled while waiting
Backend-->>JailCache: bool
JailCache-->>Cog: bool
Cog-->>Moderator: result
else still missing
Backend-->>JailCache: None
JailCache->>Service: fetch_func() (get_latest_jail_or_unjail_case)
Service->>DB: query latest case
DB-->>Service: Case | None
Service-->>JailCache: is_jailed: bool
JailCache->>Backend: set(jail_status:guild_id:user_id, is_jailed, ttl=JAIL_STATUS_TTL_SEC)
JailCache-->>Cog: is_jailed
Cog-->>Moderator: result
end
end
Class diagram for cache layer and permission-related integrationclassDiagram
class TTLCache {
+float _ttl
+int~optional~ _max_size
+dict cache
+__init__(ttl: float, max_size: int | None)
+get(key: Any) Any | None
+set(key: Any, value: Any) None
+invalidate(key: Any | None) None
+get_or_fetch(key: Any, fetch_fn: Callable) Any
+size() int
+clear() None
}
class AsyncCacheBackend {
<<interface>>
+get(key: str) Any | None
+set(key: str, value: Any, ttl_sec: float | None) None
+delete(key: str) None
+exists(key: str) bool
}
class InMemoryBackend {
-TTLCache _cache
-float _default_ttl
+__init__(default_ttl: float, max_size: int | None)
+get(key: str) Any | None
+set(key: str, value: Any, ttl_sec: float | None) None
+delete(key: str) None
+exists(key: str) bool
}
class ValkeyBackend {
-Any _client
-str _prefix
+__init__(client: Any)
+get(key: str) Any | None
+set(key: str, value: Any, ttl_sec: float | None) None
+delete(key: str) None
+exists(key: str) bool
-_key(key: str) str
}
class CacheService {
-Valkey _client
+__init__()
+connect(url: str | None, **kwargs: Any) None
+is_connected() bool
+get_client() Valkey | None
+ping() bool
+close() None
}
class GuildConfigCacheManager {
-static GuildConfigCacheManager _instance
-AsyncCacheBackend _backend
-TTLCache _cache
-dict~int, asyncio.Lock~ _locks
+__new__() GuildConfigCacheManager
+set_backend(backend: AsyncCacheBackend) None
+get(guild_id: int) dict~str, int | None~ | None
+set(guild_id: int, audit_log_id: int | None, mod_log_id: int | None, jail_role_id: int | None, jail_channel_id: int | None) None
+async_set(guild_id: int, audit_log_id: int | None, mod_log_id: int | None, jail_role_id: int | None, jail_channel_id: int | None) None
+invalidate(guild_id: int) None
+clear_all() None
-_get_lock(guild_id: int) asyncio.Lock
-_cache_key(guild_id: int) str
}
class JailStatusCache {
-static JailStatusCache _instance
-AsyncCacheBackend _backend
-TTLCache _cache
-dict~tuple~int,int~, asyncio.Lock~ _locks
-asyncio.Lock _locks_lock
+__new__() JailStatusCache
+set_backend(backend: AsyncCacheBackend) None
+get(guild_id: int, user_id: int) bool | None
+set(guild_id: int, user_id: int, is_jailed: bool) None
+get_or_fetch(guild_id: int, user_id: int, fetch_func: Callable) bool
+async_set(guild_id: int, user_id: int, is_jailed: bool) None
+invalidate(guild_id: int, user_id: int) None
+invalidate_guild(guild_id: int) None
+clear_all() None
-_cache_key(guild_id: int, user_id: int) str
-_get_lock_key(guild_id: int, user_id: int) tuple~int,int~
-_get_lock(guild_id: int, user_id: int) asyncio.Lock
}
class TuxBot {
+DatabaseService db_service
+CacheService cache_service
+DatabaseCoordinator _db_coordinator
}
class DatabaseCoordinator {
-DatabaseService db
-AsyncCacheBackend _cache_backend
+__init__(db: DatabaseService | None, cache_backend: AsyncCacheBackend | None)
+permission_ranks PermissionRankController
+permission_assignments PermissionAssignmentController
+command_permissions PermissionCommandController
}
class PermissionRankController {
-AsyncCacheBackend _backend
-TTLCache _ranks_cache
-TTLCache _guild_ranks_cache
+__init__(db: DatabaseService | None, cache_backend: AsyncCacheBackend | None)
+create_permission_rank(...)
+get_permission_ranks_by_guild(guild_id: int)
+update_permission_rank(...)
+bulk_create_permission_ranks(...)
+delete_permission_rank(...)
}
class PermissionAssignmentController {
-AsyncCacheBackend _backend
-TTLCache _assignments_cache
-TTLCache _user_rank_cache
+__init__(db: DatabaseService | None, cache_backend: AsyncCacheBackend | None)
+assign_permission_rank(...)
+get_assignments_by_guild(guild_id: int)
+remove_role_assignment(...)
+get_user_permission_rank(guild_id: int, user_id: int, user_roles: list~int~) int
}
class PermissionCommandController {
-AsyncCacheBackend _backend
-TTLCache _command_permissions_cache
+__init__(db: DatabaseService | None, cache_backend: AsyncCacheBackend | None)
+set_command_permission(...)
+get_command_permission(guild_id: int, command_name: str) PermissionCommand | None
}
class PermissionSystem {
-TTLCache _command_permission_cache
-AsyncCacheBackend _cache_backend
+__init__(bot: TuxBot, db: DatabaseCoordinator)
+set_command_permission(...)
+get_command_permission(guild_id: int, command_name: str) PermissionCommand | None
+batch_get_command_permissions(guild_id: int, command_names: list~str~) dict~str, PermissionCommand | None~
}
class PrefixManager {
-dict~int,str~ _prefix_cache
-bool _cache_loaded
+get_prefix(guild_id: int | None) str
+set_prefix(guild_id: int, prefix: str) None
+load_all_prefixes() None
+invalidate_cache(guild_id: int | None) None
}
AsyncCacheBackend <|.. InMemoryBackend
AsyncCacheBackend <|.. ValkeyBackend
InMemoryBackend o-- TTLCache
GuildConfigCacheManager o-- TTLCache
JailStatusCache o-- TTLCache
CacheService --> ValkeyBackend
CacheService --> Valkey
TuxBot o-- CacheService
TuxBot o-- DatabaseCoordinator
DatabaseCoordinator o-- PermissionRankController
DatabaseCoordinator o-- PermissionAssignmentController
DatabaseCoordinator o-- PermissionCommandController
PermissionRankController ..> AsyncCacheBackend
PermissionAssignmentController ..> AsyncCacheBackend
PermissionCommandController ..> AsyncCacheBackend
PermissionSystem ..> AsyncCacheBackend
PrefixManager ..> AsyncCacheBackend
GuildConfigCacheManager ..> AsyncCacheBackend
JailStatusCache ..> AsyncCacheBackend
File-Level Changes
Assessment against linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
This comment was marked as off-topic.
This comment was marked as off-topic.
|
Caution Review failedThe pull request is closed. 📝 WalkthroughWalkthroughAdds an async-first cache subsystem with optional Valkey backend: new cache package (backends, TTL, managers, CacheService), wiring into bot startup and core systems, Valkey config/compose/docs, health-check integration, permission/prefix/DB controllers updated to use backend, legacy shared cache removed, and comprehensive tests added. Changes
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: Organization UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (7)
✏️ Tip: You can disable this entire section by setting Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This comment was marked as resolved.
This comment was marked as resolved.
📚 Documentation Preview
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Summary
This PR adds comprehensive Valkey (Redis-compatible) caching support to Tux, providing an optional shared cache layer that persists across restarts. The implementation includes:
- Cache Service: Connection management with retry logic and graceful fallback
- Backend Abstraction: Protocol-based design supporting both in-memory and Valkey backends
- Cache Managers: Singleton managers for guild config and jail status with async APIs
- Integration: Seamless integration into existing permission system and prefix management
Critical Issues Found
Security Vulnerability: The Valkey URL construction exposes passwords in connection strings, creating a credential exposure risk through logs and error messages.
Logic Error: InMemoryBackend ignores the ttl_sec parameter, causing inconsistent behavior between backends.
Recommendations
- Fix the security vulnerability by avoiding password inclusion in URLs or implementing secure URL handling
- Implement proper TTL handling in InMemoryBackend to maintain consistency with ValkeyBackend
- Add Valkey password validation similar to existing PostgreSQL validation
- Address the incomplete guild invalidation behavior when using Valkey backend
The overall architecture is well-designed with proper fallback mechanisms and thread safety considerations. Once the critical issues are resolved, this will be a valuable addition to the caching infrastructure.
You can now have the agent implement changes and create commits directly on your pull request's source branch. Simply comment with /q followed by your request in natural language to ask the agent to make changes.
❌ 5 Tests Failed:
View the top 2 failed test(s) by shortest run time
View the full list of 3 ❄️ flaky test(s)
To view more test analytics, go to the [Prevent Tests Dashboard](https://All Things Linux.sentry.io/prevent/tests/?preventPeriod=30d&integratedOrgName=allthingslinux&repository=tux&branch=1164-valkey) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey - I've found 5 issues, and left some high level feedback:
- Several previously synchronous cache APIs are now async (e.g.
PrefixManager.invalidate_cache,CommunicationService.invalidate_guild_config_cache,GuildConfigCacheManager.invalidate/set), so it’s worth double‑checking all call sites to ensure they nowawaitthese methods to avoid un-awaited coroutine warnings and logic silently not running. - In
JailStatusCache.invalidate_guildthe current implementation clears the entire in-memory cache and ignores theguild_idargument (and does not touch Valkey keys), which may be surprising to callers; consider either scoping invalidation to the given guild or renaming/documenting this as a global clear to avoid misuse.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Several previously synchronous cache APIs are now async (e.g. `PrefixManager.invalidate_cache`, `CommunicationService.invalidate_guild_config_cache`, `GuildConfigCacheManager.invalidate`/`set`), so it’s worth double‑checking all call sites to ensure they now `await` these methods to avoid un-awaited coroutine warnings and logic silently not running.
- In `JailStatusCache.invalidate_guild` the current implementation clears the entire in-memory cache and ignores the `guild_id` argument (and does not touch Valkey keys), which may be surprising to callers; consider either scoping invalidation to the given guild or renaming/documenting this as a global clear to avoid misuse.
## Individual Comments
### Comment 1
<location> `src/tux/cache/backend.py:32-41` </location>
<code_context>
+ """Get a value by key."""
+ return self._cache.get(key)
+
+ async def set(
+ self,
+ key: str,
+ value: Any,
+ ttl_sec: float | None = None,
+ ) -> None:
+ """Set a value with optional TTL."""
+ self._cache.set(key, value)
+
+ async def delete(self, key: str) -> None:
</code_context>
<issue_to_address>
**issue (bug_risk):** InMemoryBackend ignores per-call ttl_sec, which diverges from the Valkey backend semantics.
`InMemoryBackend.set` ignores `ttl_sec` and always relies on the `TTLCache` default TTL. Callers that pass different TTLs (e.g. `PERM_RANKS_TTL`, `PERM_USER_RANK_TTL`) will see those respected only with Valkey, not with the in-memory backend, causing divergent caching behavior.
To align behavior, either:
- Use separate `InMemoryBackend` instances per logical cache with appropriate `default_ttl`, or
- Extend `TTLCache` to support per-entry TTL and apply `ttl_sec`.
If fixed in-memory TTL is intentional, consider updating the docstring and removing `ttl_sec` from this backend’s interface to avoid misleading callers.
</issue_to_address>
### Comment 2
<location> `src/tux/core/prefix_manager.py:267-270` </location>
<code_context>
self._cache_loaded = True
- def invalidate_cache(self, guild_id: int | None = None) -> None:
+ async def invalidate_cache(self, guild_id: int | None = None) -> None:
"""
Invalidate prefix cache for a specific guild or all guilds.
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Changing invalidate_cache to async and only deleting from backend for a single guild may leave inconsistencies when clearing all guilds.
In the `guild_id is None` branch, only the in-memory cache is cleared; any existing Valkey entries remain and will be treated as fresh on subsequent `get_prefix` calls, repopulating `_prefix_cache` from the backend. If this path is meant to perform a full reset, consider also clearing/invalidation on the backend, or explicitly document that this performs an in-memory–only reset.
Also, since `invalidate_cache` is now `async`, verify that all existing call sites `await` it so that invalidation still actually runs.
Suggested implementation:
```python
async def invalidate_cache(self, guild_id: int | None = None) -> None:
"""
Invalidate prefix cache for a specific guild or all guilds.
When ``guild_id`` is ``None``, this performs an in-memory–only reset:
the in-memory prefix cache is cleared, but any entries in the backing
store (e.g. Valkey) are left untouched and will be treated as fresh on
subsequent :meth:`get_prefix` calls.
When a specific ``guild_id`` is provided, both in-memory and backend
state for that guild should be invalidated.
Examples
--------
>>> await manager.invalidate_cache(123456789) # Specific guild
>>> await manager.invalidate_cache() # All guilds (in-memory only)
"""
# In-memory only reset: clear all cached prefixes and mark cache as unloaded
if guild_id is None:
self._prefix_cache.clear()
self._cache_loaded = False
return
# Per-guild reset: remove from in-memory cache and backend
self._prefix_cache.pop(guild_id, None)
self._cache_loaded = False
# Backend invalidation for a single guild should mirror how prefixes
# are stored in the backing store (e.g. Valkey). This call is async,
# so invalidate_cache must be awaited by callers.
if hasattr(self, "_valkey") and self._valkey is not None:
# Adjust key construction to match the rest of the class.
key = f"prefix:{guild_id}"
await self._valkey.delete(key)
```
1. If this class already has a helper for building prefix keys (e.g. `_prefix_key(guild_id)` or similar), replace the inline `key = f"prefix:{guild_id}"` with that helper to keep key construction consistent.
2. If the backing store attribute is named something other than `_valkey` (for example `self._backend`, `self._cache`, etc.), update the `hasattr(self, "_valkey")` block to use the correct attribute and delete API.
3. Search for all call sites of `invalidate_cache(` across the codebase (e.g. `git grep "invalidate_cache("`) and ensure each call is now awaited, for example:
- Replace `manager.invalidate_cache(guild_id)` with `await manager.invalidate_cache(guild_id)` inside async functions.
- Propagate `async`/`await` up the call chain where necessary so that invalidation is actually executed.
4. If you decide that clearing *all* guild prefixes should also clear the backend (not just in-memory), extend the `guild_id is None` branch to iterate over and delete all prefix keys in the backing store using your existing key pattern and backend API (e.g. a scan-and-delete on `prefix:*`).
</issue_to_address>
### Comment 3
<location> `tests/cache/__init__.py:1` </location>
<code_context>
+"""Tests for the cache layer (backends, service, shared cache with backend)."""
</code_context>
<issue_to_address>
**suggestion (testing):** TTLCache itself is not covered by tests, so expiry and eviction semantics are only indirectly exercised.
Given `TTLCache` is now shared by `InMemoryBackend` and the cache managers, please add focused unit tests for it (e.g., `tests/cache/test_ttl_cache.py`). In particular, cover: (1) expiration after the configured TTL, (2) `max_size` eviction order, (3) `get_or_fetch` correctness (no stale values, writes occur as expected), and (4) `clear`/`invalidate` behavior, so these semantics aren’t only tested indirectly via other components.
</issue_to_address>
### Comment 4
<location> `src/tux/database/controllers/permissions.py:85` </location>
<code_context>
+ and jail_channel_id, or None if not cached.
+ """
+ key = self._cache_key(guild_id)
+ if self._backend is not None:
+ value = await self._backend.get(key)
+ return (
</code_context>
<issue_to_address>
**issue (complexity):** Consider introducing a shared cache adapter that encapsulates backend vs local-cache behavior so the permission controllers focus on domain logic instead of repeated branching and serialization code.
Consider extracting the backend-vs-local branching into a small internal cache adapter so controllers stay focused on rank/assignment logic. Most methods only differ in key prefixing, serialization, and invalidate/get/set calls. A helper like the following would remove the repeated `if self._backend` blocks and keep functionality intact:
```python
class PermissionCacheAdapter:
def __init__(self, backend, local_cache, key_prefix: str, ttl: float):
self._backend = backend
self._local = local_cache
self._prefix = key_prefix
self._ttl = ttl
async def get_models(self, key: str, model_type: type[list[BaseModel]]) -> list[BaseModel]:
full_key = f"{self._prefix}{key}"
if self._backend:
raw = await self._backend.get(full_key)
return [model_type.model_validate(d) for d in raw] if raw else None
return self._local.get(key)
async def set_models(self, key: str, models: list[BaseModel]) -> None:
full_key = f"{self._prefix}{key}"
payload = [m.model_dump() for m in models]
if self._backend:
await self._backend.set(full_key, payload, ttl_sec=self._ttl)
else:
self._local.set(key, models)
async def invalidate(self, key: str) -> None:
full_key = f"{self._prefix}{key}"
if self._backend:
await self._backend.delete(full_key)
else:
self._local.invalidate(key)
```
Then controllers call `await self._rank_cache.invalidate(f"permission_ranks:{guild_id}")` or `await self._assignments_cache.get_models(...)` instead of repeating the branching and serialization each time. This keeps command/rank logic readable while preserving the new backend functionality.
</issue_to_address>
### Comment 5
<location> `src/tux/cache/managers.py:90` </location>
<code_context>
+ """Get a value by key. Return None if missing or expired."""
+ ...
+
+ async def set(
+ self,
+ key: str,
</code_context>
<issue_to_address>
**issue (complexity):** Consider refactoring the two `set`/`async_set` methods into a single async `set` with optional locking so the merge and backend logic is defined only once.
Unify the duplicate `set` / `async_set` implementations so the merge logic lives in one async method with optional locking; this trims API surface and removes partial divergences (`_MISSING`, lock handling) without changing behavior. For example:
```python
async def set(
self,
guild_id: int,
audit_log_id: int | None = _MISSING,
mod_log_id: int | None = _MISSING,
jail_role_id: int | None = _MISSING,
jail_channel_id: int | None = _MISSING,
*, use_lock: bool = False,
) -> None:
lock = self._get_lock(guild_id) if use_lock else asyncnullcontext()
async with lock:
key = self._cache_key(guild_id)
existing = await self._backend.get(key) if self._backend else self._cache.get(key) or {}
updated = {**existing}
if audit_log_id is not _MISSING:
updated["audit_log_id"] = audit_log_id
# …repeat for other fields…
if self._backend:
await self._backend.set(key, updated, ttl_sec=GUILD_CONFIG_TTL_SEC)
else:
self._cache.set(key, updated)
```
`asyncnullcontext` can be a tiny helper returning an async context manager that does nothing. Callers who need concurrency safety pass `use_lock=True`; others keep the simpler path, and there’s no repeated merge logic.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This is a substantial and well-executed feature. The introduction of a unified cache layer with an optional Valkey backend is a great improvement for scalability and performance. The new tux.cache package is well-structured with clear separation of concerns between the service, backends, and managers. The refactoring of existing code to use the new async cache managers is consistent and thorough. The documentation updates are excellent and will be very helpful for developers. I've found a couple of areas for improvement, one of which is a high-severity bug in the cache invalidation logic. Overall, great work on this complex feature.
| async def set( | ||
| self, | ||
| key: str, | ||
| value: Any, | ||
| ttl_sec: float | None = None, | ||
| ) -> None: | ||
| """Set a value with optional TTL.""" | ||
| self._cache.set(key, value) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The InMemoryBackend.set method signature includes ttl_sec, but the implementation ignores it, always using the default TTL set during initialization. This differs from ValkeyBackend, which respects ttl_sec. This inconsistency can lead to subtle bugs when switching between backends, as cache expiration behavior will change unexpectedly. To improve consistency, consider modifying TTLCache to support per-key TTLs, and then use ttl_sec here if provided. Alternatively, log a warning if ttl_sec is provided but ignored.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
10 issues found across 40 files
Confidence score: 2/5
invalidate_guildinsrc/tux/cache/managers.pyclears the entire cache instead of only the target guild, which is a concrete data-loss/behavior regression risk.src/tux/cache/backend.pyignoresttl_sec, creating inconsistent TTL behavior between backends and potential stale data issues.- Severity includes multiple medium-to-high issues affecting cache correctness and behavior, so merge risk is elevated despite some being fixable.
- Pay close attention to
src/tux/cache/managers.py,src/tux/cache/backend.py,src/tux/cache/ttl.py- cache invalidation and TTL semantics are inconsistent.
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them.
<file name="src/tux/core/bot.py">
<violation number="1" location="src/tux/core/bot.py:473">
P3: Missing `span.set_data("connections.cache_error_type", type(e).__name__)` to be consistent with the database and HTTP error handlers in this method.</violation>
</file>
<file name="src/tux/shared/config/settings.py">
<violation number="1" location="src/tux/shared/config/settings.py:352">
P2: Password should be URL-encoded to handle special characters. If `VALKEY_PASSWORD` contains characters like `@`, `:`, or `/`, the URL will be malformed and connection will fail.</violation>
</file>
<file name="src/tux/cache/service.py">
<violation number="1" location="src/tux/cache/service.py:86">
P2: Misleading log: `Valkey.from_url()` doesn't establish a connection - it creates a client with a lazy connection pool. The actual TCP connection happens on first command. Consider verifying connectivity with `await self._client.ping()` before logging success, or change the message to "Valkey client initialized".</violation>
</file>
<file name="src/tux/cache/backend.py">
<violation number="1" location="src/tux/cache/backend.py:86">
P1: The `ttl_sec` parameter is accepted but silently ignored. The underlying `TTLCache.set()` doesn't support per-key TTL, causing inconsistent behavior between `ValkeyBackend` (which honors TTL) and `InMemoryBackend` (which doesn't).
Consider either:
1. Modifying `TTLCache` to support per-key TTL, or
2. Creating a new cache entry with custom expiration time, or
3. Documenting that per-key TTL is not supported for in-memory backend</violation>
</file>
<file name="src/tux/core/permission_system.py">
<violation number="1" location="src/tux/core/permission_system.py:529">
P2: Sequential `await` in loop defeats batch efficiency. Use `asyncio.gather()` to fetch all cache entries concurrently, especially important with network caches like Valkey.</violation>
</file>
<file name="src/tux/cache/managers.py">
<violation number="1" location="src/tux/cache/managers.py:187">
P1: Bug: `invalidate_guild` clears ALL cache entries instead of only entries for the specified guild. The method should filter entries by guild_id prefix, not call `self._cache.clear()`. This will cause data loss for all cached jail statuses when trying to invalidate a single guild.</violation>
<violation number="2" location="src/tux/cache/managers.py:296">
P2: Misleading method behavior: `async_set` has 'set if not exists' semantics but the name and docstring suggest it should update the value. If an entry already exists, the method silently returns without updating, which could cause stale jail status data. Either rename to `set_if_not_exists` or remove the early-return logic to match the expected 'set' behavior.</violation>
</file>
<file name="src/tux/cache/ttl.py">
<violation number="1" location="src/tux/cache/ttl.py:94">
P2: Unnecessary eviction when updating existing key at max capacity. The eviction check should skip if the key already exists in the cache, since updating won't increase the size.</violation>
<violation number="2" location="src/tux/cache/ttl.py:143">
P2: `get_or_fetch` cannot cache `None` values. Since `get()` returns `None` for cache misses, there's no way to distinguish between "key not found" and "cached value is `None`". If `fetch_fn()` returns `None`, subsequent calls will re-fetch instead of using the cached value. Consider using a sentinel object or returning a wrapper/tuple from `get()`.</violation>
</file>
<file name="compose.yaml">
<violation number="1" location="compose.yaml:55">
P2: `tux-valkey` is added without a profile, so it will start by default and contradicts the file’s documented behavior that only `tux-postgres` starts without profiles. If Valkey is meant to be optional, add a dedicated profile (e.g., `valkey`) so it only runs when explicitly requested.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
- Added details about the new Valkey cache backend and its integration with existing caching mechanisms. - Documented improvements in cache handling, including async operations and performance optimizations. - Updated sections on Docker Compose and Python version changes for clarity. - Included fixes related to cache management and logging consistency.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/tux/core/prefix_manager.py (1)
158-175: Avoid returning the default prefix when backend caching fails.
Ifbackend.set(...)raises, the outerexceptreturns the default prefix even though the DB read succeeded and the in-memory cache is already updated. Consider isolating backend write errors so the real prefix is still returned.🛠️ Proposed fix
prefix = guild_config.prefix self._prefix_cache[guild_id] = prefix - backend = get_cache_backend(self.bot) - await backend.set(f"prefix:{guild_id}", prefix, ttl_sec=None) + try: + backend = get_cache_backend(self.bot) + await backend.set(f"prefix:{guild_id}", prefix, ttl_sec=None) + except Exception as e: + logger.warning( + "Failed to cache prefix for guild {}: {}", + guild_id, + type(e).__name__, + )
🤖 Fix all issues with AI agents
In `@src/tux/services/moderation/moderation_coordinator.py`:
- Around line 350-369: The asyncio.gather call with return_exceptions=True may
return asyncio.CancelledError (a BaseException) for cancelled tasks, which the
current isinstance(..., Exception) checks miss; update the post-gather checks
for case_result and dm_result (from case_task/dm_task) to treat BaseException
(or specifically asyncio.CancelledError) as an error: if the result is an
instance of BaseException or asyncio.CancelledError, log via
logger.error/logger.warning as currently done and set case = None or dm_sent =
False respectively, otherwise cast to Case or bool as before.
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
CHANGELOG.mdcompose.yamldocs/content/developer/best-practices/caching.mdsrc/tux/cache/managers.pysrc/tux/core/prefix_manager.pysrc/tux/modules/moderation/jail.pysrc/tux/services/moderation/moderation_coordinator.py
🚧 Files skipped from review as they are similar to previous changes (1)
- compose.yaml
🧰 Additional context used
📓 Path-based instructions (6)
**/*.md
📄 CodeRabbit inference engine (.cursor/rules/rules.mdc)
**/*.md: Follow master documentation rules as defined in docs/docs.mdc
Follow practical documentation examples and templates as defined in docs/patterns.mdc
Follow documentation organization structure as defined in docs/structure.mdc
Follow documentation writing standards as defined in docs/style.mdc
Files:
docs/content/developer/best-practices/caching.mdCHANGELOG.md
**/*.py
📄 CodeRabbit inference engine (.cursor/rules/rules.mdc)
**/*.py: Follow Python code style guide and best practices as defined in core/style-guide.mdc
Follow error handling patterns as defined in error-handling/patterns.mdc
Follow loguru logging patterns as defined in error-handling/logging.mdc
Follow Sentry integration patterns as defined in error-handling/sentry.mdc
**/*.py: Use strict type hints with union syntax (Type | NonenotOptional[Type]) in Python
Use NumPy docstring format in Python
Prefer absolute imports; relative imports are allowed within the same module in Python
Group imports in order: stdlib → third-party → local, with blank lines separating groups in Python
Use 88 character line length limit in Python
Use snake_case for functions and variables, PascalCase for classes, UPPER_CASE for constants in Python
Always add imports to the top of the file unless absolutely necessary in Python
Use async/await for all I/O operations in Python
Log errors and events with context information in Python
Keep individual source files under 1600 lines
Include complete type hints for all function parameters and return values
Include NumPy-format docstrings for all public APIs
Files:
src/tux/modules/moderation/jail.pysrc/tux/core/prefix_manager.pysrc/tux/services/moderation/moderation_coordinator.pysrc/tux/cache/managers.py
src/tux/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
src/tux/**/*.py: Use custom exceptions for business logic errors in Python
Handle Discord rate limits gracefully in API calls
Files:
src/tux/modules/moderation/jail.pysrc/tux/core/prefix_manager.pysrc/tux/services/moderation/moderation_coordinator.pysrc/tux/cache/managers.py
src/tux/modules/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
src/tux/modules/**/*.py: Use hybrid commands (slash + traditional) for Discord command modules
Implement role-based permission checks for Discord commands
Implement cooldowns and rate limiting in Discord commands
Files:
src/tux/modules/moderation/jail.py
**/services/**/*.py
📄 CodeRabbit inference engine (.cursor/rules/rules.mdc)
Follow DatabaseService patterns as defined in database/services.mdc
Files:
src/tux/services/moderation/moderation_coordinator.py
src/tux/services/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
Implement dependency injection pattern for services in Python
Files:
src/tux/services/moderation/moderation_coordinator.py
🧠 Learnings (5)
📚 Learning: 2026-01-25T18:11:17.210Z
Learnt from: CR
Repo: allthingslinux/tux PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-25T18:11:17.210Z
Learning: Applies to src/tux/shared/cache.py : Use TTL cache system for frequently accessed data (guild config, jail status, permissions)
Applied to files:
docs/content/developer/best-practices/caching.mdsrc/tux/cache/managers.py
📚 Learning: 2026-01-25T18:11:17.210Z
Learnt from: CR
Repo: allthingslinux/tux PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-25T18:11:17.210Z
Learning: Applies to src/tux/modules/**/*.py : Implement role-based permission checks for Discord commands
Applied to files:
src/tux/modules/moderation/jail.py
📚 Learning: 2026-01-25T18:11:17.210Z
Learnt from: CR
Repo: allthingslinux/tux PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-25T18:11:17.210Z
Learning: Applies to src/tux/modules/**/*.py : Implement cooldowns and rate limiting in Discord commands
Applied to files:
src/tux/modules/moderation/jail.pysrc/tux/services/moderation/moderation_coordinator.py
📚 Learning: 2026-01-25T18:11:17.210Z
Learnt from: CR
Repo: allthingslinux/tux PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-25T18:11:17.210Z
Learning: Applies to src/tux/database/service.py : Use psycopg async connection pooling for PostgreSQL database access
Applied to files:
src/tux/core/prefix_manager.py
📚 Learning: 2026-01-25T18:11:17.210Z
Learnt from: CR
Repo: allthingslinux/tux PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-25T18:11:17.210Z
Learning: Applies to src/tux/core/bot.py : Pre-warm cache on bot startup for frequently accessed data
Applied to files:
src/tux/core/prefix_manager.py
🧬 Code graph analysis (3)
src/tux/core/prefix_manager.py (2)
tests/cache/test_shared_cache_backend.py (1)
cache(133-140)src/tux/cache/backend.py (1)
get_cache_backend(165-193)
src/tux/services/moderation/moderation_coordinator.py (2)
src/tux/database/controllers/__init__.py (1)
case(153-157)src/tux/database/models/models.py (1)
Case(569-735)
src/tux/cache/managers.py (2)
tests/cache/test_shared_cache_backend.py (1)
cache(133-140)src/tux/cache/backend.py (6)
get(28-29)get(71-73)get(122-135)delete(39-40)delete(88-90)delete(151-154)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Seer Code Review
- GitHub Check: cubic · AI code reviewer
- GitHub Check: Run All Tests (3.13.11)
🔇 Additional comments (8)
src/tux/services/moderation/moderation_coordinator.py (2)
8-11: No issues to note in the import update.
311-327: Post-action DM block looks fine.CHANGELOG.md (1)
24-45: Changelog entry for Valkey/cache is clear and well-scoped.
Nicely summarizes the new backend, lifecycle, and integration touchpoints.docs/content/developer/best-practices/caching.md (1)
117-135: Backend vs in-memory TTL behavior is explained clearly.
The contrast between Valkey and fallback behavior is easy to follow.src/tux/cache/managers.py (2)
65-70: Thread-safe lock creation in GuildConfigCacheManager looks solid.
Good protection against concurrent lock creation.
334-367: Stampede protection inget_or_fetchis well implemented.
The double-checked lock pattern is clean and safe.src/tux/modules/moderation/jail.py (1)
130-186: Cooldown + post-rejail cache invalidation are good improvements.
They reinforce rate limiting and cache freshness without altering behavior.src/tux/core/prefix_manager.py (1)
273-306: Backend invalidation path now mirrors in-memory clearing.
Good to see full-cache invalidation deleting backend keys for all cached guilds.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
…nator - Updated exception checks in the ModerationCoordinator class to use BaseException for more comprehensive error handling. - This change improves the robustness of the moderation process by ensuring all exception types are properly logged and managed.
- Added exception handling around the caching process in the PrefixManager class to log warnings when caching fails. - This change enhances the robustness of prefix management by ensuring that errors during caching are properly logged, aiding in troubleshooting.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2 issues found across 2 files (changes from recent commits).
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them.
<file name="src/tux/services/moderation/moderation_coordinator.py">
<violation number="1" location="src/tux/services/moderation/moderation_coordinator.py:355">
P2: Handling BaseException here swallows task cancellations (and other non-Exception BaseExceptions) returned by asyncio.gather, so cancellation won’t propagate. Re-raise non-Exception BaseExceptions before logging.</violation>
<violation number="2" location="src/tux/services/moderation/moderation_coordinator.py:363">
P2: This BaseException check will swallow cancellations from the DM task; re-raise non-Exception BaseExceptions so cancellation propagates correctly.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| except Exception as e: | ||
| # DM failed, but continue | ||
| logger.warning(f"Failed to send post-action DM to user {user.id}: {e}") | ||
| if isinstance(case_result, BaseException): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: Handling BaseException here swallows task cancellations (and other non-Exception BaseExceptions) returned by asyncio.gather, so cancellation won’t propagate. Re-raise non-Exception BaseExceptions before logging.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/tux/services/moderation/moderation_coordinator.py, line 355:
<comment>Handling BaseException here swallows task cancellations (and other non-Exception BaseExceptions) returned by asyncio.gather, so cancellation won’t propagate. Re-raise non-Exception BaseExceptions before logging.</comment>
<file context>
@@ -352,15 +352,15 @@ async def execute_moderation_action( # noqa: PLR0912, PLR0915
return_exceptions=True,
)
- if isinstance(case_result, Exception):
+ if isinstance(case_result, BaseException):
logger.error(
f"Failed to create case for {case_type.value} on user {user.id}: {case_result!r}",
</file context>
| case = None | ||
| else: | ||
| case = cast(Case, case_result) | ||
| if isinstance(dm_result, BaseException): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: This BaseException check will swallow cancellations from the DM task; re-raise non-Exception BaseExceptions so cancellation propagates correctly.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/tux/services/moderation/moderation_coordinator.py, line 363:
<comment>This BaseException check will swallow cancellations from the DM task; re-raise non-Exception BaseExceptions so cancellation propagates correctly.</comment>
<file context>
@@ -352,15 +352,15 @@ async def execute_moderation_action( # noqa: PLR0912, PLR0915
else:
case = cast(Case, case_result)
- if isinstance(dm_result, Exception):
+ if isinstance(dm_result, BaseException):
logger.warning(
f"Failed to send post-action DM to user {user.id}: {dm_result}",
</file context>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/tux/core/prefix_manager.py (1)
280-313: Ensure local cache invalidation happens even if backend delete fails.Currently, if
backend.delete()raises an exception, it preventsself._prefix_cache.clear()orself._prefix_cache.pop()from executing, leaving stale in-memory cache. Use tolerant deletes and always clear local state.For the all-guilds case, use
asyncio.gather(..., return_exceptions=True)to capture errors without raising. For the single-guild case, wrapbackend.delete()in try/except. Then proceed with local cache clearing regardless of backend success.Proposed fix
backend = get_cache_backend(self.bot) if guild_id is None: keys_to_delete = list(self._prefix_cache.keys()) if keys_to_delete: - await asyncio.gather( - *(backend.delete(f"prefix:{gid}") for gid in keys_to_delete), - ) + results = await asyncio.gather( + *(backend.delete(f"prefix:{gid}") for gid in keys_to_delete), + return_exceptions=True, + ) + for gid, result in zip(keys_to_delete, results): + if isinstance(result, Exception): + logger.warning( + "Failed to delete prefix cache key for guild {}: {}", + gid, + type(result).__name__, + ) self._prefix_cache.clear() self._cache_loaded = False logger.debug("All prefix cache invalidated") else: - await backend.delete(f"prefix:{guild_id}") + try: + await backend.delete(f"prefix:{guild_id}") + except Exception as e: + logger.warning( + "Failed to delete prefix cache key for guild {}: {}", + guild_id, + type(e).__name__, + ) self._prefix_cache.pop(guild_id, None) logger.debug(f"Prefix cache invalidated for guild {guild_id}")
🤖 Fix all issues with AI agents
In `@src/tux/core/prefix_manager.py`:
- Around line 96-106: The backend read in PrefixManager.get_prefix is unguarded
and can raise, causing prefix resolution to fail; wrap the call to
backend.get(key) in a try/except, catch Exception, log a warning with context
(include guild_id and the exception) using the module's logger, and on exception
simply skip returning from the cache-backed path so the function falls back to
the DB/in-memory path; only set self._prefix_cache[guild_id] and return when
backend.get succeeds and returns a valid str.
- Around line 132-135: Surround the cache write call that uses
get_cache_backend(self.bot) and await backend.set(f"prefix:{guild_id}", prefix,
ttl_sec=None) with a try/except so backend failures are logged and swallowed
instead of propagating; in the except block log the error with context (guild_id
and prefix) using the module/class logger (e.g., self.logger or a module logger)
and include exception details (exc_info or logger.exception) but do not
re-raise, ensuring DB persistence continues even if the cache write fails.
🧹 Nitpick comments (2)
src/tux/services/moderation/moderation_coordinator.py (1)
412-427: Consider applying the sameBaseExceptionfix here for consistency.This unchanged code block uses
isinstance(..., Exception)for handlingasyncio.gatherresults, which has the same issue that was just fixed in lines 355 and 363. Ifresponse_taskormod_log_taskgets cancelled, theasyncio.CancelledErrorwould not be caught by theExceptioncheck.This is a pre-existing issue and not blocking for this PR, but for consistency you may want to update these checks as well.
♻️ Suggested consistency fix
# Log any exceptions from response embed - if isinstance(response_result, Exception): + if isinstance(response_result, BaseException): logger.error( f"Failed to send response embed for {case_type.value}: {response_result}", exc_info=response_result, ) # Log any exceptions from mod log embed - if isinstance(mod_log_result, Exception): + if isinstance(mod_log_result, BaseException): logger.error( f"Failed to send mod log embed for case #{case.case_number}: {mod_log_result}", exc_info=mod_log_result, )src/tux/core/prefix_manager.py (1)
251-264: Make bulk backend writes resilient to partial failures.
A singlebackend.setexception will abort the batch and skip remaining writes. Considerreturn_exceptions=Truewith per-guild warnings so in-memory loading remains clean.As per coding guidelines: Follow error handling patterns as defined in error-handling/patterns.mdc; Log errors and events with context information in Python.🔧 Proposed refactor
backend = get_cache_backend(self.bot) write_tasks: list[Coroutine[Any, Any, None]] = [] + write_ids: list[int] = [] for config in all_configs: self._prefix_cache[config.id] = config.prefix + write_ids.append(config.id) write_tasks.append( backend.set( f"prefix:{config.id}", config.prefix, ttl_sec=None, ), ) if write_tasks: - await asyncio.gather(*write_tasks) + results = await asyncio.gather( + *write_tasks, + return_exceptions=True, + ) + for guild_id, result in zip(write_ids, results): + if isinstance(result, Exception): + logger.warning( + "Failed to cache prefix for guild {}: {}", + guild_id, + type(result).__name__, + )
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/tux/core/prefix_manager.pysrc/tux/services/moderation/moderation_coordinator.py
🧰 Additional context used
📓 Path-based instructions (4)
**/*.py
📄 CodeRabbit inference engine (.cursor/rules/rules.mdc)
**/*.py: Follow Python code style guide and best practices as defined in core/style-guide.mdc
Follow error handling patterns as defined in error-handling/patterns.mdc
Follow loguru logging patterns as defined in error-handling/logging.mdc
Follow Sentry integration patterns as defined in error-handling/sentry.mdc
**/*.py: Use strict type hints with union syntax (Type | NonenotOptional[Type]) in Python
Use NumPy docstring format in Python
Prefer absolute imports; relative imports are allowed within the same module in Python
Group imports in order: stdlib → third-party → local, with blank lines separating groups in Python
Use 88 character line length limit in Python
Use snake_case for functions and variables, PascalCase for classes, UPPER_CASE for constants in Python
Always add imports to the top of the file unless absolutely necessary in Python
Use async/await for all I/O operations in Python
Log errors and events with context information in Python
Keep individual source files under 1600 lines
Include complete type hints for all function parameters and return values
Include NumPy-format docstrings for all public APIs
Files:
src/tux/services/moderation/moderation_coordinator.pysrc/tux/core/prefix_manager.py
**/services/**/*.py
📄 CodeRabbit inference engine (.cursor/rules/rules.mdc)
Follow DatabaseService patterns as defined in database/services.mdc
Files:
src/tux/services/moderation/moderation_coordinator.py
src/tux/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
src/tux/**/*.py: Use custom exceptions for business logic errors in Python
Handle Discord rate limits gracefully in API calls
Files:
src/tux/services/moderation/moderation_coordinator.pysrc/tux/core/prefix_manager.py
src/tux/services/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
Implement dependency injection pattern for services in Python
Files:
src/tux/services/moderation/moderation_coordinator.py
🧠 Learnings (3)
📚 Learning: 2026-01-25T18:11:17.210Z
Learnt from: CR
Repo: allthingslinux/tux PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-25T18:11:17.210Z
Learning: Applies to src/tux/modules/**/*.py : Implement cooldowns and rate limiting in Discord commands
Applied to files:
src/tux/services/moderation/moderation_coordinator.py
📚 Learning: 2026-01-25T18:11:17.210Z
Learnt from: CR
Repo: allthingslinux/tux PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-25T18:11:17.210Z
Learning: Applies to src/tux/database/service.py : Use psycopg async connection pooling for PostgreSQL database access
Applied to files:
src/tux/core/prefix_manager.py
📚 Learning: 2026-01-25T18:11:17.210Z
Learnt from: CR
Repo: allthingslinux/tux PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-25T18:11:17.210Z
Learning: Applies to src/tux/core/bot.py : Pre-warm cache on bot startup for frequently accessed data
Applied to files:
src/tux/core/prefix_manager.py
🧬 Code graph analysis (2)
src/tux/services/moderation/moderation_coordinator.py (2)
src/tux/database/controllers/__init__.py (1)
case(153-157)src/tux/database/models/models.py (1)
Case(569-735)
src/tux/core/prefix_manager.py (1)
src/tux/cache/backend.py (1)
get_cache_backend(165-193)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
- GitHub Check: Seer Code Review
- GitHub Check: Validate
- GitHub Check: Run All Tests (3.13.11)
- GitHub Check: cubic · AI code reviewer
🔇 Additional comments (5)
src/tux/services/moderation/moderation_coordinator.py (3)
11-11: LGTM!The
castimport is correctly added and used for type narrowing after theasyncio.gatherresults.
311-327: LGTM!The post-action DM handling for non-removal actions follows the established pattern and correctly allows the workflow to continue even if DM sending fails. The
_handle_post_action_dmmethod already handlesasyncio.CancelledErrorinternally, so the outerExceptioncatch is appropriate here.
350-370: LGTM - properly handlesasyncio.CancelledErrornow.The change from
isinstance(..., Exception)toisinstance(..., BaseException)correctly addresses the previous review concern. Sinceasyncio.CancelledErrorinherits fromBaseException(notException), this ensures cancelled tasks returned byasyncio.gather(return_exceptions=True)are properly caught and logged rather than being mis-cast.src/tux/core/prefix_manager.py (2)
19-25: Import additions look good.
173-181: Good defensive caching guard.
The backend write is isolated so load failures don’t break DB reads.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| # Sync mirror for error extractor and fast path | ||
| if guild_id in self._prefix_cache: | ||
| return self._prefix_cache[guild_id] | ||
|
|
||
| # Backend (Valkey or in-memory) when available | ||
| backend = get_cache_backend(self.bot) | ||
| key = f"prefix:{guild_id}" | ||
| backend_val = await backend.get(key) | ||
| if backend_val is not None and isinstance(backend_val, str): | ||
| self._prefix_cache[guild_id] = backend_val | ||
| return backend_val |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard backend reads so prefix resolution can’t fail.
If backend.get raises, get_prefix fails and command handling can break. Wrap the read and fall back to the DB path with a warning.
🔧 Proposed fix
backend = get_cache_backend(self.bot)
key = f"prefix:{guild_id}"
- backend_val = await backend.get(key)
- if backend_val is not None and isinstance(backend_val, str):
- self._prefix_cache[guild_id] = backend_val
- return backend_val
+ try:
+ backend_val = await backend.get(key)
+ except Exception as e:
+ logger.warning(
+ "Failed to read prefix from backend for guild {}: {}",
+ guild_id,
+ type(e).__name__,
+ )
+ else:
+ if backend_val is not None and isinstance(backend_val, str):
+ self._prefix_cache[guild_id] = backend_val
+ return backend_val🤖 Prompt for AI Agents
In `@src/tux/core/prefix_manager.py` around lines 96 - 106, The backend read in
PrefixManager.get_prefix is unguarded and can raise, causing prefix resolution
to fail; wrap the call to backend.get(key) in a try/except, catch Exception, log
a warning with context (include guild_id and the exception) using the module's
logger, and on exception simply skip returning from the cache-backed path so the
function falls back to the DB/in-memory path; only set
self._prefix_cache[guild_id] and return when backend.get succeeds and returns a
valid str.
| # Write to backend when available (no TTL; prefix is long-lived) | ||
| backend = get_cache_backend(self.bot) | ||
| await backend.set(f"prefix:{guild_id}", prefix, ttl_sec=None) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don’t let backend write failures abort prefix updates.
A backend error currently bubbles up and can fail the command. Log and continue so DB persistence still happens.
🔧 Proposed fix
# Write to backend when available (no TTL; prefix is long-lived)
backend = get_cache_backend(self.bot)
- await backend.set(f"prefix:{guild_id}", prefix, ttl_sec=None)
+ try:
+ await backend.set(f"prefix:{guild_id}", prefix, ttl_sec=None)
+ except Exception as e:
+ logger.warning(
+ "Failed to cache prefix for guild {}: {}",
+ guild_id,
+ type(e).__name__,
+ )🤖 Prompt for AI Agents
In `@src/tux/core/prefix_manager.py` around lines 132 - 135, Surround the cache
write call that uses get_cache_backend(self.bot) and await
backend.set(f"prefix:{guild_id}", prefix, ttl_sec=None) with a try/except so
backend failures are logged and swallowed instead of propagating; in the except
block log the error with context (guild_id and prefix) using the module/class
logger (e.g., self.logger or a module logger) and include exception details
(exc_info or logger.exception) but do not re-raise, ensuring DB persistence
continues even if the cache write fails.
- Updated the error handling in the ValkeyBackend class to delete non-JSON keys and return None instead of the raw value. This change enhances the robustness of cache management by ensuring that invalid data does not persist in the cache.
- Updated the method for fetching the current database revision to use `scalar()` for better reliability and clarity. - Enhanced logging to provide a clearer message when no migrations have been applied, improving debugging and monitoring of database states.
…ands - Added a new method to set up permission checks for commands that utilize dynamic permissions, ensuring users receive "permission denied" messages before encountering missing argument errors. - Enhanced error logging to capture any issues during the setup of permission checks, improving maintainability and debugging.
- Introduced a new `command_permission_check` function to facilitate permission checks during the command check phase, ensuring users receive immediate feedback on permission issues before argument conversion. - Enhanced the existing `requires_command_permission` decorator to support dynamic permissions and streamline permission handling for commands.
…rmissions - Introduced `invalidate_command_permission_cache` method to clear the command permission fallback cache for a specific command and its parents. - This method should be called after modifying command permissions outside of the standard permission setting methods, ensuring that subsequent permission checks reflect the latest data. - Added logging to trace cache invalidation actions for better monitoring and debugging.
- Added `invalidate_command_permission` method to clear the command permission cache for a specific command and its parent commands. - This method ensures that the cache reflects the latest permission data after modifications, improving the accuracy of permission checks. - Included logging for cache invalidation actions to enhance monitoring and debugging capabilities.
- Removed unnecessary type casting for case results and DM sent status in the ModerationCoordinator class. - This change enhances code clarity and maintains type safety without the use of `cast`, streamlining the handling of asynchronous operations.
… commands - Updated the TuxPermissionDeniedError class to provide clearer error messages when a command has not been configured with a permission rank. - Added specific messaging for cases where both required and user ranks are zero, improving user feedback on permission issues.
- Updated role removal logic to use `remove_role_assignment`, ensuring the controller cache is invalidated correctly. - Improved command permission assignment by utilizing the permission system for cache invalidation, ensuring fresh data is used for subsequent checks. - Added cache invalidation calls for both command permissions and the permission system to maintain data accuracy after modifications.
…lidation - Modified the role removal process to utilize `remove_role_assignment`, ensuring the controller cache is invalidated correctly. - This change enhances the accuracy of permission data after role modifications, aligning with recent improvements in permission handling.
…n error - Added a call to `instance.close` in the `test_setup_when_connect_raises_sets_cache_service_none` test to verify that the cache service is properly closed when a connection error occurs. - This change enhances the reliability of the cache setup process by ensuring resources are managed correctly during error scenarios.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
3 issues found across 11 files (changes from recent commits).
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them.
<file name="src/tux/core/decorators.py">
<violation number="1" location="src/tux/core/decorators.py:197">
P2: Rule violated: **Code Style and Architecture Guidelines**
Inconsistent error handling pattern. The existing code in this module logs exceptions when `defer()` fails, but this new code silently suppresses all exceptions with `contextlib.suppress(Exception)`. For consistency and debuggability, use the established try/except pattern with logging.</violation>
</file>
<file name="src/tux/core/permission_system.py">
<violation number="1" location="src/tux/core/permission_system.py:498">
P2: Rule violated: **Code Style and Architecture Guidelines**
Code duplication: This cache invalidation logic is duplicated from `set_command_permission`. The `set_command_permission` method should be refactored to call this new method instead of containing duplicate code. This violates DRY principles and creates maintenance burden where changes would need to be made in two places.</violation>
</file>
<file name="src/tux/ui/views/config/dashboard.py">
<violation number="1" location="src/tux/ui/views/config/dashboard.py:1376">
P1: Logic bug: `remove_role_assignment` doesn't filter by `permission_rank_id`, so this removes the role from ALL ranks in the guild instead of just the targeted rank. The `rank_obj.id` that was previously used in the filter is no longer passed to the deletion logic.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/tux/database/controllers/permissions.py`:
- Around line 709-739: The in-memory TTL cache (_command_permissions_cache)
currently stores result directly which makes cached None indistinguishable from
a miss; change the in-memory path to mirror backend behavior by storing a
sentinel-wrapped value (use the same wrap_optional_perm/unwrap_optional_perm
semantics) or use the TTLCache's get_or_fetch / _CACHED_NONE pattern: when
saving results from find_one(PermissionCommand.guild_id == guild_id &
PermissionCommand.command_name == command_name) wrap None via wrap_optional_perm
before set and unwrap via unwrap_optional_perm on get so cached "not found" is
distinguished, and keep TTL usage consistent with PERM_RANKS_TTL and existing
backend keys (PERM_KEY_PREFIX/command_permission:{guild_id}:{command_name}).
🧹 Nitpick comments (6)
src/tux/ui/views/config/dashboard.py (1)
1372-1383: Consider batching role removals to avoid repeated cache invalidations.
remove_role_assignmentlikely invalidates the assignments cache on each call; looping per role can cause extra DB + Valkey churn when multiple roles are removed at once. A batch delete (or a controller flag to defer invalidation until after the loop) would reduce overhead for multi-role removals.src/tux/core/permission_system.py (1)
498-520: Consider extracting duplicated invalidation logic.This method duplicates the cache invalidation logic from
set_command_permission(lines 480-491). While functional, extracting a private helper would reduce duplication.♻️ Optional refactor to reduce duplication
+ async def _invalidate_command_fallback_cache( + self, + guild_id: int, + command_name: str, + ) -> None: + """Invalidate fallback cache for command and parents.""" + await self._cache_backend.delete( + f"{PERM_FALLBACK_KEY_PREFIX}{guild_id}:{command_name}", + ) + parts = command_name.split() + for i in range(len(parts) - 1, 0, -1): + parent_name = " ".join(parts[:i]) + await self._cache_backend.delete( + f"{PERM_FALLBACK_KEY_PREFIX}{guild_id}:{parent_name}", + ) + logger.trace( + f"Invalidated command permission fallback cache for {command_name} (guild {guild_id})", + ) async def set_command_permission(...) -> PermissionCommand: ... - await self._cache_backend.delete(...) - parts = command_name.split() - for i in range(len(parts) - 1, 0, -1): - ... - logger.trace(...) + await self._invalidate_command_fallback_cache(guild_id, command_name) async def invalidate_command_permission_cache(...) -> None: - await self._cache_backend.delete(...) - parts = command_name.split() - ... + await self._invalidate_command_fallback_cache(guild_id, command_name)src/tux/database/controllers/permissions.py (3)
403-420: Control flow issue: in-memory cache checked after backend cache miss.When
self._backend is not Nonebut there's a cache miss (line 406 returns a non-list), the code falls through to line 413-419 which checksif self._backend is None. This is correct.However, if the backend returns something that's not a list (malformed data), the code silently falls through and queries the DB. Consider logging this case for debugging.
♻️ Add logging for malformed backend data
if self._backend is not None: raw = await self._backend.get(backend_key) if raw is not None and isinstance(raw, list): logger.trace( f"Cache hit for permission assignments (guild {guild_id})", ) items = cast(list[dict[str, Any]], raw) return [PermissionAssignment.model_validate(d) for d in items] + elif raw is not None: + logger.warning( + f"Malformed cache entry for permission assignments (guild {guild_id}), fetching from DB", + )
487-499: Duplicatecache_keydefinition.
cache_keyis defined at line 490 and again at line 499. The second definition is identical and unnecessary.♻️ Remove duplicate cache_key definition
sorted_roles = tuple(sorted(user_roles)) backend_key = ( f"{PERM_KEY_PREFIX}user_permission_rank:{guild_id}:{user_id}:{sorted_roles}" ) cache_key = f"user_permission_rank:{guild_id}:{user_id}:{sorted_roles}" if self._backend is not None: raw = await self._backend.get(backend_key) if raw is not None and isinstance(raw, int): logger.trace( f"Cache hit for user permission rank (guild {guild_id}, user {user_id})", ) return raw else: - cache_key = f"user_permission_rank:{guild_id}:{user_id}:{sorted_roles}" cached = self._user_rank_cache.get(cache_key)
663-694: Cache invalidation duplicated across layers.
invalidate_command_permissionin the controller usesPERM_KEY_PREFIXwhileinvalidate_command_permission_cacheinpermission_system.pyusesPERM_FALLBACK_KEY_PREFIX. These are two separate cache namespaces that both need invalidation, which is why callbacks.py calls both.This dual-cache architecture works but adds complexity. Consider documenting this relationship or consolidating in the future.
src/tux/core/decorators.py (1)
186-218: Consider more specific exception handling for defer.
contextlib.suppress(Exception)at line 197-198 silently ignores all exceptions during interaction deferral. While the wrapper version (line 155-157) logs the exception, this version does not.For consistency and debuggability, consider logging or handling specific exceptions.
♻️ Add logging for defer failures
inter = getattr(ctx, "interaction", None) if inter is not None and not inter.response.is_done(): - with contextlib.suppress(Exception): - await inter.response.defer(ephemeral=True) + try: + await inter.response.defer(ephemeral=True) + except Exception as e: + logger.debug(f"Could not defer interaction in check phase: {e}")
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
src/tux/cache/backend.pysrc/tux/core/base_cog.pysrc/tux/core/decorators.pysrc/tux/core/permission_system.pysrc/tux/core/setup/database_setup.pysrc/tux/database/controllers/permissions.pysrc/tux/services/moderation/moderation_coordinator.pysrc/tux/shared/exceptions.pysrc/tux/ui/views/config/callbacks.pysrc/tux/ui/views/config/dashboard.pytests/cache/test_cache_setup.py
🚧 Files skipped from review as they are similar to previous changes (2)
- tests/cache/test_cache_setup.py
- src/tux/cache/backend.py
🧰 Additional context used
📓 Path-based instructions (8)
**/*.py
📄 CodeRabbit inference engine (.cursor/rules/rules.mdc)
**/*.py: Follow Python code style guide and best practices as defined in core/style-guide.mdc
Follow error handling patterns as defined in error-handling/patterns.mdc
Follow loguru logging patterns as defined in error-handling/logging.mdc
Follow Sentry integration patterns as defined in error-handling/sentry.mdc
**/*.py: Use strict type hints with union syntax (Type | NonenotOptional[Type]) in Python
Use NumPy docstring format in Python
Prefer absolute imports; relative imports are allowed within the same module in Python
Group imports in order: stdlib → third-party → local, with blank lines separating groups in Python
Use 88 character line length limit in Python
Use snake_case for functions and variables, PascalCase for classes, UPPER_CASE for constants in Python
Always add imports to the top of the file unless absolutely necessary in Python
Use async/await for all I/O operations in Python
Log errors and events with context information in Python
Keep individual source files under 1600 lines
Include complete type hints for all function parameters and return values
Include NumPy-format docstrings for all public APIs
Files:
src/tux/core/base_cog.pysrc/tux/services/moderation/moderation_coordinator.pysrc/tux/ui/views/config/callbacks.pysrc/tux/shared/exceptions.pysrc/tux/core/decorators.pysrc/tux/core/permission_system.pysrc/tux/ui/views/config/dashboard.pysrc/tux/database/controllers/permissions.pysrc/tux/core/setup/database_setup.py
src/tux/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
src/tux/**/*.py: Use custom exceptions for business logic errors in Python
Handle Discord rate limits gracefully in API calls
Files:
src/tux/core/base_cog.pysrc/tux/services/moderation/moderation_coordinator.pysrc/tux/ui/views/config/callbacks.pysrc/tux/shared/exceptions.pysrc/tux/core/decorators.pysrc/tux/core/permission_system.pysrc/tux/ui/views/config/dashboard.pysrc/tux/database/controllers/permissions.pysrc/tux/core/setup/database_setup.py
**/services/**/*.py
📄 CodeRabbit inference engine (.cursor/rules/rules.mdc)
Follow DatabaseService patterns as defined in database/services.mdc
Files:
src/tux/services/moderation/moderation_coordinator.py
src/tux/services/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
Implement dependency injection pattern for services in Python
Files:
src/tux/services/moderation/moderation_coordinator.py
**/ui/**/*.py
📄 CodeRabbit inference engine (.cursor/rules/rules.mdc)
Follow Discord.py Components V2 rules as defined in ui/cv2.mdc
Files:
src/tux/ui/views/config/callbacks.pysrc/tux/ui/views/config/dashboard.py
**/database/**/*.py
📄 CodeRabbit inference engine (.cursor/rules/rules.mdc)
**/database/**/*.py: Follow SQLModel database model patterns as defined in database/models.mdc
Follow database query patterns and optimization guidelines as defined in database/queries.mdc
Files:
src/tux/database/controllers/permissions.py
src/tux/database/controllers/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
src/tux/database/controllers/**/*.py: Use database controllers with CRUD operations for data access
Use batch operations for permission checks and database queries for optimization
Files:
src/tux/database/controllers/permissions.py
src/tux/database/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
Use transactions for multi-step database operations
Files:
src/tux/database/controllers/permissions.py
🧠 Learnings (8)
📚 Learning: 2026-01-25T18:11:17.210Z
Learnt from: CR
Repo: allthingslinux/tux PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-25T18:11:17.210Z
Learning: Applies to src/tux/modules/**/*.py : Implement role-based permission checks for Discord commands
Applied to files:
src/tux/core/base_cog.pysrc/tux/ui/views/config/callbacks.pysrc/tux/shared/exceptions.pysrc/tux/core/decorators.py
📚 Learning: 2026-01-25T18:11:17.210Z
Learnt from: CR
Repo: allthingslinux/tux PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-25T18:11:17.210Z
Learning: Applies to src/tux/**/*.py : Use custom exceptions for business logic errors in Python
Applied to files:
src/tux/services/moderation/moderation_coordinator.pysrc/tux/shared/exceptions.py
📚 Learning: 2026-01-25T18:11:17.210Z
Learnt from: CR
Repo: allthingslinux/tux PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-25T18:11:17.210Z
Learning: Applies to src/tux/modules/**/*.py : Implement cooldowns and rate limiting in Discord commands
Applied to files:
src/tux/ui/views/config/callbacks.py
📚 Learning: 2026-01-25T18:11:17.210Z
Learnt from: CR
Repo: allthingslinux/tux PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-25T18:11:17.210Z
Learning: Applies to src/tux/shared/cache.py : Use TTL cache system for frequently accessed data (guild config, jail status, permissions)
Applied to files:
src/tux/core/permission_system.py
📚 Learning: 2026-01-25T18:11:17.210Z
Learnt from: CR
Repo: allthingslinux/tux PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-25T18:11:17.210Z
Learning: Applies to src/tux/database/controllers/**/*.py : Use batch operations for permission checks and database queries for optimization
Applied to files:
src/tux/core/permission_system.pysrc/tux/ui/views/config/dashboard.pysrc/tux/database/controllers/permissions.py
📚 Learning: 2026-01-25T18:11:17.210Z
Learnt from: CR
Repo: allthingslinux/tux PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-25T18:11:17.210Z
Learning: Applies to src/tux/core/bot.py : Pre-warm cache on bot startup for frequently accessed data
Applied to files:
src/tux/core/permission_system.py
📚 Learning: 2026-01-25T18:11:17.210Z
Learnt from: CR
Repo: allthingslinux/tux PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-25T18:11:17.210Z
Learning: Applies to src/tux/database/controllers/**/*.py : Use database controllers with CRUD operations for data access
Applied to files:
src/tux/database/controllers/permissions.py
📚 Learning: 2026-01-25T18:11:17.210Z
Learnt from: CR
Repo: allthingslinux/tux PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-25T18:11:17.210Z
Learning: Applies to src/tux/database/migrations/versions/**/*.py : Use Alembic for database migrations in Python
Applied to files:
src/tux/core/setup/database_setup.py
🧬 Code graph analysis (5)
src/tux/core/base_cog.py (1)
src/tux/core/decorators.py (1)
command_permission_check(186-218)
src/tux/ui/views/config/callbacks.py (2)
src/tux/core/permission_system.py (4)
get_permission_system(971-992)remove_role_assignment(339-368)invalidate_command_permission_cache(498-520)set_command_permission(426-496)src/tux/database/controllers/permissions.py (3)
remove_role_assignment(433-458)invalidate_command_permission(663-694)set_command_permission(616-661)
src/tux/core/permission_system.py (3)
src/tux/cache/backend.py (1)
get_cache_backend(166-194)src/tux/database/controllers/__init__.py (2)
DatabaseCoordinator(55-208)command_permissions(201-208)src/tux/database/controllers/permissions.py (2)
unwrap_optional_perm(582-590)wrap_optional_perm(577-579)
src/tux/ui/views/config/dashboard.py (3)
src/tux/database/controllers/__init__.py (2)
permission_assignments(191-198)guild(118-122)src/tux/database/controllers/permissions.py (1)
remove_role_assignment(433-458)src/tux/core/permission_system.py (1)
remove_role_assignment(339-368)
src/tux/database/controllers/permissions.py (2)
src/tux/cache/ttl.py (1)
TTLCache(17-196)src/tux/database/models/models.py (3)
PermissionAssignment(414-483)PermissionCommand(486-561)PermissionRank(331-411)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
- GitHub Check: Seer Code Review
- GitHub Check: Run All Tests (3.13.11)
- GitHub Check: Validate
- GitHub Check: cubic · AI code reviewer
🔇 Additional comments (24)
src/tux/core/setup/database_setup.py (2)
90-94: Good simplification of revision retrieval.Using
scalar()and explicitly returningNonewhen absent is clean and clear.
119-122: Clearer revision logging.The human-friendly
rev_displayplus placeholder logging reads well.src/tux/shared/exceptions.py (1)
178-186: No action required. The permission system correctly uses the (0, 0) values as a sentinel exclusively for unconfigured commands. Whencmd_perm is None(unconfigured), the error is raised with (0, 0). When a command is configured—even withrequired_rank=0—cmd_permis not None, and the actual rank values are used in the permission check. These code paths (lines 304-316 vs. 327-339 in decorators.py) are mutually exclusive, eliminating any collision between the unconfigured sentinel and valid rank 0 permissions. Rank 0 is correctly defined as a valid permission level (e.g., "Member") and the system handles it appropriately.src/tux/services/moderation/moderation_coordinator.py (2)
312-327: Post-action DM after successful actions looks good.Clear gating to non-removal actions and safe error handling without disrupting the flow.
329-386: Parallel case creation + DM for no-action path looks good.The concurrent execution with BaseException handling keeps the response path non-blocking while still capturing failures.
src/tux/core/permission_system.py (6)
22-27: LGTM! Clean imports and constants for backend-backed caching.The new imports and constants establish a clear pattern for permission fallback caching with a 10-minute TTL.
Also applies to: 37-38
134-134: LGTM! Backend initialization follows the established pattern.The cache backend is correctly obtained from the bot instance, supporting both Valkey and in-memory fallback.
480-491: LGTM! Proper hierarchical cache invalidation.The invalidation correctly clears the command and all parent commands (e.g., "config ranks init" → "config ranks" → "config"), ensuring the fallback mechanism sees fresh data.
650-661: Good concurrent cache operations for network backends.The use of
asyncio.gatherfor both reads and writes is appropriate for Valkey/network caches where sequential operations would add significant latency.
717-726: LGTM! Concurrent cache writes for uncached commands.The parallel cache population after DB lookup is efficient and correctly uses the same key pattern.
548-555: No change needed - the cache logic correctly distinguishes miss from hit.The wrap/unwrap protocol intentionally distinguishes cache miss from cache hit at the
raw is not Nonecheck. A cache miss returnsNonefrom the backend, while a cache hit returns{"_v": value}. Whenunwrap_optional_permis called on a hit, it correctly extracts the wrapped value, which may itself beNone(for{"_v": None}). Malformed entries are gracefully handled by returningNone, treating them as unconfigured permissions—which is intentional and safe. The logic is correct.src/tux/ui/views/config/callbacks.py (4)
17-18: LGTM! Clean import updates for permission system integration.The imports correctly bring in
get_permission_systemandRESTRICTED_COMMANDSfor the updated callback flows.
231-238: LGTM! Improved role removal using controller method.Using
remove_role_assignmentinstead ofdelete_whereensures the controller's cache invalidation logic is executed automatically.
328-332: LGTM! Command assignment routed through permission system.Using
get_permission_system().set_command_permissionensures consistent cache invalidation through the centralized permission system, rather than requiring separate invalidation calls.
301-311: The dual cache invalidation is correct and necessary for this two-layer caching architecture. The controller cache (usingPERM_KEY_PREFIX) stores raw database query results, while the permission system cache (usingPERM_FALLBACK_KEY_PREFIX) stores computed fallback results. Both layers must be invalidated together to maintain consistency after a database change, so no consolidation is possible without losing cache performance in the permission system layer.src/tux/database/controllers/permissions.py (4)
10-10: LGTM! Clean imports and constants for backend-backed caching.The new constants establish consistent TTLs and key prefixes across the permission controllers.
Also applies to: 16-16, 25-31
41-57: LGTM! Constructor properly supports optional cache backend.The dual-path initialization enables graceful fallback to in-memory caching when Valkey is unavailable.
130-157: LGTM! Cache read/write with proper serialization.The code correctly serializes models to dicts for backend storage and deserializes with
model_validate, handling both Valkey and in-memory paths.
577-590: LGTM! Sentinel pattern for cached None vs cache miss.The wrap/unwrap helpers correctly use
{"_v": value}to distinguish between a cached None value and a cache miss. The unwrap returnsNonefor both cached-None and missing, which is the intended semantic (both mean "no permission configured").src/tux/core/base_cog.py (2)
20-20: LGTM! Permission check setup integrated into BaseCog initialization.The new
_setup_permission_checkscall ensures all dynamic-permission commands get pre-argument-conversion permission checks.Also applies to: 67-68
70-88: Subcommands using@requires_command_permission()are not processed for check-phase permission insertion.
self.get_commands()only returns top-level commands within the cog. Subcommands (e.g.,levels set,cases view,config ranks init) that use@requires_command_permission()won't have the check-phase permission check inserted, causing users to see "missing required argument" instead of "permission denied" when they lack permission.The method should recursively iterate through command groups to access their subcommands, similar to how
permission_system.pyhandles command collection at lines 800–802.src/tux/core/decorators.py (3)
14-14: LGTM! Clean imports and exports for new permission check function.The
contextlibimport and updated__all__properly expose the newcommand_permission_checkfunction.Also applies to: 30-30
103-109: LGTM! Short-circuit prevents double permission checking.When permission was already checked in the command check phase, the wrapper correctly bypasses its own check and calls the original function directly.
176-179: LGTM! Metadata attributes enable check-phase introspection.Storing decorator options as wrapper attributes allows
BaseCog._setup_permission_checksandcommand_permission_checkto read configuration without re-parsing.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
…eferral - Updated the `command_permission_check` function to replace the use of `contextlib.suppress` with a try-except block for deferring interactions. - This change improves error handling by logging exceptions that occur during the deferral process, enhancing debugging capabilities.
…separate method - Introduced the `_invalidate_command_fallback_cache` method to encapsulate the logic for invalidating the command permission fallback cache for a command and its parent commands. - This refactor improves code readability and maintainability by reducing duplication in the `set_command_permission` and `remove_command_permission` methods, while ensuring accurate cache management after permission modifications.
…alization - Updated the CaseController instantiation in the DatabaseCoordinator to include an optional cache backend parameter. - This change allows for improved cache management and flexibility in handling database interactions.
…oller - Enhanced the CaseController to support caching for case lookups by number, reducing database hits and improving performance. - Introduced methods for wrapping and unwrapping case data for cache storage, ensuring proper serialization and deserialization. - Updated the get_case_by_id and update_case_by_number methods to utilize the cache, invalidating it as necessary to maintain data accuracy.
…he invalidation - Introduced `remove_role_assignments_from_rank` method to delete permission level assignments for multiple roles from a specific rank, improving role management. - Enhanced cache invalidation logic to ensure the permission assignments cache is cleared after deletions, maintaining data accuracy. - Added logging for malformed cache entries to aid in debugging and monitoring.
…validation - Updated the role removal process to use `remove_role_assignments_from_rank`, allowing for batch removal of roles from a specific rank and improving cache invalidation efficiency. - This change enhances the accuracy of permission data after role modifications, aligning with recent improvements in permission handling.
- Updated the `is_snippetbanned` method to utilize the latest case for a user, ensuring that unbans are respected without altering the original ban case. - This change enhances the accuracy of snippet ban checks by directly evaluating the most recent case type, streamlining the logic for determining user restrictions.
| async def is_snippetbanned(self, guild_id: int, user_id: int) -> bool: | ||
| """Check if a user is currently snippet banned in a guild. | ||
|
|
||
| Uses the latest case for the user: if the most recent case is | ||
| SNIPPETBAN, the user is banned; if it is SNIPPETUNBAN or anything | ||
| else, they are not. This ensures unbans are respected without | ||
| mutating or closing the original ban case. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| guild_id : int |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: The is_snippetbanned() and is_pollbanned() methods incorrectly check ban status by using get_latest_case_by_user(), which can return unrelated moderation cases, leading to bypasses.
Severity: HIGH
Suggested Fix
Create and use new database controller methods, such as get_latest_snippetban_or_snippetunban_case() and get_latest_pollban_or_pollunban_case(). These methods should filter cases to only include the relevant ban/unban types, mirroring the existing logic in get_latest_jail_or_unjail_case().
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location: src/tux/modules/snippets/__init__.py#L35-L45
Potential issue: The `is_snippetbanned()` and `is_pollbanned()` methods use
`get_latest_case_by_user()` to determine a user's ban status. This function returns the
most recent moderation case of any type. If a user is snippet-banned and then receives
another, unrelated moderation action (e.g., a `WARN`), the check will evaluate the
unrelated case. This causes the function to incorrectly report the user as not banned,
allowing them to bypass the restriction. This logic contradicts the pattern used for
jail checks, which correctly filters for only `JAIL` or `UNJAIL` case types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1 issue found across 1 file (changes from recent commits).
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them.
<file name="src/tux/modules/snippets/__init__.py">
<violation number="1" location="src/tux/modules/snippets/__init__.py:55">
P1: Logic bug: `get_latest_case_by_user` returns the latest case of ANY type. If a snippet-banned user receives any other case (warning, kick, etc.), this check will incorrectly return `False`. The method should filter to only `SNIPPETBAN`/`SNIPPETUNBAN` case types to determine snippet ban status, similar to how `get_latest_jail_or_unjail_case` filters jail-related cases.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| """ | ||
| return await self.db.case.is_user_under_restriction( | ||
| guild_id=guild_id, | ||
| latest_case = await self.db.case.get_latest_case_by_user( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P1: Logic bug: get_latest_case_by_user returns the latest case of ANY type. If a snippet-banned user receives any other case (warning, kick, etc.), this check will incorrectly return False. The method should filter to only SNIPPETBAN/SNIPPETUNBAN case types to determine snippet ban status, similar to how get_latest_jail_or_unjail_case filters jail-related cases.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/tux/modules/snippets/__init__.py, line 55:
<comment>Logic bug: `get_latest_case_by_user` returns the latest case of ANY type. If a snippet-banned user receives any other case (warning, kick, etc.), this check will incorrectly return `False`. The method should filter to only `SNIPPETBAN`/`SNIPPETUNBAN` case types to determine snippet ban status, similar to how `get_latest_jail_or_unjail_case` filters jail-related cases.</comment>
<file context>
@@ -47,12 +52,11 @@ async def is_snippetbanned(self, guild_id: int, user_id: int) -> bool:
"""
- return await self.db.case.is_user_under_restriction(
- guild_id=guild_id,
+ latest_case = await self.db.case.get_latest_case_by_user(
user_id=user_id,
- active_restriction_type=DBCaseType.JAIL,
</file context>
Pull Request
Description
Provide a clear summary of your changes and reference any related issues. Include the motivation behind these changes and list any new dependencies if applicable.
If your PR is related to an issue, please include the issue number below:
Related Issue: Closes #1164
Type of Change:
Guidelines
My code follows the style guidelines of this project (formatted with Ruff)
I have performed a self-review of my own code
I have commented my code, particularly in hard-to-understand areas
I have made corresponding changes to the documentation if needed
My changes generate no new warnings
I have tested this change
Any dependent changes have been merged and published in downstream modules
I have added all appropriate labels to this PR
I have followed all of these guidelines.
How Has This Been Tested? (if applicable)
Please describe how you tested your code. e.g describe what commands you ran, what arguments, and any config stuff (if applicable)
Screenshots (if applicable)
Please add screenshots to help explain your changes.
Additional Information
Please add any other information that is important to this PR.
Summary by Sourcery
Introduce a unified cache layer with optional Valkey (Redis-compatible) backend and wire it into permissions, guild config, prefixes, and health checks for shared, multi-guild-safe caching.
New Features:
Enhancements:
Documentation:
Tests: