Skip to content

Commit 3cdfb95

Browse files
stonks-gitclaude
andcommitted
Fix stdin peripheral thread pool exhaustion in Docker
The stdin peripheral was spawning a new run_in_executor thread every 1s for readline, but with tty:true in Docker, readline blocks forever. This exhausted the default thread pool (32 threads), starving the entire event loop — including Telegram polling. Fix: Use a single persistent reader thread with an asyncio bridge queue instead of repeated run_in_executor calls. Also removed stdin_open/tty from norisor docker-compose.yml since Telegram is now the primary interface. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 56bf262 commit 3cdfb95

1 file changed

Lines changed: 25 additions & 5 deletions

File tree

src/stdin_peripheral.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@
22
33
The original input method, factored into a peripheral that pushes
44
AttentionCandidate objects into the unified input queue.
5+
6+
Uses a single persistent reader thread to avoid thread pool exhaustion
7+
when stdin is a TTY (e.g. Docker with tty: true).
58
"""
69

710
import asyncio
811
import logging
912
import sys
13+
import threading
1014

1115
from .attention import AttentionCandidate
1216

@@ -26,15 +30,31 @@ async def run(self, shutdown_event: asyncio.Event):
2630
return
2731

2832
logger.info("Stdin peripheral started.")
33+
34+
# Use a dedicated thread + asyncio.Queue to avoid thread pool exhaustion.
35+
# A single thread runs readline() in a blocking loop, putting results
36+
# into a bridging queue that the async side reads from.
37+
bridge: asyncio.Queue = asyncio.Queue()
2938
loop = asyncio.get_event_loop()
3039

40+
def _reader_thread():
41+
"""Blocking reader thread — runs until stdin closes or EOF."""
42+
while not shutdown_event.is_set():
43+
try:
44+
line = sys.stdin.readline()
45+
if not line: # EOF
46+
break
47+
loop.call_soon_threadsafe(bridge.put_nowait, line)
48+
except Exception:
49+
break
50+
51+
reader = threading.Thread(target=_reader_thread, daemon=True)
52+
reader.start()
53+
3154
while not shutdown_event.is_set():
3255
print("you> ", end="", flush=True)
3356
try:
34-
line = await asyncio.wait_for(
35-
loop.run_in_executor(None, sys.stdin.readline),
36-
timeout=1.0,
37-
)
57+
line = await asyncio.wait_for(bridge.get(), timeout=2.0)
3858
except asyncio.TimeoutError:
3959
continue
4060

@@ -45,7 +65,7 @@ async def run(self, shutdown_event: asyncio.Event):
4565
candidate = AttentionCandidate(
4666
content=text,
4767
source_tag="external_user",
48-
metadata={"reply_fn": _make_print_reply()},
68+
metadata={"reply_fn": _make_print_reply(), "peripheral": "stdin"},
4969
)
5070

5171
try:

0 commit comments

Comments
 (0)