Commit graph

1 commit

Author SHA1 Message Date
Claude
498a04dc6c
feat(plugins): cross-worker tool + function cache invalidation via Redis
Bug: request.app.state.TOOLS / FUNCTIONS are per-worker Python dicts.
Save on worker A does not touch worker B's cache, so worker B keeps
serving the stale compiled module until the process restarts.
utils/tools.py's invocation path was also completely cache-blind —
a bare dict.get with no content-hash fallback, so even same-worker
edits skipped the TOOL_CONTENTS check and served the first-seen
module forever.

Symptom: editing a tool in the Workspace UI, triggering a chat, and
watching the OLD code execute until backend restart. Confirmed on a
multi-worker deployment (UVICORN_WORKERS > 1) with Redis.

Fix — three parts:

1. backend/open_webui/utils/plugin_cache.py (new)
   * publish_invalidation(app, kind, id)
     - drops the local TOOLS/FUNCTIONS + _CONTENTS entry
     - publishes {kind, id} on REDIS_PLUGIN_CACHE_CHANNEL
   * plugin_cache_listener(app)
     - subscribes on startup, drops caches on every inbound message
     - mirrors the existing redis_task_command_listener pattern
   * falls back to pure local invalidation when Redis is unconfigured

2. utils/tools.py
   * invocation path swapped from `TOOLS.get(tool_id)` to
     `await get_tool_module_from_cache(request, tool_id)`, which does
     content-hash invalidation from DB. Covers edits that predate or
     race the Redis pub/sub message.

3. routers/tools.py + routers/functions.py
   * create / update / delete / toggle / toggle_global all call
     publish_invalidation after the DB mutation commits.
   * toggle_function_by_id + toggle_global_by_id gained a `request`
     parameter they were missing.

main.py lifespan starts the listener alongside the task listener and
cancels it on shutdown. No new env vars — reuses REDIS_URL and
REDIS_KEY_PREFIX.

Single-worker deployments still work: publish_invalidation
unconditionally drops the local cache before trying to publish.
2026-04-20 14:24:33 +00:00