Skip to content

Conversation

@kzndotsh
Copy link
Contributor

@kzndotsh kzndotsh commented Jan 28, 2026

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:

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Performance improvement
  • Code refactoring
  • Test improvements

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:

  • Add a cache package providing TTL-based in-memory caching, Valkey-backed async cache backends, and a CacheService for Valkey connection management.
  • Integrate an optional Valkey-backed cache into permission controllers, the permission system, guild config, jail status, and prefix management to share cached data across restarts and processes.
  • Extend configuration, orchestration, and Docker Compose to support Valkey deployment and environment configuration, plus cache-aware health checks.

Enhancements:

  • Refactor existing caching code to use new async cache managers and backends, including adapting controllers and services to invalidate and fetch from the shared cache layer.
  • Update documentation and developer guides to describe the new cache architecture, Valkey configuration, multi-guild keying, and best practices for async cache usage.
  • Adjust test suites and performance benchmarks to exercise the new cache interfaces and async behavior.

Documentation:

  • Expand caching best practices and environment reference docs to cover the new cache package, Valkey configuration, and multi-guild-safe key design.
  • Update project documentation (AGENTS and config generator docs) to mention Valkey as an optional cache dependency and how to run it via Docker.

Tests:

  • Add unit tests for cache backends, cache service, cache managers with backends, and cache-related health checks, and update existing tests to work with async cache APIs and Valkey integration.

- 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.
@kzndotsh kzndotsh linked an issue Jan 28, 2026 that may be closed by this pull request
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Jan 28, 2026

Reviewer's Guide

Introduces 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 backend

sequenceDiagram
    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
Loading

Sequence diagram for JailStatusCache.get_or_fetch with shared backend

sequenceDiagram
    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
Loading

Class diagram for cache layer and permission-related integration

classDiagram
    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
Loading

File-Level Changes

Change Details Files
Introduce unified cache package with TTLCache, async backends (in-memory and Valkey), cache service, and shared managers for guild config and jail status, then wire it into bot setup and permission system.
  • Add src/tux/cache package with ttl-based in-memory cache, AsyncCacheBackend protocol, InMemoryBackend, ValkeyBackend with JSON serialization and key prefixing, CacheService for Valkey connection lifecycle, and managers for guild config and jail status including async APIs and stampede protection.
  • Expose cache primitives and managers via tux.cache init, including AsyncCacheBackendProtocol alias, and update imports across the codebase to use tux.cache instead of tux.shared.cache.
  • Add CacheSetupService to bot setup orchestrator to initialize Valkey when configured, wire the chosen backend (Valkey or shared InMemoryBackend) into GuildConfigCacheManager and JailStatusCache, and store CacheService on the bot for later use and graceful shutdown.
  • Update DatabaseCoordinator and permission controllers (PermissionRankController, PermissionAssignmentController, PermissionCommandController) to accept an optional AsyncCacheBackend and use it for get/set/delete of permission ranks, assignments, user ranks, and command permissions, while preserving existing TTLCache behavior as a fallback.
  • Extend PermissionSystem to use the cache backend for its command-permission fallback cache, sharing the same wrapping/unwrapping helpers used by PermissionCommandController.
src/tux/cache/__init__.py
src/tux/cache/ttl.py
src/tux/cache/backend.py
src/tux/cache/service.py
src/tux/cache/managers.py
src/tux/core/setup/cache_setup.py
src/tux/core/setup/orchestrator.py
src/tux/core/bot.py
src/tux/database/controllers/__init__.py
src/tux/database/controllers/permissions.py
src/tux/core/permission_system.py
Integrate caching backend into higher-level features (prefix manager, guild config controller, jail system, moderation communication service) and update their APIs to be async where needed.
  • Modify PrefixManager to read/write prefixes through the cache backend (Valkey or in-memory) using prefix:{guild_id} keys, update load_all_prefixes to backfill the backend, and make invalidate_cache async for backend invalidation.
  • Switch GuildConfigCacheManager usages in guild_config controller and communication_service to the new async API (await get/set/invalidate), ensuring cache invalidation happens after config changes and log-channel lookups.
  • Update JailStatusCache usages in jail system, moderation module, and tests to use the async API (await set/invalidate/clear_all), and add cache invalidation in tests before assertions that rely on fresh DB state.
  • Adjust communication_service.invalidate_guild_config_cache to be async and delegate to GuildConfigCacheManager.invalidate.
  • Make minor type-safety tweaks (e.g., del_afk runtime isinstance guard, chunks_list typing in info views) to satisfy new tooling and keep tests passing.
