Interaction-based idle culling for VS Code (code-server) - #226
Interaction-based idle culling for VS Code (code-server)#226tylerpotts wants to merge 22 commits into
Conversation
Design for #208: stop counting VS Code proxy keepalives as jupyter activity, report real user interaction via a server-side code-server extension, and wire CODE_SERVER_IDLE_TIMEOUT_SECONDS to the idle culler timeout.
Also records pre-implementation corrections in the spec: registration moves to the image-owned jupyter_server_config.py, vsix packaging uses a stdlib script instead of vsce, and e2e adapts to the kind/HTTP harness.
…wiring Final-review fixes for the VS Code idle-culling feature (issue #208): - Flip VSCODE_PROXY_UPDATE_LAST_ACTIVITY polarity: absent/empty now means the image's OLD behavior (count proxied traffic as activity); the chart actively sets "false" to opt pods into the new interaction-based behavior. Chart/image skew now fails safe (over-spend) instead of culling active users who lack the activity-reporter extension. - Correct the culling mental model in the design spec and configuration docs: configurable-http-proxy still sees websocket activity on the /vscode/ route regardless of update_last_activity, so the hub-level jupyterhub.cull culler is not fixed by this change. The load-bearing mechanism is the in-pod singleuserCuller.server.shutdownNoActivityTimeout. Extended the e2e module docstring to note the in-pod curls bypass CHP and cannot observe that hub-side signal. - postStart install now runs under `timeout 60` so a hung install cannot stall pod startup, and mirrors CODE_EXTENSIONSDIR via ${CODE_EXTENSIONSDIR:+--extensions-dir "$CODE_EXTENSIONSDIR"} so the extension installs into the same directory VS Code reads from. - Guard the cull.timeout int() conversion so a non-numeric deployer value disables the feature instead of raising and crashing the spawner config. Tests updated/added first (TDD) for each behavior change: inverted the proxy-activity env-var tests, renamed the image-config tests to match the new semantics, updated the postStart command assertion, and added boundary (61) and non-numeric cull.timeout regression tests.
|
Docs preview for |
The e2e job pulls the singleuser image tag pinned in values.yaml, so without this bump the two image-dependent VS Code idle-culling tests would run against the pre-branch image and fail. sha-7a07d8e is the Build Docker Images output for this PR's merge commit.
spawn_user waits for the pod Ready condition, but singleuser pods have no readiness probe on the jupyter port, so kubectl exec raced jupyterhub-singleuser binding :8888 (curl rc=7 in CI). Poll /api/status until it answers before the server-dependent assertions.
|
CI status note: all checks are green except The four new e2e tests pass against this branch's built image ( |
Culling behavior reference: what keeps a VS Code pod alive, and where it can cull against user intentFor reviewers (and future docs): the exhaustive list, derived from the implementation in this PR ( What keeps the pod alive (resets the in-pod idle clock)The extension reports activity (throttled to one ping / 60s) on:
Independent of the extension, the pod also stays alive through the pre-existing Jupyter-side signals: a busy notebook kernel (never culled, Where it can cull against user intent (false-idle cases)
Knobs / workarounds
|
|
Late to this and most of what I had is already in your reference post and the description, so just the bits I don't think are covered. Your item 6, extension-delivery failure. The guards listed are the e2e install tripwire and activation logging, and I think both sit at the wrong layer for the failure that'll actually happen. The chart sets Could the two share a fate? postStart writing a marker on success and
For the soak: https://redirect.github.com/coder/code-server/blob/v4.133.0/src/node/routes/index.ts#L35-L53 If that trips with a browser attached the state goes Last one, minor: Hey @krassowski , pinging you here because you might have some idea around idle culling or code-server on the Jupyter side. Short version is that an open but idle VS Code tab keeps the pod alive, since jupyter-server-proxy counts the websocket keepalives as API activity, and this PR opts the |
- CODE_SERVER_IDLE_TIMEOUT_SECONDS now derives from singleuserCuller.server.shutdownNoActivityTimeout (via _CHART_DERIVED) instead of cull.enabled/cull.timeout: the in-pod culler is the schedule idle pods actually cull on, and disabling the hub culler no longer silently turns the code-server exit timer off. - update_last_activity=False now also requires the activity-reporter artifact to be present in the extensions dir (shared fate): a failed per-pod vsix install degrades to over-spending instead of culling an actively-working user with no keep-alive channel. - Fix stale comment claiming the hub idle culler is fixed by update_last_activity; CHP route-level websocket activity defeats it regardless. - Update unit/e2e tests and docs accordingly. Addresses #226 (comment) review feedback from @viniciusdc.
|
@viniciusdc all four points were on target. What happened with each: Extension-delivery shared fate: implemented (f656491). Went with a variant of your marker idea: instead of a postStart-written marker,
Full soak on the review-fix image ( |
Closes #208
Problem
An open VS Code tab holds a websocket whose keepalives flow through jupyter-server-proxy and count as Jupyter activity, so pods with an idle VS Code tab are never culled. The heartbeat itself is a local file touch; the proxied connection traffic is what defeats culling. The env var proposed in the issue (
CODE_SERVER_IDLE_TIMEOUT_SECONDS) only fires after all browser connections close (code-server's idle timer gates ongetConnections() > 0), so on its own it does not cover the open-tab case — and it doesn't exist in the previously pinned code-server 4.104.3 (added in 4.106.0 via coder/code-server#7539).What this PR does
Two composing mechanisms:
vscodejupyter-server-proxy entry is now registered by this repo (thejupyter-vscode-proxypackage is removed) withupdate_last_activity: False, so VS Code keepalives stop refreshing the in-podapi_last_activity. A bundled server-side VS Code extension (nebari-activity-reporter, plain JS, packaged to a vsix by a stdlib script at image build, installed per-user by a postStart hook) reports real interaction — typing, scrolling, terminal use, focus — and treats a running terminal command as busy (mirrorscullBusy: false). Idle-tab pods are then culled by the in-podsingleuserCuller.server.shutdownNoActivityTimeout(900s default). Note: the hub-leveljupyterhub.cullculler still sees CHP-level websocket activity while a tab is connected, so the in-pod culler is the load-bearing mechanism — documented indocs/src/content/docs/configuration.md.install.shis byte-identical between tags; hash pin unchanged) andCODE_SERVER_IDLE_TIMEOUT_SECONDSis derived fromjupyterhub.cull.timeout(skipped when culling is disabled or timeout ≤ 60, which code-server rejects), so lingering code-server processes exit on the culler's schedule after the last connection drops.Escape hatch:
vscodeActivity.enabled: falsereverts to counting raw proxied traffic. The polarity is fail-safe: an image running without the new chart plumbing defaults to the old behavior (over-spending) rather than culling active users who have no reporter installed.Testing
tests/e2e/test_vscode_idle_culling.py): proxied/vscode/traffic does not advancelast_activity; a contents-API ping does; the extension is installed; the idle-timeout env matchescull.timeout.values.yaml, so once CI publishes this branch's image I'll runscripts/bump_image_tags.pyso the two image-dependent e2e tests exercise the new image.shutdownNoActivityTimeout; active typing does not).