open-webui/backend/open_webui/utils
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
..
access_control chore: format 2026-04-17 14:28:18 +09:00
images refac 2026-04-15 10:17:40 -07:00
mcp refac: async db 2026-04-12 14:22:11 -05:00
telemetry refac: async db 2026-04-12 14:22:11 -05:00
actions.py refac: async db 2026-04-12 14:22:11 -05:00
anthropic.py chore: format 2026-04-14 17:27:31 -05:00
asgi_middleware.py chore: format 2026-04-14 17:27:31 -05:00
audit.py refac 2026-04-12 16:25:01 -05:00
auth.py chore: format 2026-04-12 18:12:59 -05:00
automations.py refac 2026-04-20 08:34:15 +09:00
calendar.py feat: calendar 2026-04-19 19:15:05 +09:00
channels.py refac 2026-03-17 17:58:01 -05:00
chat.py refac 2026-04-20 08:53:06 +09:00
code_interpreter.py refac 2026-03-17 17:58:01 -05:00
embeddings.py refac: async db 2026-04-12 14:22:11 -05:00
files.py refac 2026-04-20 09:21:30 +09:00
filter.py refac: async db 2026-04-12 14:22:11 -05:00
groups.py refac: async db 2026-04-12 14:22:11 -05:00
headers.py refac 2026-03-17 17:58:01 -05:00
logger.py refac 2026-04-10 10:15:55 -07:00
middleware.py chore: format 2026-04-19 22:45:54 +09:00
misc.py refac 2026-04-17 12:37:44 +09:00
models.py chore: format 2026-04-17 14:28:18 +09:00
oauth.py refac 2026-04-17 12:25:43 +09:00
payload.py refac 2026-04-14 16:47:43 -05:00
pdf_generator.py refac 2026-03-17 17:58:01 -05:00
plugin.py refac: async db 2026-04-12 14:22:11 -05:00
plugin_cache.py feat(plugins): cross-worker tool + function cache invalidation via Redis 2026-04-20 14:24:33 +00:00
rate_limit.py refac 2026-03-17 17:58:01 -05:00
redis.py chore: format 2026-04-12 18:12:59 -05:00
response.py refac 2026-03-24 04:49:48 -05:00
sanitize.py refac 2026-03-17 17:58:01 -05:00
security_headers.py refac 2026-04-20 09:10:48 +09:00
session_pool.py refac 2026-04-14 01:17:39 -05:00
task.py fix: Fix the error that occurs when the task model does not exist (#23169) 2026-04-01 07:01:25 -05:00
tools.py feat(plugins): cross-worker tool + function cache invalidation via Redis 2026-04-20 14:24:33 +00:00
validate.py chore: format 2026-04-12 18:12:59 -05:00
webhook.py refac 2026-04-20 08:34:15 +09:00