Skip to content

fix(session): notice, explain and replace a new session that fails to start - #228

Open
dborowitz wants to merge 4 commits into
dborowitz-hygienefrom
dborowitz-session-start
Open

dborowitz wants to merge 4 commits into
dborowitz-hygienefrom
dborowitz-session-start

Conversation

@dborowitz

Copy link
Copy Markdown
Contributor

Stacked on #217.

CI integration tests intermittently fail the same way on every OS: a new session takes ~10 minutes to create, the first RPC's WebSocket handshake times out, and the liveness guard raises a bare RuntimeError("Session not active. Please create a new session"), because Dataproc by then reports the session not ACTIVE. The library returned a broken session from getOrCreate() and threw away what Dataproc said about it.

This makes the library notice a new session that never came up, say why, and replace it when it can:

  1. Say why a session is not active. RPCs on a session Dataproc reports gone now raise SessionNotActiveError (a RuntimeError, as before, and a ManagedSparkConnectException) with what Dataproc said, e.g. Session not active: Managed Spark Session sc-123 is FAILED: <state_message>. Please create a new session.
  2. Check the CreateSession result before connecting, as reuse already does: a session already FAILED/TERMINATING/TERMINATED raises; one without an endpoint yet is polled. The endpoint poll also checked the endpoint before the state, so it could return a session that had failed after publishing its endpoint; fixed.
  3. Check that a new session works before getOrCreate() returns, by making its first RPC (the Spark version, which notebooks already fetched for the session-created message). If it fails or takes over 60s, Dataproc is asked; a session it reports gone raises SessionNotActiveError from getOrCreate() instead of from the caller's first query. Otherwise the session is returned as before. Recorded as a session.check_new span.
  4. Replace a new session that fails to start, for generated session IDs only, up to MANAGED_SPARK_CONNECT_SESSION_START_RETRIES times (default 1, 0 disables). Sessions created with sessionId() raise instead. Recorded as a session.start_failed event; logs a WARNING and shows a status line in notebooks.

Not covered: a session Dataproc keeps reporting ACTIVE while its endpoint never accepts a WebSocket connection (seen once in the live run below). The check fails open there by design; the caller's first query still hangs on handshake timeouts.

Testing

  • Unit tests: new tests/unit/test_session_start.py and guard tests; full suite passes on pyspark-client 4.0.3.
  • Live, against Dataproc (datagravity-demo, us-east4) with the internal resilience suite's new session_start.* scenarios, which kill or cut off the first new session in a subprocess:
    • dead_before_first_rpc(recreate): ok, replaced by a second session (2 sessions, one session.start_failed with retry=true, query ok).
    • dead_before_first_rpc(no_retries), (custom_id): SessionNotActiveError from getOrCreate(), ... is TERMINATING, no replacement.
    • unreachable(until_ready): ok, first session returned after the 60s check (Dataproc reported it ACTIVE).
    • terminated_mid_session(default), (liveness_ttl0): SessionNotActiveError with ... is TERMINATED / ... is TERMINATING.
    • Existing fault.guard_transient_503 (3 sizes), fault.ws_drop_while_idle, progress.driver_only on ws+guard and ws+noguard: unchanged (ok). The primary session's first-RPC check cost 1.7s of a 48s getOrCreate().

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces automatic retries for Managed Spark Sessions that fail to start during getOrCreate(). It adds a new SessionNotActiveError exception, which provides detailed error messages from Dataproc when a session fails or is deleted. A new configuration variable, MANAGED_SPARK_CONNECT_SESSION_START_RETRIES, allows users to specify the number of creation retries for generated session IDs. Additionally, the PR adds instrumentation for tracking session startup failures and includes comprehensive unit tests to verify the retry and error-handling behavior. I have no feedback to provide as there are no review comments.

An RPC on a session that Dataproc reports gone used to fail with a bare
RuntimeError("Session not active. Please create a new session"), dropping
the state the liveness check had just fetched. It now raises
SessionNotActiveError, which names the session and what Dataproc said:
"Managed Spark Session sc-123 is FAILED: <state_message>", "... is
TERMINATED", "... was deleted". The liveness cache keeps that detail with
a dead verdict, so RPCs that fast-fail on the cached verdict say the same.

SessionNotActiveError subclasses RuntimeError, as before, and
ManagedSparkConnectException, and its message keeps the old prefix and
suffix.
getOrCreate() connected to whatever session the CreateSession operation
returned, without looking at its state or its Spark Connect endpoint.
Reusing a session already checks both; a new one now does too: a session
that is already FAILED, TERMINATING or TERMINATED raises
SessionNotActiveError with what Dataproc reported, and one that is still
CREATING or has no endpoint yet is polled until it has one.

Polling for the endpoint also checked the endpoint before the state, so
it returned a session that had failed after publishing its endpoint. It
now checks for FAILED, TERMINATING (new) or TERMINATED first, and raises
SessionNotActiveError with the state message instead of a bare
ManagedSparkConnectException (which SessionNotActiveError subclasses).
…turns

getOrCreate() returned a new session without any RPC to it outside
notebooks, so a session that came up broken was only found out by the
caller's first query: in CI, an 11-minute CreateSession, then a WebSocket
handshake timeout and "Session not active" on the first createDataFrame().

getOrCreate() now makes the new session's first RPC itself: the Spark
version, which the session-created message already fetched in notebooks
and now gets from here. If it fails, or takes more than 60s (enough for
a 30s WebSocket handshake timeout plus the liveness guard), Dataproc is
asked about the session; one it reports gone is closed client-side and
getOrCreate() raises SessionNotActiveError saying why. Anything else,
including Dataproc being unreachable, returns the session as before.

The check is recorded as a session.check_new span (first_rpc_ok, dead,
timed_out). Tests that mock the Spark version now return one, since a
version is what tells getOrCreate() that the new session is usable.
When a session getOrCreate() just created turns out dead before it could
be used (Dataproc reports it failed or terminated in the CreateSession
result, while waiting for its endpoint, or after its first RPC failed),
getOrCreate() now creates another session in its place and returns that,
up to MANAGED_SPARK_CONNECT_SESSION_START_RETRIES times (default 1, 0 to
disable). Nothing has run on the dead session, so nothing is lost but
time; the dead session itself is left alone, since Dataproc already has
it failed or terminated.

Only sessions with a generated ID are replaced: a session created with
sessionId() belongs to the caller, so its failure still raises
SessionNotActiveError. A replacement logs a WARNING, shows a status line
in notebooks, and is recorded as a session.start_failed event (detail,
attempt, retry).
@dborowitz
dborowitz force-pushed the dborowitz-session-start branch from 0349168 to ae55192 Compare November 2, 2026 14:21
@dborowitz
dborowitz added this pull request to stack #220 November 2, 2026 14:24
@dborowitz
dborowitz requested a review from tim-u November 2, 2026 14:24
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