Skip to content

perf: batch salvage of 5 verified micro-fixes (cache leaks, per-call waste, blocking sleep, O(n²) drain) - #35758

Merged
kshitijk4poor merged 6 commits into
NousResearch:mainfrom
kshitijk4poor:salvage/perf-microfixes-batch
May 31, 2026
Merged

perf: batch salvage of 5 verified micro-fixes (cache leaks, per-call waste, blocking sleep, O(n²) drain)#35758
kshitijk4poor merged 6 commits into
NousResearch:mainfrom
kshitijk4poor:salvage/perf-microfixes-batch

Conversation

@kshitijk4poor

@kshitijk4poor kshitijk4poor commented May 31, 2026

Copy link
Copy Markdown
Collaborator

Batch salvage of 5 small, independent, verified type/perf fixes (user-requested). Each is a self-contained leak / per-call-waste / blocking-call fix in its own subsystem — non-overlapping files, all ≤38 lines. Original authorship preserved via cherry-pick.

Included PRs

Original PR Author Fix
#23706 @AhmetArif0 feishu: LRU-cap _message_text_cache (was an unbounded plain dict keyed by message_id)
#30523 @dskwe bluebubbles: LRU-cap _guid_cache (was unbounded)
#22155 @amathxbt tool_output_limits: cache limits for process lifetime instead of re-reading config on every tool call
#32636 @ErnestHysa MCP auth reconnect poll: replace blocking time.sleep(0.25) with await asyncio.sleep(0.25) on the MCP loop
#32708 @ErnestHysa gateway: drain pending_watchers without list.pop(0) (avoids O(n²) on large recovery), add event-loop yield points, upgrade plugin/bundle dispatch failure logs from debug→warning

Follow-up commit (mine)

