Files
ironclaw/src/testing
Henry Park 2dc78b2d94 feat(db): add per-user CachedSettingsStore decorator (#2425)
* feat(db): add per-user CachedSettingsStore decorator

SettingsStore methods hit the database on every call. The v2 engine
path (effect_adapter) and the dispatcher's per-turn tool permission
loading both called get_all_settings() without caching, adding
unnecessary DB round-trips on every agentic loop iteration.

Add a write-through CachedSettingsStore decorator that caches
get_all_settings() results per user_id. Write operations (set_setting,
delete_setting, set_all_settings) delegate to the inner store then
invalidate that user's cache entry. The write lock is held across DB
loads to prevent stale-data races from concurrent invalidations.

Wire the cache into TenantScope via a new settings_store field on
AgentDeps, so all settings reads in the agent loop go through the
cache. Remove the per-turn cached_tool_permissions Mutex hack from
ChatDelegate that was working around the missing cache layer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address PR review feedback

- Store Arc<HashMap> in cache instead of bare HashMap to avoid cloning
  the full settings map on every cache hit. get_setting/has_settings now
  only clone the single requested value or check emptiness through the Arc.
- Route get_setting_with_admin_fallback() through self.settings() instead
  of self.inner so both the per-user and admin lookups go through the cache.
- Update settings section comment to accurately describe which methods
  delegate through settings().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address all PR review feedback

- Use `crate::db::` imports instead of `super::` (convention fix)
- Add `wrap()` factory fn to CachedSettingsStore, simplify app.rs construction
- Store `Arc<HashMap>` in cache to avoid full map clones on hits
- Expose `invalidate_user()` and `flush()` public methods
- Wire `flush()` into SIGHUP handler via concrete `settings_cache` on AppComponents
- Wire `settings_store` into GatewayState and route all settings handlers
  through it so web UI writes invalidate the cache (critical fix)
- Route `get_setting_with_admin_fallback()` through `self.settings()`
- Add error-path test (FailingStore mock, cache not poisoned on error)
- Add concurrent-access test (8 concurrent readers, inner store hit once)
- Add TenantScope caller-level test (read/write through cache wiring)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: reuse resolve_settings_store() in settings_tools_set_handler

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address all review feedback on CachedSettingsStore

- Add TTL (300s) and max-entries cap (1000) to bound cache staleness
  and memory growth. Entries expire after TTL; cache clears when cap
  exceeded.
- Route admin tool_policy GET/PUT through resolve_settings_store() so
  writes invalidate the __admin__ cache entry.
- Route settings_export_handler and settings_tools_list_handler through
  resolve_settings_store() (were bypassing cache on reads).
- Wire invalidate_user() into users_delete_handler and
  users_suspend_handler so deleted/suspended users' settings are evicted.
- Replace GatewayState.settings_store (trait object) with
  settings_cache (concrete CachedSettingsStore) — single field for both
  trait dispatch and cache management, no desync risk.
- Add settings_override to ExtensionManager with with_settings_store()
  builder. All settings reads/writes in ExtensionManager now route
  through the cached store when available.
- Make ExtensionManager::settings_store() pub(crate); update
  AuthManager to call it instead of database(), closing the auth
  descriptor cache bypass.
- Remove unused wrap() method; merge redundant invalidate/invalidate_user.
- Add tracing::debug on SIGHUP cache flush.
- Expand module docs with design assumptions, known bypass paths, TTL
  and eviction semantics.
- Add tests: expired_entry_triggers_reload, fresh_entry_does_not_reload,
  max_entries_cap_triggers_eviction.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: collapse nested if into filter to satisfy clippy

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:10:10 -07:00
..