src/tux/core/prefix_manager.py
src/tux/database/controllers/guild_config.py
src/tux/services/moderation/communication_service.py
src/tux/modules/moderation/jail.py
src/tux/modules/moderation/__init__.py
tests/modules/test_jail_system.py
src/tux/modules/utility/__init__.py
src/tux/modules/info/views.py
Extend configuration, Docker compose, health checks, and docs to support optional Valkey deployment and document new caching behavior.
  • Add Valkey configuration fields (VALKEY_HOST/PORT/DB/PASSWORD/URL) and computed valkey_url to Config, ensure env-schema generator treats VALKEY_* as .env keys, and update env reference docs accordingly.
  • Add tux-valkey service and volume to compose.yaml for local Valkey deployment, and mention Valkey as an optional cache dependency in AGENTS.md and performance guidelines.
  • Extend scripts/db/health to run an optional Valkey health check (_check_cache) alongside DB health, returning skipped/healthy/unhealthy and failing the command when cache is unhealthy but configured.
  • Rewrite/expand caching best-practices docs to reflect the new cache package, backends, managers, multi-guild keying, async APIs, and Valkey-specific behavior, including updated examples for invalidation and tests.
  • Declare valkey Python dependency in pyproject.toml and remove old tux.shared.cache export/implementation in favor of the new cache package, keeping all empty in tux.shared.init.
  • Update performance tests for GuildConfigCacheManager and JailStatusCache to use the async clear_all/set/get APIs within asyncio.run wrappers.
src/tux/shared/config/settings.py
scripts/config/generate.py
docs/content/reference/env.md
docs/content/developer/best-practices/caching.md
compose.yaml
AGENTS.md
scripts/db/health.py
pyproject.toml
src/tux/shared/__init__.py
src/tux/shared/cache.py
tests/performance/test_cache_performance.py
Add targeted unit tests for the new cache layer (backends, service, shared managers, and health integration) to validate behavior and edge cases.
  • Add tests for InMemoryBackend and ValkeyBackend (key prefixing, JSON serialization, TTL behavior, delete/exists) plus get_cache_backend selection and fallback reuse.
  • Add tests for CacheService connect/ping/close behavior using mocks, including handling of missing URL, connection errors, and client ping failures.
  • Add tests for GuildConfigCacheManager and JailStatusCache when a backend is set, verifying get/set/invalidate and get_or_fetch semantics.
  • Add tests for the new Valkey health check helper _check_cache to ensure correct status/messaging for skipped, healthy, and unhealthy scenarios, including connection and ping failures.
tests/cache/__init__.py
tests/cache/test_backend.py
tests/cache/test_service.py
tests/cache/test_shared_cache_backend.py
tests/cache/test_db_health_cache.py

Assessment against linked issues

Issue Objective Addressed Explanation
#1164 Introduce a Valkey-based cache layer (backends, service, configuration) and integrate it with bot startup and health checks, while retaining an in-memory fallback.
#1164 Refactor existing caching consumers (permission controllers/system, guild config, jail status, prefixes, moderation communication) to use the new async cache backend/manager APIs instead of tux.shared.cache, preserving correct invalidation and TTL behavior.
#1164 Update infrastructure, configuration, documentation, and tests to support and describe the new Valkey caching behavior (docker-compose service, env/config docs, caching docs, AGENTS overview, and cache-related tests).

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actions

This comment was marked as off-topic.