One small test-consistency commit on top, required by two of the changes on current main:

  • test_tool_output_limits.py: autouse fixture resets the new module-level limits cache between tests (otherwise the first test's read leaks into the rest → 5 failures).
  • test_feishu.py: hand-built adapter fixture seeded _message_text_cache as a plain dict; it's now an OrderedDict and _fetch_message_text calls .move_to_end(), so the fixture type had to match.

Verification

  • All 5 diffs verified against current main by reading the actual code at each touched site — confirmed each problem still exists (not already fixed / not stale).
  • Each cherry-pick applied cleanly; combined diff reviewed for cross-PR / current-main interactions.
  • Targeted tests pass: tests/gateway/test_feishu.py, tests/gateway/test_bluebubbles.py, tests/tools/test_tool_output_limits.py, tests/tools/test_mcp_circuit_breaker.py276 passed.
  • Import smoke check on all touched modules + gateway.run passes.

Deliberately excluded during triage

AhmetArif0 and others added 5 commits May 31, 2026 12:48
…ounded growth

_message_text_cache was a plain dict with no size limit. Every unique
message_id whose text was fetched (for reply-context lookups) stayed in
memory permanently, causing unbounded growth in long-running deployments
with active group chats.

Replace with an OrderedDict and evict the least-recently-used entry
whenever the cache exceeds _FEISHU_MESSAGE_TEXT_CACHE_SIZE (512). Cache
hits call move_to_end() to refresh LRU order. Mirrors the identical
pattern already used by _pending_processing_reactions in the same class.
…ded growth

The _guid_cache dict grows without bound as new contacts/groups are
resolved.  In a long-running gateway instance with many unique targets
this becomes a slow memory leak.

Replace the plain dict with an OrderedDict capped at 500 entries.
When the cap is exceeded the oldest (least-recently-used) entries are
evicted.
…nnect poll

PAIN BEFORE:
Inside _handle_auth_error_and_retry() (a sync function that runs on the MCP
event loop thread), there was a blocking polling loop:

    while time.monotonic() < deadline:
        if srv.session is not None and srv._ready.is_set():
            break
        time.sleep(0.25)   # BLOCKS THE ENTIRE EVENT LOOP

Since _handle_auth_error_and_retry is invoked from tool handlers that run ON
the MCP event loop, time.sleep(0.25) blocked ALL concurrent MCP operations
(including other tools, keepalive heartbeats, OAuth refreshes) for 250ms per
iteration. With a 15-second deadline, worst case = 60 * 250ms = 15 seconds
of fully blocked concurrency.

WHAT WAS FIXED:
Extracted the blocking poll into an async helper _await_ready() that uses
asyncio.sleep(0.25) (non-blocking), and runs it via _run_on_mcp_loop().
_run_on_mcp_loop() properly awaits the coroutine on the event loop without
blocking the caller's thread. Added exception handling around the poll so
stuck reconnects still fall through to the error path.

The sync _handle_auth_error_and_retry now:
1. Fires reconnect signal (threadsafe)
2. Calls _run_on_mcp_loop(_await_ready(), timeout=15) — non-blocking
3. Returns; the event loop handles the polling

File: tools/mcp_tool.py
Lines: _handle_auth_error_and_retry() (~1886-1920)

Found by: exhaustive multi-pass audit (10 strategies, 1901 files, 913K lines)
…her recovery

N43 — Silent plugin/bundle errors:
- Plugin command dispatch: logger.debug() -> logger.warning()
- Bundle dispatch: logger.debug() -> logger.warning()
Plugin/auth failures are no longer invisible to operators.

N42 — O(n^2) pending_watchers recovery:
- Both recovery loops (startup + per-message) used while+pop(0) which is O(n) per pop
- Replaced with enumerate() over the list + periodic asyncio.sleep(0) yield points
- Clears the list after iteration instead of per-pop
- Batch size of 100 balances throughput vs event-loop responsiveness
@kshitijk4poor
kshitijk4poor force-pushed the salvage/perf-microfixes-batch branch from 7fc05c9 to a2d9b76 Compare May 31, 2026 07:25
@kshitijk4poor kshitijk4poor changed the title perf: batch salvage of 7 verified micro-fixes (cache leaks, per-call waste, blocking sleep, O(n²) drain) perf: batch salvage of 5 verified micro-fixes (cache leaks, per-call waste, blocking sleep, O(n²) drain) May 31, 2026
@alt-glitch alt-glitch added type/perf Performance improvement or optimization P2 Medium — degraded but workaround exists comp/gateway Gateway runner, session dispatch, delivery comp/tools Tool registry, model_tools, toolsets tool/mcp MCP client and OAuth platform/feishu Feishu / Lark adapter labels May 31, 2026
@kshitijk4poor
kshitijk4poor force-pushed the salvage/perf-microfixes-batch branch 2 times, most recently from a3c6678 to 7b7d18a Compare May 31, 2026 07:43
…align test fixtures + AUTHOR_MAP

Self-review follow-up on top of the salvaged perf fixes:

- gateway/run.py (both watcher-drain sites): the salvaged O(n^2) fix
  (NousResearch#32708) replaced `while pending_watchers: pop(0)` with iterate-then-
  `watchers.clear()`, but `watchers` aliased the registry's live list.
  A watcher appended by a concurrent session during the `await
  asyncio.sleep(0)` yield would be cleared without ever being scheduled.
  Detach the batch atomically (`pending_watchers = []`) before iterating.

- gateway/platforms/bluebubbles.py: normalize the salvaged _guid_cache
  LRU (NousResearch#30523) to match feishu/codebase precedent — module-level
  `_GUID_CACHE_SIZE` constant, `while len > cap`, and drop the redundant
  post-insert `move_to_end` (a fresh insert is already most-recent).

- gateway/platforms/feishu.py: drop the same redundant post-insert
  `move_to_end` from the salvaged _message_text_cache LRU (NousResearch#23706).

- scripts/release.py: add AUTHOR_MAP entries for the salvaged commits'
  authors (amathxbt NousResearch#22155, ErnestHysa NousResearch#32636/NousResearch#32708) so the contributor
  audit passes when these commits land on main.

- tests/tools/test_tool_output_limits.py: autouse fixture resets the new
  module-level limits cache between tests.

- tests/gateway/test_feishu.py: hand-built adapter fixture seeded
  _message_text_cache as a plain dict; it's now an OrderedDict, so the
  fixture type had to match.
@kshitijk4poor
kshitijk4poor force-pushed the salvage/perf-microfixes-batch branch from 7b7d18a to 123d556 Compare May 31, 2026 07:45
@kshitijk4poor
kshitijk4poor enabled auto-merge (rebase) May 31, 2026 07:45
@kshitijk4poor
kshitijk4poor merged commit 3289927 into NousResearch:main May 31, 2026
22 checks passed
@kshitijk4poor
kshitijk4poor deleted the salvage/perf-microfixes-batch branch August 5, 2026 07:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery comp/tools Tool registry, model_tools, toolsets P2 Medium — degraded but workaround exists platform/feishu Feishu / Lark adapter tool/mcp MCP client and OAuth type/perf Performance improvement or optimization

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants