fix(core): unbind state-saver on agent close to prevent OOM leak#2322
Open
Buktal wants to merge 1 commit into
Open
fix(core): unbind state-saver on agent close to prevent OOM leak#2322Buktal wants to merge 1 commit into
Buktal wants to merge 1 commit into
Conversation
GracefulShutdownManager.stateSavers was write-only (bindStateSaver puts, no removal path), so every agent built with a stateStore pinned its entire object graph forever via the manager's process-wide singleton. With agentId = UUID per instance and a saver lambda capturing enclosing this, short-lived agents created per call caused monotonic heap growth ending in OOM. - Add GracefulShutdownManager.unbindStateSaver(Agent), symmetric with bindStateSaver, mirroring registerRequest/unregisterRequest. - Call it from ReActAgent.close() (HarnessAgent delegates here via delegate.close()). - Capture the store in a final local so the saver lambda does not capture enclosing this, shrinking any leaked entry. Closes agentscope-ai#2321
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
AgentScope-Java Version
2.0.0
Description
Fixes #2321.
Background
GracefulShutdownManager.stateSaversis a process-wide singletonConcurrentHashMapthat was effectively write-only:bindStateSaver()puts an entry keyed byagentId, but there was no removal path anywhere in production code (onlystateSavers.clear()insideresetForTesting()). SinceagentId = UUID.randomUUID()is a fresh key per instance, and the registered saver lambda captured the enclosing agentthis(via the instance fieldstateStore), every agent built with astateStorepermanently pinned its entire object graph (model/HttpClient, toolkit,stateCacheconversation history, middlewares) from theGracefulShutdownManager.INSTANCEGC root - even afteragent.close()was correctly called.In "create a fresh agent per call" deployments (e.g. wiring a new agent per request against Spring
@RefreshScope/ Nacos dynamic config), this produced monotonic heap growth ending inOutOfMemoryError(observed in production: ~2.74 GB retained by the singleton, 373 leakedHttpClientSelectorManager threads, recurring OOM within days).The sibling map
activeRequestsByIdalready has a symmetricregisterRequest/unregisterRequestlifecycle (the latter invoked fromdoFinally);stateSaverswas missing that unregister half.Changes
GracefulShutdownManager.unbindStateSaver(Agent)- new public method, symmetric withbindStateSaver, mirroring theregisterRequest/unregisterRequestpair. No-op onnull/ unregistered agent.ReActAgent.close()- now callsshutdownManager.unbindStateSaver(this)so the entry's lifetime is bound to the agent instance.HarnessAgent.close()already delegates toReActAgent.close()viadelegate.close(), so wrapped agents are covered automatically. Notry/finallyis needed sinceunbindStateSavercannot throw.ReActAgentconstructor - the saver lambda now captures afinal AgentStateStore stateSaverStorelocal instead of the enclosingthis. Defense in depth: even if an entry is somehow left behind (e.g. a caller forgetsclose()), it no longer pins the entire agent object graph - only a single store reference. (Item 1 is the root fix; item 3 alone is insufficient since the entry would still accumulate.)Tests
Added 3 unit tests in
GracefulShutdownTest:unbindStateSaver removes a previously bound saver- end-to-end:bind->registerRequest->saveOnInterruptObservedinvokes the saver;unbind->registerRequest->saveOnInterruptObservedis a no-op (verifies the saver is actually removed from the map, sinceActiveRequestContext.saveStateshort-circuits on anullsaver).unbindStateSaver with null agent is no-opunbindStateSaver for an unregistered agent is no-opHow to test
mvn -pl agentscope-core testResult:
Tests run: 2218, Failures: 0, Errors: 0, Skipped: 8- all green, including the 3 new tests.Checklist
mvn spotless:applymvn test) - core suite green (2218 tests); harness failures are pre-existing Windows@TempDirissues (see note above)