@coderabbitai
Copy link

coderabbitai bot commented Jan 28, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
Cache package
src/tux/cache/__init__.py, src/tux/cache/backend.py, src/tux/cache/service.py, src/tux/cache/ttl.py, src/tux/cache/managers.py
Adds async cache layer: AsyncCacheBackend protocol, InMemory and Valkey backends, TTLCache, CacheService (connect/ping/close with retries), and async cache managers (GuildConfigCacheManager, JailStatusCache) with per-key locking and backend integration.
Startup & wiring
src/tux/core/setup/cache_setup.py, src/tux/core/setup/orchestrator.py, src/tux/core/bot.py
Introduces CacheSetupService into orchestrator; conditionally connects CacheService, assigns bot.cache_service, wires backend into singleton managers, and ensures cache closure on shutdown.
Controllers & DB wiring
src/tux/database/controllers/__init__.py, src/tux/database/controllers/*
DatabaseCoordinator accepts optional cache_backend and propagates it; multiple controllers (permissions, guild_config, case, etc.) become backend-aware and perform async cache operations with standardized key/TTL conventions.
Core systems
src/tux/core/permission_system.py, src/tux/core/prefix_manager.py, src/tux/core/setup/permission_setup.py
PermissionSystem and PrefixManager moved to backend-backed async caching via get_cache_backend(bot); some invalidation APIs converted to async; setup wires cache backend into DB coordinator.
Consumers & services
src/tux/modules/..., src/tux/services/moderation/..., src/tux/ui/views/...
Callsites updated to import from tux.cache, await async cache methods, adapt method signatures where needed, and perform backend-aware invalidation; moderation flow refactored for parallelism and DM handling.
Legacy removal
src/tux/shared/cache.py, src/tux/shared/__init__.py
Deletes legacy shared TTL cache and its exports; consumers migrated to new async cache package and shared module exports adjusted.
Config, env & compose
.env.example, scripts/config/generate.py, src/tux/shared/config/settings.py, config/config.json.example, docs/content/reference/env.md, compose.yaml
Adds VALKEY_* env vars and Config fields (including computed valkey_url), updates generator and examples to include Valkey configuration; adds tux-valkey compose service and persistent volume.
Health & scripts
scripts/db/health.py
Adds optional cache health check when VALKEY_URL configured; integrates cache check into health command and handles connect/ping/close flows.
Documentation
docs/content/developer/best-practices/caching.md, AGENTS.md, docs/content/selfhost/*, docs/content/selfhost/install/docker.md
Extensive docs updates describing cache architecture, Valkey usage, Docker profile guidance, and operational notes.
Tests
tests/cache/*, tests/core/*, tests/database/*, tests/shared/*
Adds comprehensive tests for backends, CacheService, TTLCache, cache managers, CacheSetupService, health-check; updates tests to async cache API and new import paths.
Miscellaneous
pyproject.toml, small modulessrc/tux/modules/info/views.py, src/tux/modules/utility/__init__.py
Adds valkey>=6.1.1 dependency; minor typing/annotation tweaks and small refactors.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • feat: valkey #1179: Implements Valkey-backed cache layer and mirrors almost the same cache package, service, controller wiring, docs, and tests — strong overlap.
  • Misc refactorings and optimizations #1160: Touches caching code and alternative cache implementations; may conflict where legacy cache was removed and replaced.
  • refactor: scripts #1108: Edits scripts/config/generate.py and scripts/db/health.py; overlaps on environment generation and health-check behavior.

Suggested labels

priority: high

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: valkey' is concise and accurately reflects the main feature added—a Valkey cache backend integration across the project.
Description check ✅ Passed The description is comprehensive and clearly relates to the changeset, outlining the cache layer implementation, Valkey integration, and refactoring of existing caching code.
Linked Issues check ✅ Passed The PR closes issue #1164 and implements the primary objective of introducing a unified cache layer with optional Valkey backend integration across permissions, configs, and other systems.
Out of Scope Changes check ✅ Passed Changes are scoped to cache implementation, integration, configuration, and related documentation. Minor unrelated changes appear (e.g., utility adjustments, test restructuring) but are minimal and reasonable.
Docstring Coverage ✅ Passed Docstring coverage is 93.89% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 1164-valkey

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f6b30ae and 6f49386.

📒 Files selected for processing (7)
  • src/tux/core/decorators.py
  • src/tux/core/permission_system.py
  • src/tux/database/controllers/__init__.py
  • src/tux/database/controllers/case.py
  • src/tux/database/controllers/permissions.py
  • src/tux/modules/snippets/__init__.py
  • src/tux/ui/views/config/dashboard.py

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist

This comment was marked as resolved.

@github-actions
Copy link
Contributor

github-actions bot commented Jan 28, 2026

📚 Documentation Preview

Type URL Version Message
Production https://tux.atl.dev - -
Preview https://8d1c9f0c-tux-docs.allthingslinux.workers.dev 8d1c9f0c-5917-42cf-98d7-1c81ea944066 Preview: tux@bcbeaa08948dfa54b497cedf0610586dc1739fd4 on 1179/merge by kzndotsh (run 381)

Copy link

@amazon-q-developer amazon-q-developer bot left a 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

  1. Fix the security vulnerability by avoiding password inclusion in URLs or implementing secure URL handling
  2. Implement proper TTL handling in InMemoryBackend to maintain consistency with ValkeyBackend
  3. Add Valkey password validation similar to existing PostgreSQL validation
  4. 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.


⚠️ This PR contains more than 30 files. Amazon Q is better at reviewing smaller PRs, and may miss issues in larger changesets.

@sentry
Copy link

sentry bot commented Jan 28, 2026

❌ 5 Tests Failed:

Tests completed Failed Passed Skipped
664 5 659 36
View the top 2 failed test(s) by shortest run time
tests/cache/test_backend.py::TestValkeyBackend::test_setex_called_when_ttl_sec_provided
Stack Traces | 0.005s run time
tests/cache/test_backend.py:162: in test_setex_called_when_ttl_sec_provided
    assert args[2] == "v"
E   assert '"v"' == 'v'
E     
E     #x1B[0m#x1B[91m- v#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
E     #x1B[92m+ "v"#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
tests/cache/test_backend.py::TestValkeyBackend::test_string_value_stored_as_is
Stack Traces | 0.007s run time
tests/cache/test_backend.py:128: in test_string_value_stored_as_is
    assert mock_client.set.call_args[0][1] == "plain"
E   assert '"plain"' == 'plain'
E     
E     #x1B[0m#x1B[91m- plain#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
E     #x1B[92m+ "plain"#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
E     ? +     +#x1B[90m#x1B[39;49;00m
View the full list of 3 ❄️ flaky test(s)
tests/modules/test_moderation_service_integration.py::TestModerationCoordinatorIntegration::test_complete_workflow_with_mod_logging_success

Flake rate in main: 68.18% (Passed 7 times, Failed 15 times)

Stack Traces | 0.184s run time
tests/modules/test_moderation_service_integration.py:569: in test_complete_workflow_with_mod_logging_success
    assert case.mod_log_message_id == mod_message.id
E   AssertionError: assert None == 999888777
E    +  where None = <Case id=1 guild=123456789 num=1 type=CaseType.BAN user=555666777>.mod_log_message_id
E    +  and   999888777 = <MagicMock id='139815596216000'>.id
tests/modules/test_moderation_service_integration.py::TestModerationCoordinatorIntegration::test_mod_log_case_update_failure

Flake rate in main: 68.18% (Passed 7 times, Failed 15 times)

Stack Traces | 0.167s run time
.../hostedtoolcache/Python/3.13.11.../x64/lib/python3.13/unittest/mock.py:958: in assert_called_once
    raise AssertionError(msg)
E   AssertionError: Expected 'send' to have been called once. Called 0 times.

During handling of the above exception, another exception occurred:
tests/modules/test_moderation_service_integration.py:852: in test_mod_log_case_update_failure
    mod_channel.send.assert_called_once()
E   AssertionError: Expected 'send' to have been called once. Called 0 times.
tests/modules/test_moderation_service_integration.py::TestModerationCoordinatorIntegration::test_mod_log_send_failure_permissions

Flake rate in main: 68.18% (Passed 7 times, Failed 15 times)

Stack Traces | 0.168s run time
.../hostedtoolcache/Python/3.13.11.../x64/lib/python3.13/unittest/mock.py:958: in assert_called_once
    raise AssertionError(msg)
E   AssertionError: Expected 'send' to have been called once. Called 0 times.

During handling of the above exception, another exception occurred:
tests/modules/test_moderation_service_integration.py:782: in test_mod_log_send_failure_permissions
    mod_channel.send.assert_called_once()
E   AssertionError: Expected 'send' to have been called once. Called 0 times.

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)

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a 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 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.
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>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@kzndotsh kzndotsh self-assigned this Jan 28, 2026
Copy link

@gemini-code-assist gemini-code-assist bot left a 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.

Comment on lines 79 to 86
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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Copy link

@cubic-dev-ai cubic-dev-ai bot left a 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_guild in src/tux/cache/managers.py clears the entire cache instead of only the target guild, which is a concrete data-loss/behavior regression risk.
  • src/tux/cache/backend.py ignores ttl_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.
Copy link

@coderabbitai coderabbitai bot left a 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.
If backend.set(...) raises, the outer except returns 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

📥 Commits

Reviewing files that changed from the base of the PR and between 9c74b36 and 9041ce5.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • compose.yaml
  • docs/content/developer/best-practices/caching.md
  • src/tux/cache/managers.py
  • src/tux/core/prefix_manager.py
  • src/tux/modules/moderation/jail.py
  • src/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.md
  • CHANGELOG.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 | None not Optional[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.py
  • src/tux/core/prefix_manager.py
  • src/tux/services/moderation/moderation_coordinator.py
  • src/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.py
  • src/tux/core/prefix_manager.py
  • src/tux/services/moderation/moderation_coordinator.py
  • src/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.md
  • src/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.py
  • 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 (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 in get_or_fetch is 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.
Copy link

@cubic-dev-ai cubic-dev-ai bot left a 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):
Copy link

@cubic-dev-ai cubic-dev-ai bot Jan 29, 2026

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>
Fix with Cubic

case = None
else:
case = cast(Case, case_result)
if isinstance(dm_result, BaseException):
Copy link

@cubic-dev-ai cubic-dev-ai bot Jan 29, 2026

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>
Fix with Cubic

Copy link

@coderabbitai coderabbitai bot left a 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 prevents self._prefix_cache.clear() or self._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, wrap backend.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 same BaseException fix here for consistency.

This unchanged code block uses isinstance(..., Exception) for handling asyncio.gather results, which has the same issue that was just fixed in lines 355 and 363. If response_task or mod_log_task gets cancelled, the asyncio.CancelledError would not be caught by the Exception check.

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 single backend.set exception will abort the batch and skip remaining writes. Consider return_exceptions=True with per-guild warnings so in-memory loading remains clean.

🔧 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__,
+                            )
As per coding guidelines: Follow error handling patterns as defined in error-handling/patterns.mdc; Log errors and events with context information in Python.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9041ce5 and 5de958b.

📒 Files selected for processing (2)
  • src/tux/core/prefix_manager.py
  • src/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 | None not Optional[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.py
  • src/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.py
  • src/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 cast import is correctly added and used for type narrowing after the asyncio.gather results.


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_dm method already handles asyncio.CancelledError internally, so the outer Exception catch is appropriate here.


350-370: LGTM - properly handles asyncio.CancelledError now.

The change from isinstance(..., Exception) to isinstance(..., BaseException) correctly addresses the previous review concern. Since asyncio.CancelledError inherits from BaseException (not Exception), this ensures cancelled tasks returned by asyncio.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.

Comment on lines +96 to +106
# 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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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
As per coding guidelines: Follow error handling patterns as defined in error-handling/patterns.mdc; Log errors and events with context information in Python.
🤖 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.

Comment on lines +132 to +135
# 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)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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__,
+            )
As per coding guidelines: Follow error handling patterns as defined in error-handling/patterns.mdc; Log errors and events with context information in Python.
🤖 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.
Copy link

@cubic-dev-ai cubic-dev-ai bot left a 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.

Copy link

@coderabbitai coderabbitai bot left a 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_assignment likely 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 None but there's a cache miss (line 406 returns a non-list), the code falls through to line 413-419 which checks if 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: Duplicate cache_key definition.

cache_key is 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_permission in the controller uses PERM_KEY_PREFIX while invalidate_command_permission_cache in permission_system.py uses PERM_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

📥 Commits

Reviewing files that changed from the base of the PR and between 5de958b and f6b30ae.

📒 Files selected for processing (11)
  • src/tux/cache/backend.py
  • src/tux/core/base_cog.py
  • src/tux/core/decorators.py
  • src/tux/core/permission_system.py
  • src/tux/core/setup/database_setup.py
  • src/tux/database/controllers/permissions.py
  • src/tux/services/moderation/moderation_coordinator.py
  • src/tux/shared/exceptions.py
  • src/tux/ui/views/config/callbacks.py
  • src/tux/ui/views/config/dashboard.py
  • tests/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 | None not Optional[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.py
  • src/tux/services/moderation/moderation_coordinator.py
  • src/tux/ui/views/config/callbacks.py
  • src/tux/shared/exceptions.py
  • src/tux/core/decorators.py
  • src/tux/core/permission_system.py
  • src/tux/ui/views/config/dashboard.py
  • src/tux/database/controllers/permissions.py
  • src/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.py
  • src/tux/services/moderation/moderation_coordinator.py
  • src/tux/ui/views/config/callbacks.py
  • src/tux/shared/exceptions.py
  • src/tux/core/decorators.py
  • src/tux/core/permission_system.py
  • src/tux/ui/views/config/dashboard.py
  • src/tux/database/controllers/permissions.py
  • src/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.py
  • src/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.py
  • src/tux/ui/views/config/callbacks.py
  • src/tux/shared/exceptions.py
  • src/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.py
  • src/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.py
  • src/tux/ui/views/config/dashboard.py
  • 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/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 returning None when absent is clean and clear.


119-122: Clearer revision logging.

The human-friendly rev_display plus 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. When cmd_perm is None (unconfigured), the error is raised with (0, 0). When a command is configured—even with required_rank=0cmd_perm is 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.gather for 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 None check. A cache miss returns None from the backend, while a cache hit returns {"_v": value}. When unwrap_optional_perm is called on a hit, it correctly extracts the wrapped value, which may itself be None (for {"_v": None}). Malformed entries are gracefully handled by returning None, 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_system and RESTRICTED_COMMANDS for the updated callback flows.


231-238: LGTM! Improved role removal using controller method.

Using remove_role_assignment instead of delete_where ensures the controller's cache invalidation logic is executed automatically.


328-332: LGTM! Command assignment routed through permission system.

Using get_permission_system().set_command_permission ensures 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 (using PERM_KEY_PREFIX) stores raw database query results, while the permission system cache (using PERM_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 returns None for 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_checks call 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.py handles 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 contextlib import and updated __all__ properly expose the new command_permission_check function.

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_checks and command_permission_check to 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.
@kzndotsh kzndotsh merged commit 720061f into main Jan 29, 2026
23 of 24 checks passed
@kzndotsh kzndotsh deleted the 1164-valkey branch January 29, 2026 20:04
Comment on lines 35 to 45
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
Copy link

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.

Copy link

@cubic-dev-ai cubic-dev-ai bot left a 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(
Copy link

@cubic-dev-ai cubic-dev-ai bot Jan 29, 2026

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>
Fix with Cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Valkey

2 participants