Provider-slot concurrency cap is silently skipped in the post-session fan-out: "Event loop is closed" #573
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Seen on the dev replay of 2026-09-09. Every LLM call made by the post-session fan-out tasks (storyline, title suggestions, lore proposals, highlights) logged:
So the #356 slot machinery, which exists to keep a self-hosted router from being hammered by parallel tasks, never applies to the fan-out. The message is the graceful-degradation path doing its job, but the condition it degrades on is a bug, not an outage: the Redis client used by
provider_slotsis bound to an asyncio loop that a previousasyncio.run()in the same Celery worker process has already closed, and the next task's loop cannot use it.Fix. Create the slot client per call (or per event loop) rather than caching one on the module or worker process; the pattern already used elsewhere for async Redis in Celery tasks should apply. A test that runs two fan-out tasks in sequence in one process and asserts the second still takes a slot.
Related: #356 (the cap), #357 (usage telemetry, which saw the calls).
Fix is on its way in the PR opened alongside this comment. Two things the investigation added to the issue as filed:
bot_pubsub(and, harmlessly for now,auth/session). Thebot_pubsubcase matters:detect_unread_trimandpublish_bot_event_asyncrun underasyncio.runin worker tasks, so the "were bot-event stream entries trimmed unread?" check has been answering "no data" rather than "no loss" whenever it ran as a worker's second-or-later task. Fixed by the same per-loop pool.asyncio.runcycles in one process, the fallback fired on every other one. The alternation is the tell: the failure poisons the connection, redis-py drops it, the next loop opens a fresh one, and the loop after that finds it closed.