Skip to content

tbench2 env server: enable idle-session reaper to reclaim leaked slots - #1000

Merged
burtenshaw merged 1 commit into
huggingface:mainfrom
Shi-Dong:shi/openenv-session-reaper-timeout
Sep 16, 2026
Merged

burtenshaw merged 1 commit into
huggingface:mainfrom
Shi-Dong:shi/openenv-session-reaper-timeout

Conversation

@Shi-Dong

@Shi-Dong Shi-Dong commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

What

The tbench2_env server now enables the core idle-session reaper by passing an explicit ConcurrencyConfig(max_concurrent_envs=..., session_timeout=...) to create_app, instead of the bare max_concurrent_envs= argument. A new SESSION_IDLE_TIMEOUT_S env var (default 3600) controls the reap interval.

Why

The /mcp WebSocket handler releases its session slot and Docker container only in a finally block, which runs solely on a clean socket close. Half-open or uncleanly-dropped connections never raise WebSocketDisconnect, so receive_text() blocks forever, the finally never runs, and the session slot + its Docker container leak.

Under sustained load these leaks accumulate over roughly a day until all MAX_CONCURRENT_ENVS slots are permanently held. New episodes then deadlock on CAPACITY_REACHED and the only recovery is a manual env-server restart.

Passing a bare max_concurrent_envs maps to ConcurrencyConfig(session_timeout=None) in the core server, which disables _reap_idle_sessions. Supplying an explicit session_timeout re-enables the reaper so stale sessions are evicted and their containers reclaimed automatically, without operator intervention.

Notes

  • Default SESSION_IDLE_TIMEOUT_S=3600 preserves existing behavior for short-lived sessions while bounding leaked slots.
  • Single-file change scoped to envs/tbench2_env/server/app.py; no change to the core server or to the on-the-wire protocol.

Note

Low Risk
Single-file server bootstrap change with a conservative default timeout; no protocol or core server edits.

Overview
The tbench2_env server now passes an explicit ConcurrencyConfig to create_app (with max_concurrent_envs and session_timeout) instead of only max_concurrent_envs, which turns on the core idle-session reaper that was previously off when session_timeout was unset.

A new SESSION_IDLE_TIMEOUT_S env var (default 3600) sets how long a session can sit idle before it is reaped so its WebSocket slot and Docker container are released. That targets leaks from half-open /mcp WebSocket connections that never hit a clean disconnect, which could otherwise hold all MAX_CONCURRENT_ENVS slots until a manual restart.

Reviewed by Cursor Bugbot for commit adc5d44. Bugbot is set up for automated code reviews on this repo. Configure here.

The /mcp WebSocket handler releases its session slot and Docker container
only in a finally block, which runs solely on a clean socket close.
Half-open or uncleanly-dropped connections never raise WebSocketDisconnect,
so receive_text() blocks forever, finally never runs, and the slot +
container leak. Over ~20-24h these accumulate until all MAX_CONCURRENT_ENVS
slots are held and new episodes deadlock on CAPACITY_REACHED, requiring a
manual env-server reset.

create_app was called with a bare max_concurrent_envs, which the core
server maps to ConcurrencyConfig(session_timeout=None) -> the idle-session
reaper is disabled. Pass an explicit ConcurrencyConfig with session_timeout
instead (SESSION_IDLE_TIMEOUT_S env var, default 3600s) so
_reap_idle_sessions runs and evicts stale sessions, reclaiming their
containers automatically.
@bot-ci-comment

Copy link
Copy Markdown

The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update.

@nblintao nblintao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

@caiotheodoro caiotheodoro left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src/openenv/core/env_server/http_server.py:1168 — risk: this is the first env in the repo to turn the idle reaper on (grep shows no other env passes session_timeout), and it surfaces a gap in the shared reaper path rather than one in this diff. mcp_websocket_endpoint closes over session_env as a local and never rereads self._sessions. _destroy_session (line 441) pops the session and calls env.close() — for tbench2 that stops+removes the Docker container — without touching the websocket itself. A client that's slow rather than dead, past the 1hr SESSION_IDLE_TIMEOUT_S, gets its container killed mid-connection; its next message still runs mcp_handler against the now-closed session_env and comes back as an opaque INTERNAL_ERROR instead of a clean timeout signal. Worth closing the websocket (or flagging the session so the handler bails cleanly) from _destroy_session before this ships, since it's the first PR to make that path reachable.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Still not merge-ready despite the earlier human approval on adc5d447.

  • Branch is 117 commits behind main (mergeStateStatus: BEHIND). Refresh and re-run exact-head CI before any merge attempt.
  • The follow-up comment from caiotheodoro is a real product concern: enabling the idle reaper for the first time can destroy a live Docker-backed session while the WebSocket is still open, turning a slow client into an opaque INTERNAL_ERROR. Prefer closing/signaling the websocket from session destroy, or otherwise fail cleanly, before this becomes the first production caller of that path.
  • Automation cannot update the PR branch (403). Author/maintainer refresh needed.
Open in Web View Automation 

Sent by Cursor Automation: Release

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Refresh review vs main b12450f2 (Ben ask):

Verdict: LGTM after rebase. Exact head adc5d447 is ~129 behind main; historical CI was green and nblintao APPROVED. Diff correctly enables core idle reaper via ConcurrencyConfig(session_timeout=SESSION_IDLE_TIMEOUT_S) (default 3600) for tbench2 capacity leaks from half-open sockets. Env-local, low risk. Please rebase onto current main and re-run CI, then this is squash-merge ready.

View PR

Open in Web View Automation 

Sent by Cursor Automation: Release

@burtenshaw

Copy link
Copy Markdown
Collaborator

Thanks

@burtenshaw
burtenshaw merged commit 3859f11 into huggingface:main Sep 16, 2026
8 checks passed

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refresh review (Wed 2026-09-16)

Still BEHIND main (adc5d447). Diff is still only enabling session_timeout in envs/tbench2_env/server/app.py.

Prior blocker stands: core _destroy_session does not close/signal the MCP WebSocket. Enabling the reaper makes tbench2 the first production caller of that broken path → opaque INTERNAL_ERROR / hung receive for slow clients. #1169’s /ws?session_id= attach exemptions do not protect ordinary /mcp WS sessions.

Also: please remove the copy of this change that was folded into #1172 until the core signaling fix exists.

Needs author/core fix — not mergeable after refresh alone.

View PR

Open in Web View Automation 

Sent by Cursor Automation: Release

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants