Skip to content

fix(py): scope server(data_source=) cleanup and table guards to live sessions - #308

Merged
cpsievert merged 3 commits into
mainfrom
fix/py-server-cleanup-and-description-snapshot
Sep 12, 2026
Merged

fix(py): scope server(data_source=) cleanup and table guards to live sessions#308
cpsievert merged 3 commits into
mainfrom
fix/py-server-cleanup-and-description-snapshot

Conversation

@cpsievert

@cpsievert cpsievert commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

The problem

Follow-up to the merged per-session registration stack (#301#304): server(data_source=) passes cleanup_replaced=False unconditionally. Before that stack, the call went through add_table(replace=True), which cleaned up the source it replaced. So if the object was constructed with a data source (or add_table() was called before any session) and this is the first .server() call, the pre-existing source/executor used to be cleaned up and now silently leaks — even though no earlier session can possibly be using it yet.

The fix

Track live sessions on the instance (_active_sessions, incremented when a session's server starts, decremented via session.on_ended()) and clean up the replaced source/executor exactly when no session is live at registration time (cleanup_replaced = _active_sessions == 0). A registration made while a session is running skips cleanup, since the replaced resource may still be in use by it.

Scoping shared-state decisions to live sessions (rather than to whether .server() has ever been called) also means:

  • a source registered by a session that has since ended is reclaimed when a later session replaces it;
  • add_table()/add_tables()/remove_table() are blocked only while a session is actually active, not forever after the first .server() call.

If on_ended never fires, the count stays elevated and behavior degrades to over-conservative (skipping cleanups), never premature cleanup.

What intentionally isn't changed

The other resource-lifecycle point from the Copilot review of the R port (#306) — the greeting snapshot omitting data_description — doesn't apply to Python: _data_description is fixed at construction and never mutated (nothing assigns to it after __init__), and inferred descriptions live on the DataSource objects themselves (get_data_description()), which the greeting snapshot already covers via its data_sources override. The R side needed the fix because its auto_fill_data_description() mutates shared state at registration time.

Test plan

  • New lifecycle tests (TestServerDataSourceSessionLifecycle): a replaced source is cleaned up once the session that registered it has ended; a replaced source survives while any session is live (overlap protection); add_table() is allowed once all sessions have ended.
  • Existing cleanup-safety tests (second concurrent session doesn't clean up the first's source/executor; public add_table(replace=True) still does; first-call cleanup of constructor-registered sources) all pass, locking in the concurrent-session behavior.
  • Full unit suite passes (942 tests; playwright/smoke excluded).

Part of #300.

Review follow-ups (Copilot)

  • Sources/executors replaced while sessions are live are retained (_retired_resources) and cleaned up when the last live session ends (or cleanup() runs) — closing the leaked-executor/connection path flagged in review.
  • QueryChat(None, table_name=...) now validates the deferred name at construction instead of failing later at .server() registration (mirrors the R-side finding on fix(r): let $server(data_source=) survive a second session safely #306).

cpsievert added a commit that referenced this pull request Sep 12, 2026
…ation

Replace the .server_initialized boolean with an .active_sessions counter
(incremented on (), decremented via session()):

- (data_source=) cleans up the source/executor it replaces whenever
  no session is live -- including a later session whose predecessor has
  fully ended, which the boolean treated as permanently uncleanable.
- ()/()/() are blocked only while a
  session is actually active, rather than forever after the first
  () call.

Fails safe: if onSessionEnded never fires, behavior degrades to the
previous over-conservative semantics, never premature cleanup.

Mirrors #308 (Python).
@cpsievert cpsievert changed the title fix(py): clean up the source replaced by a first server(data_source=) call fix(py): scope server(data_source=) cleanup and table guards to live sessions Sep 12, 2026
@cpsievert
cpsievert requested a lite review from Copilot September 12, 2026 16:56
…sessions

Track live sessions on the instance (_active_sessions, incremented when a
session's server starts, decremented via session.on_ended) and key
shared-state decisions off it:

- server(data_source=) cleans up the source/executor it replaces exactly
  when no session is live (cleanup_replaced = _active_sessions == 0),
  preserving add_table(replace=True) semantics for the first call without
  leaking resources registered by sessions that have since ended.
- add_table()/add_tables()/remove_table() are blocked only while a
  session is actually active, rather than forever after the first
  server() call.

Fails safe: a missed on_ended degrades to over-conservative cleanup
skipping, never premature cleanup.
@cpsievert
cpsievert force-pushed the fix/py-server-cleanup-and-description-snapshot branch from a097d23 to 701a5ee Compare September 12, 2026 16:57

This comment was marked as resolved.

Address Copilot review on #308:
- Retain sources/executors replaced while sessions are live and clean
  them up when the last live session ends (or cleanup() runs), fixing a
  leaked executor/connection when cleanup_replaced was False.
- Correct add_tables() docstring: error occurs while a server session
  is active, not after .server() has been invoked.
- Rename _untrack_session to untrack_session per nested-function naming
  convention in private modules.
QueryChat(None, table_name=...) stored the name unvalidated, so a bad
name was baked into the module id and only failed later, at .server()
registration time. Validate with the same rule used at registration.
cpsievert added a commit that referenced this pull request Sep 12, 2026
…deferred name validation

Address Copilot review on #306:
- Retain sources/executors replaced while sessions are live
  (.retired_resources) and clean them up when the last live session ends
  (or $cleanup() runs), fixing a leaked executor/connection when
  cleanup_replaced was FALSE. Mirrors the Python side (#308).
- greeter$build_client() distinguishes an omitted data_sources/
  data_description override (falls back to live state) from an explicit
  snapshot, which may itself be NULL -- a session whose snapshot had no
  inferred description no longer picks up a later session's.
- QueryChat$new(NULL, table_name = ...) validates the deferred name at
  construction instead of failing later at $server() registration.
@cpsievert
cpsievert requested a lite review from Copilot September 12, 2026 17:14

This comment was marked as resolved.

@cpsievert
cpsievert merged commit c9b3e4c into main Sep 12, 2026
8 checks passed
@cpsievert
cpsievert deleted the fix/py-server-cleanup-and-description-snapshot branch September 12, 2026 17:21
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.

2 participants