Skip to content

feat: parallel setup, WebSocket I/O, per-session locks - #48

Merged
datasciencemonkey merged 4 commits into
mainfrom
feat/performance-improvements-v2
Mar 10, 2026
Merged

datasciencemonkey merged 4 commits into
mainfrom
feat/performance-improvements-v2

Conversation

@datasciencemonkey

@datasciencemonkey datasciencemonkey commented Mar 9, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Applies PR feat: Parallel setup, WebSocket I/O, per-session locks #47 from David O'Keeffe with bug fixes, hardening, and WS↔HTTP parity
  • Parallel setup via ThreadPoolExecutor (~5x faster startup)
  • Per-session locks replacing global lock for WS/output ops (reduced contention)
  • WebSocket terminal I/O via Flask-SocketIO (eliminates 50-200ms polling latency, seamless fallback to HTTP polling)

Fixes applied on top of PR #47

Critical bugs (3)

Fix Problem Solution
Session timeout read_pty_output() never updated last_poll_time during WS output — sessions reaped while user watches output Update last_poll_time on every PTY read under per-session lock
panes → getAllPanes() All WS event handlers referenced undefined panes variable — forEach silently did nothing, WS rooms never joined, polling never stopped Replace with getAllPanes() (tabs.flatMap(t => t.panes))
Thread pool exhaustion 8 threads insufficient when WS connections hold threads Increase to 16 threads

WS↔HTTP parity gaps (3)

Fix Problem Solution
Idle session keepalive HTTP poll-worker sends heartbeat every 30s; with polling stopped, idle WS sessions got reaped after 5 min Client sends batched heartbeat event every 30s; server handler refreshes last_poll_time
SIGTERM notification HTTP poll detects shutting_down flag on next poll; WS clients just saw a disconnect handle_sigterm now broadcasts shutting_down event to all WS clients
Background tab throttling Browsers throttle setInterval to 1min+ in background tabs; WS heartbeat could miss 5-min window Immediate heartbeat on visibilitychange resets timeout clock

Security & deployment (2)

Fix Problem Solution
CDN dependency cdn.socket.io may be blocked on Databricks Apps Vendored to /static/lib/socket.io.min.js
CORS cors_allowed_origins='*' allowed cross-origin WS Changed to cors_allowed_origins=[] (same-origin only)

Reliability (2)

Fix Problem Solution
Gunicorn timeout 120s too aggressive for single-worker app Reduced to 60s (balances WS + hung-worker detection)
Duplicate output WS→HTTP transport switch replayed buffered output Clear output_buffer on join_session

WS↔HTTP Feature Parity Audit (12/12 pass)

Feature WS Path HTTP Path Status
Output delivery terminal_output event → pane by session_id /api/output-batch → distribute by session_id ✅
Session exit session_exited event → cleanupPane() data.exited flag → session_ended msg ✅
Input delivery terminal_input event → os.write(fd) /api/input POST → os.write(fd) ✅
Terminal resize terminal_resize event → ioctl TIOCSWINSZ /api/resize POST → ioctl TIOCSWINSZ ✅
Session keepalive 30s heartbeat event (batched) Poll/heartbeat updates last_poll_time ✅
Background tabs Immediate heartbeat on visibilitychange Web Worker 30s heartbeat (not throttled) ✅
SIGTERM shutdown shutting_down broadcast event shutting_down flag in poll response ✅
Auth (403) _check_ws_authorization() on connect 403 response → stop polling ✅
Reconnection disconnect → restart poll-worker connect → join rooms + stop polling ✅
New pane join_session if wsConnected start_poll if not wsConnected ✅
Pane close leave_session + stop_poll + sendBeacon stop_poll + sendBeacon ✅
Multi-pane routing Room-based (join_room/room=session_id) session_id-keyed output dict ✅

Commits

  1. f9e20dd — Apply PR feat: Parallel setup, WebSocket I/O, per-session locks #47 + 6 original fixes
  2. eed8076 — Fix panes → getAllPanes() in all WS handlers
  3. a5fdf4b — Add WS heartbeat, SIGTERM broadcast, background tab safety

Supersedes

PR #47 — incorporates all changes with conflict resolution and 10 fixes.

Test plan

  • Socket.IO polling endpoint returns valid session
  • Vendored socket.io.min.js serves correctly
  • App starts and serves index page
  • Browser: WS connects, [ws] Connected in console, no /api/output-batch spam
  • Browser: Terminal works with real-time output (no polling lag)
  • Browser: WS disconnect → poll-worker takes over → WS reconnect → polling stops
  • Browser: Idle terminal stays alive >5 min (WS heartbeat working)
  • Browser: Background tab for 2+ min → return → session still alive
  • Deploy to Databricks Apps end-to-end

🤖 Generated with Claude Code

Apply PR #47 from David O'Keeffe with the following enhancements:
- Parallel setup via ThreadPoolExecutor (~5x faster startup)
- Per-session locks (reduced contention vs global lock)
- WebSocket terminal I/O via Flask-SocketIO (eliminates 50-200ms polling latency)

Fixes applied on top of PR #47:
- Fix session timeout: update last_poll_time on WS output emit
- Vendor Socket.IO client to /static/lib/ (avoid CDN dependency)
- Restrict CORS: cors_allowed_origins=[] (same-origin only)
- Increase thread pool: 8→16 for WS connection headroom
- Adjust gunicorn timeout: 120s→60s (balanced for WS + hung-worker detection)
- Clear output buffer on WS join (prevent duplicate output on transport switch)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@datasciencemonkey datasciencemonkey self-assigned this Mar 9, 2026
datasciencemonkey and others added 2 commits March 9, 2026 19:12
…lers

The WS connect/disconnect/error handlers and terminal_output/session_exited
event handlers referenced `panes` (undefined) instead of `getAllPanes()`.
This caused WS rooms to never be joined and poll-worker to never stop,
resulting in both WS and HTTP polling running simultaneously.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three gaps found during WS↔HTTP parity audit:

1. Idle session reaping: With HTTP polling stopped, nothing updated
   last_poll_time for idle WS sessions. Added client-side 30s heartbeat
   (batched session_ids) and server-side handler to refresh timestamps.

2. SIGTERM notification: HTTP poll detects shutting_down flag on next
   poll. WS clients had no equivalent — they'd just see a disconnect.
   Added socketio.emit('shutting_down') in SIGTERM handler for immediate
   notification before the connection drops.

3. Background tab throttling: Browsers throttle setInterval in background
   tabs (1min+ delays). Added immediate WS heartbeat on visibilitychange
   to reset the 5-min session timeout clock when tab goes to/from background.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
sendBeacon with a raw string sends Content-Type: text/plain, which Flask
rejects with 415 Unsupported Media Type. The heartbeat sendBeacon already
uses Blob with application/json — apply the same pattern to session close.

Without this fix, closed sessions linger until the 5-min cleanup thread
reaps them instead of being terminated immediately.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@datasciencemonkey

Copy link
Copy Markdown
Owner Author
image fall back works as expected

@datasciencemonkey
datasciencemonkey merged commit 26576b3 into main Mar 10, 2026
datasciencemonkey added a commit that referenced this pull request Mar 10, 2026
Document the current state after PR #48: WebSocket I/O (primary) with
HTTP polling fallback, parallel agent setup, new API endpoints, Socket.IO
events, updated gunicorn config, and expanded project structure.
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.

1 participant