Repository navigation
Fix ToolManager instance retention - #337
Nandith-0777 wants to merge 4 commits into
Conversation
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (1)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. Walkthrough
ChangesToolManager Weak Instance Registry
Priority: ➖ Normal Estimated code review effort: 2 (Simple) | ~10 minutes Change: Bug fix · Severity of issue fixed: Medium Merge Risk: ⚪ Minimal · up to Unused ToolManager instances can be collected, and bulk updates use construction-safe snapshots. The change is ready to merge. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. A rabbit sees the weak set glow Comment |
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
Hi @Nandith-0777 ,I opened #338 for the same issue a day after this one, before your Sep 22 update. Your current branch covers both the leak and the |
Pre-PR Checklist
Summary
ToolManager.instancesheld a strong reference to every manager ever created (one perLLMAgent), so none could be garbage-collected. This PR makes the registry weak and keepsadd_tool_to_all()safe under concurrent construction, which a bareWeakSetdoes not.Bug / Issue
Fixes #336.
Measured on
main: after building and discarding 20 models × 25 agents, 500 managers were still registered (agents and models themselves were freed). Memory grows without bound across long runs and batch sweeps, and everyadd_tool_to_all()call also iterates all dead managers.Implementation
instancesis now aweakref.WeakSet, so managers are released as soon as the application drops them.__init__, afterself.toolsis fully populated, so a concurrentadd_tool_to_all()snapshot can never observe a half-constructed manager.add_tool_to_all()takes a snapshot of the registry and retries only if that snapshot is interrupted by a concurrent.add()— no lock anywhere, no overhead added to__init__.Why not just swap in a bare
WeakSet(my own first commit here, and the approach in #338): iterating aWeakSetwhile another thread adds to it raisesRuntimeError: Set changed size during iteration. I also found something one level worse — registering the instance at the start of__init__(beforeself.toolsis set) means a concurrent snapshot can capture a half-built manager, soadd_tool_to_all()crashes withAttributeError: 'ToolManager' object has no attribute 'tools'instead. Moving registration to the end of__init__fixes both at once: no half-constructed instance is ever visible, and the retry loop handles the ordinary "new manager added mid-snapshot" case without a lock.Why no lock:
add_tool_to_all()can be called frequently andToolManageris constructed per-agent, so a lock there adds contention to a hot path. The retry-on-interrupted-snapshot approach gets the same correctness without taking a lock on every construction or every broadcast.Alternatives considered:
threading.Lockguarding bothadd()and the broadcast snapshot (the approach in Hold ToolManager instances weakly in the class registry #338): correct, but adds synchronization overhead to everyToolManager()construction, which happens once per agent.weakref.refwith removal callbacks: callbacks fire during GC, possibly mid-iteration, and mutating a list during iteration silently skips entries.Testing
New regression tests in
tests/test_tools/test_tool_manager.py:mainWeakSet(my 1st commit)test_tool_manager_can_be_garbage_collectedtest_add_tool_to_all_skips_collected_managerstest_add_tool_to_all_tolerates_manager_created_mid_broadcastAttributeError/RuntimeError(~1 in 12 runs under a tightened thread-switch interval)main500 → 500 retained; this PR 500 → 0 retained.test_async_generator_cleanup.py, pre-existing and timing-sensitive — they fail identically on unmodifiedmain, confirmed unrelated to this change.pre-commit run --all-filespasses (ruff check,ruff format).Additional Notes
instanceskeeps the operations used in the codebase (len,in, iteration,.clear())..append()and indexing are no longer available; nothing in the repo uses them.WeakSetis unordered. Broadcast order has no effect because eachregister()call is independent.WeakSet, before I'd found and fixed the race). It correctly identifies that a bareWeakSetisn't safe under concurrent construction, but that's now fixed on this branch via snapshot-and-retry rather than a lock, so the two PRs currently overlap almost entirely. Happy to consolidate in whatever way maintainers prefer — main difference is lock vs. lock-free retry foradd_tool_to_all().Summary by CodeRabbit
Bug Fixes
Tests