fix(agent-server): bound native async runs with max_concurrent_runs - #4389
fix(agent-server): bound native async runs with max_concurrent_runs#4389Shailendra005 wants to merge 3 commits into
Conversation
|
🚦 CI is currently failing on this PR's latest commit. Please fix the failing checks before OpenHands reviews it - this is re-checked automatically once you push a new commit. (A maintainer can also request This is an automated check - no AI was used to generate this comment. |
…own, cover sync path
|
@VascoSch92 all three addressed in 284bb71. Pause/interrupt while queued. You were right, and it was a real hole: a queued run has nothing to cancel, so the request was dropped and the run started anyway once the permit arrived. The run now captures Teardown handing out Sync path untested. Added All three new tests fail on One thing I did not touch: bounded queueing / explicit backpressure from the issue's expected-behaviour list is still out of scope here, since it looks like a policy decision that interacts with #3153. Happy to take it as a follow-up. |
HUMAN:
Picked this up because a limit that only covers the fallback path isn't really a limit. Kept the change to the admission mechanism itself and left the queueing/status questions for you to weigh in on.
AGENT:
Why
max_concurrent_runsis documented as the cap on conversations executing agent steps concurrently, but it only sizes theThreadPoolExecutorbehind the synchronousconversation.run()fallback.EventService.run()prefers nativeconversation.arun()whenever the agent overridesastep(), and that branch takes no permit, so the path normal conversations actually use is unbounded. SettingOH_MAX_CONCURRENT_RUNS=Nhas no effect there, which is what #4063 reports along with the memory figures behind it.Summary
asyncio.Semaphore(max_concurrent_runs)created next to the existing thread pool inConversationService.__aenter__and injected into eachEventServicealongside_run_executor._run_and_publishnow holds a permit across both branches, so one shared pool bounds the total rather than the two paths each having their own ceiling.async withso the permit is returned on exception and on cancellation; a leaked permit would be permanent and would throttle the server toward zero over time._run_semaphoreisNonefor a standaloneEventService, which keeps current unlimited behaviour for embedders and existing tests.Issue Number
Closes #4063
How to Test
tests/agent_server/test_event_service.py::TestRunAdmissionLimitimplements the shared active-counter check described in the issue: threeEventServiceinstances share oneSemaphore(1), each is driven through the realawait service.run()with an agent whosearun()blocks, and the test asserts the peak observed concurrency never exceeds the limit. A second test asserts the permit is released when a run raises.Reverting just the
async withline and re-running gives the reported behaviour:Full suite and checks:
Two notes for anyone extending these tests:
has_native_aruninspectstype(conversation).arunandtype(conversation.agent).astep, so the methods have to be defined on mock subclasses — instance attributes silently fail the check and route the run down the sync branch instead. And_run_taskis cleared when a run finishes, so task handles need capturing before the blocked runs are released.Scope
This covers the first and last bullets of the expected behaviour: a shared admission mechanism across both paths, with permits released on exception and cancellation. Two of your bullets are deliberately not in this PR, since they look like design calls rather than mechanics:
Happy to take either as a follow-up, or to fold one in here if you would prefer it in a single change.
I also did not add a separate sync-agent test: that path was already bounded by the executor, and it now shares the same permit pool, which the exception test exercises. Glad to add an explicit one if you want the coverage stated.