-
Notifications
You must be signed in to change notification settings - Fork 145
Fix kernel async statement telemetry handle #923
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6629d87
c90439d
9d19911
8d31abc
a064df3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,7 +25,7 @@ | |
| import logging | ||
| import threading | ||
| import uuid | ||
| from typing import Any, Dict, List, Optional, TYPE_CHECKING, Union | ||
| from typing import Any, Dict, List, Optional, Set, TYPE_CHECKING, Union | ||
|
|
||
| from databricks.sql.backend.databricks_client import DatabricksClient | ||
| from databricks.sql.backend.kernel._errors import ( | ||
|
|
@@ -251,16 +251,19 @@ def __init__( | |
| # concurrent cursors on the same connection don't race on submit / | ||
| # close / close-session. | ||
| # | ||
| # This is a KEEP-ALIVE registry, not a state/result lookup: the | ||
| # This is primarily a KEEP-ALIVE registry: the | ||
| # submitting ``ExecutedAsyncStatement``'s ``Drop`` fires a | ||
| # fire-and-forget ``close_statement``, which would kill the | ||
| # still-running async query the moment the handle is dropped. We | ||
| # retain it (and its parent ``Statement``) here so the live query | ||
| # survives until an explicit close. ``get_query_state`` / | ||
| # ``get_execution_result`` do NOT consult this map — they | ||
| # re-attach to the statement by id (the server is the source of | ||
| # truth for async state), so they work even cross-process. | ||
| # survives until an explicit close. ``get_query_state`` and | ||
| # ``get_execution_result`` use this owning handle before result | ||
| # streaming starts so kernel async statement telemetry is | ||
| # finalized on the original ``ExecuteStatementAsync`` telemetry | ||
| # object, then fall back to attach-by-id for re-fetch / | ||
| # cross-process cases. | ||
| self._async_handles: Dict[str, Any] = {} | ||
| self._async_result_stream_started: Set[str] = set() | ||
| # Parent ``Statement`` objects kept alive alongside async handles. | ||
| # On the kernel, ``Statement.close()`` flips the validity flag on | ||
| # the produced executed handle (see kernel | ||
|
|
@@ -403,6 +406,7 @@ def close_session(self, session_id: SessionId) -> None: | |
| tracked_stmts = list(self._async_statements.items()) | ||
| self._async_handles.clear() | ||
| self._async_statements.clear() | ||
| self._async_result_stream_started.clear() | ||
| for _, handle in tracked: | ||
| # Per-handle close errors are non-fatal — PEP 249 | ||
| # discourages raising from session close — so log and | ||
|
|
@@ -654,6 +658,7 @@ def close_command(self, command_id: CommandId) -> None: | |
| with self._async_handles_lock: | ||
| handle = self._async_handles.pop(command_id.guid, None) | ||
| stmt = self._async_statements.pop(command_id.guid, None) | ||
| self._async_result_stream_started.discard(command_id.guid) | ||
| # Closing the handle below fires the server-side CloseStatement. | ||
| # A subsequent ``get_query_state`` re-attaches by id and reads | ||
| # ``CLOSED`` straight from the server — no connector-side | ||
|
|
@@ -683,18 +688,28 @@ def close_command(self, command_id: CommandId) -> None: | |
| pass | ||
|
|
||
| def get_query_state(self, command_id: CommandId) -> CommandState: | ||
| # Server is the source of truth for async command state. Re-attach | ||
| # to the statement by its id and read the state the server reports | ||
| # — no connector-side state to drift. SEA keys GetStatementStatus | ||
| # purely on the id, so a statement the connector no longer holds a | ||
| # handle for (or never held — a different process) is still | ||
| # queryable. CLOSED comes straight from the server: after a | ||
| # Server is the source of truth for async command state. Use the | ||
| # retained owning handle before result streaming starts so kernel | ||
| # async statement telemetry is finalized on the original | ||
| # ExecuteStatementAsync telemetry object. Once result streaming | ||
| # has been claimed (or when this connector never held the handle | ||
| # — cross-process / fresh-cursor cases), re-attach to the | ||
| # statement by id. SEA keys GetStatementStatus purely on the id, | ||
| # so a statement the connector no longer holds a handle for is | ||
| # still queryable. CLOSED comes straight from the server: after a | ||
| # statement is closed (DELETE) the server still returns 200 | ||
| # state=CLOSED until the result TTL elapses. | ||
| if self._kernel_session is None: | ||
| raise InterfaceError("get_query_state requires an open session.") | ||
| with self._async_handles_lock: | ||
| handle = ( | ||
| None | ||
| if command_id.guid in self._async_result_stream_started | ||
| else self._async_handles.get(command_id.guid) | ||
| ) | ||
| try: | ||
| handle = self._kernel_session.attach_async_statement(command_id.guid) | ||
| if handle is None: | ||
| handle = self._kernel_session.attach_async_statement(command_id.guid) | ||
| state, failure = handle.status() | ||
| except Exception as exc: | ||
| if _is_not_found(exc): | ||
|
|
@@ -740,25 +755,39 @@ def get_execution_result( | |
| command_id: CommandId, | ||
| cursor: "Cursor", | ||
| ) -> "ResultSet": | ||
| # Re-attach to the statement by id and await its result. SEA keys | ||
| # GetStatementResult on the id, so this works whether or not the | ||
| # connector still holds the submitting handle — and it's | ||
| # inherently re-callable (each call attaches a fresh handle and | ||
| # re-materialises the result stream), matching the Thrift backend | ||
| # where the operation handle stays re-fetchable until an explicit | ||
| # close. No connector-side handle lookup, so no | ||
| # ``unknown command_id`` failure on a second call. | ||
| # Prefer the original owning async handle for the first | ||
| # in-process result stream. The kernel attaches the real | ||
| # ExecuteStatementAsync telemetry to that handle; attached | ||
| # handles intentionally use no-op telemetry, so always | ||
| # re-attaching loses the SEA async statement row when the result | ||
| # is drained. After the owning result stream has been started, | ||
| # attach by id for re-fetch. This preserves the Thrift-parity | ||
| # behavior where results remain re-callable until explicit close. | ||
| # | ||
| # ``attach_async_statement`` issues a GetStatementStatus to seed | ||
| # the handle; a 404 (unknown / aged-out id) surfaces as a | ||
| # NotFound KernelError mapped to ``ProgrammingError`` below via | ||
| # ``_wrap_kernel_exception``. | ||
| # If this process does not hold the owning handle (fresh cursor, | ||
| # restarted process, already re-fetched), ``attach_async_statement`` | ||
| # issues a GetStatementStatus to seed the handle; a 404 (unknown | ||
| # / aged-out id) surfaces as a NotFound KernelError mapped to | ||
| # ``ProgrammingError`` below via ``_wrap_kernel_exception``. | ||
| if self._kernel_session is None: | ||
| raise InterfaceError("get_execution_result requires an open session.") | ||
| with self._async_handles_lock: | ||
| handle = ( | ||
| None | ||
| if command_id.guid in self._async_result_stream_started | ||
| else self._async_handles.get(command_id.guid) | ||
| ) | ||
| uses_owning_handle = handle is not None | ||
| if uses_owning_handle: | ||
| self._async_result_stream_started.add(command_id.guid) | ||
| try: | ||
| handle = self._kernel_session.attach_async_statement(command_id.guid) | ||
| if handle is None: | ||
| handle = self._kernel_session.attach_async_statement(command_id.guid) | ||
| stream = handle.await_result() | ||
| except Exception as exc: | ||
| if uses_owning_handle: | ||
| with self._async_handles_lock: | ||
| self._async_result_stream_started.discard(command_id.guid) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 Low — The retry-preservation logic is asymmetric. This is narrow (only when (Anchored to the nearest changed line — see the description for the exact location.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 Low — Asymmetric cleanup of the In However, if This is a narrow edge (arrow_schema raising after await succeeds) and telemetry is best-effort, hence Low. If the intent is that a post-await construction failure should still allow a telemetry-preserving retry, discard the marker in the 796-797 handler too (symmetric with the await handler). (Anchored to the nearest changed line — see the description for the exact location.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium — The result-set construction failure path leaves In
Per the method's own comment, kernel async telemetry is finalized on the owning handle only when the result stream is drained (during later fetch), not at Consider discarding the marker on this path too (mirroring L788–790), e.g. wrap the construction failure with the same (Anchored to the nearest changed line — see the description for the exact location.) |
||
| raise _wrap_kernel_exception("get_execution_result", exc) from exc | ||
| # ``KernelResultSet.__init__`` calls ``arrow_schema()`` which | ||
| # can raise — map that to PEP 249 too. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔵 Low — The owning-handle failure path only discards
_async_result_stream_startedwhenawait_result()raises. Ifawait_result()succeeds (marker stays set) but the subsequentKernelResultSet.__init__→arrow_schema()raises and is re-wrapped, the guid remains marked as started. A later retry then takes the attach-by-id (no-op telemetry) branch.Whether this loses the
ExecuteStatementAsynctelemetry row depends on when the kernel finalizes it: if finalization happens whenawait_result()returns, this is harmless (telemetry already committed). If finalization only completes once the result stream is drained, the telemetry is lost on this retry because the owning handle is never reused. The PR's own comments ("first in-process result stream", "clear the claimed marker so a retry can still use the telemetry-bearing owning handle") are ambiguous on this point, and the addedtest_get_execution_result_owning_handle_failure_can_retry_owning_handleonly exercises theawait_result()-raises case, not the construct-failure-after-await case. Worth confirming the finalization semantics and, if drain-based, discarding the marker on the construction-failure path too.