|
25 | 25 | import logging |
26 | 26 | import threading |
27 | 27 | import uuid |
28 | | -from typing import Any, Dict, List, Optional, TYPE_CHECKING, Union |
| 28 | +from typing import Any, Dict, List, Optional, Set, TYPE_CHECKING, Union |
29 | 29 |
|
30 | 30 | from databricks.sql.backend.databricks_client import DatabricksClient |
31 | 31 | from databricks.sql.backend.kernel._errors import ( |
@@ -251,16 +251,20 @@ def __init__( |
251 | 251 | # concurrent cursors on the same connection don't race on submit / |
252 | 252 | # close / close-session. |
253 | 253 | # |
254 | | - # This is a KEEP-ALIVE registry, not a state/result lookup: the |
| 254 | + # This is primarily a KEEP-ALIVE registry: the |
255 | 255 | # submitting ``ExecutedAsyncStatement``'s ``Drop`` fires a |
256 | 256 | # fire-and-forget ``close_statement``, which would kill the |
257 | 257 | # still-running async query the moment the handle is dropped. We |
258 | 258 | # retain it (and its parent ``Statement``) here so the live query |
259 | | - # survives until an explicit close. ``get_query_state`` / |
260 | | - # ``get_execution_result`` do NOT consult this map — they |
261 | | - # re-attach to the statement by id (the server is the source of |
262 | | - # truth for async state), so they work even cross-process. |
| 259 | + # survives until an explicit close. ``get_query_state`` still |
| 260 | + # re-attaches to the statement by id (the server is the source |
| 261 | + # of truth for async state). ``get_execution_result`` uses this |
| 262 | + # owning handle for the first in-process result stream so kernel |
| 263 | + # async statement telemetry is finalized on the original |
| 264 | + # ``ExecuteStatementAsync`` telemetry object, then falls back to |
| 265 | + # attach-by-id for re-fetch / cross-process cases. |
263 | 266 | self._async_handles: Dict[str, Any] = {} |
| 267 | + self._async_result_stream_started: Set[str] = set() |
264 | 268 | # Parent ``Statement`` objects kept alive alongside async handles. |
265 | 269 | # On the kernel, ``Statement.close()`` flips the validity flag on |
266 | 270 | # the produced executed handle (see kernel |
@@ -403,6 +407,7 @@ def close_session(self, session_id: SessionId) -> None: |
403 | 407 | tracked_stmts = list(self._async_statements.items()) |
404 | 408 | self._async_handles.clear() |
405 | 409 | self._async_statements.clear() |
| 410 | + self._async_result_stream_started.clear() |
406 | 411 | for _, handle in tracked: |
407 | 412 | # Per-handle close errors are non-fatal — PEP 249 |
408 | 413 | # discourages raising from session close — so log and |
@@ -654,6 +659,7 @@ def close_command(self, command_id: CommandId) -> None: |
654 | 659 | with self._async_handles_lock: |
655 | 660 | handle = self._async_handles.pop(command_id.guid, None) |
656 | 661 | stmt = self._async_statements.pop(command_id.guid, None) |
| 662 | + self._async_result_stream_started.discard(command_id.guid) |
657 | 663 | # Closing the handle below fires the server-side CloseStatement. |
658 | 664 | # A subsequent ``get_query_state`` re-attaches by id and reads |
659 | 665 | # ``CLOSED`` straight from the server — no connector-side |
@@ -740,25 +746,39 @@ def get_execution_result( |
740 | 746 | command_id: CommandId, |
741 | 747 | cursor: "Cursor", |
742 | 748 | ) -> "ResultSet": |
743 | | - # Re-attach to the statement by id and await its result. SEA keys |
744 | | - # GetStatementResult on the id, so this works whether or not the |
745 | | - # connector still holds the submitting handle — and it's |
746 | | - # inherently re-callable (each call attaches a fresh handle and |
747 | | - # re-materialises the result stream), matching the Thrift backend |
748 | | - # where the operation handle stays re-fetchable until an explicit |
749 | | - # close. No connector-side handle lookup, so no |
750 | | - # ``unknown command_id`` failure on a second call. |
| 749 | + # Prefer the original owning async handle for the first |
| 750 | + # in-process result stream. The kernel attaches the real |
| 751 | + # ExecuteStatementAsync telemetry to that handle; attached |
| 752 | + # handles intentionally use no-op telemetry, so always |
| 753 | + # re-attaching loses the SEA async statement row when the result |
| 754 | + # is drained. After the owning result stream has been started, |
| 755 | + # attach by id for re-fetch. This preserves the Thrift-parity |
| 756 | + # behavior where results remain re-callable until explicit close. |
751 | 757 | # |
752 | | - # ``attach_async_statement`` issues a GetStatementStatus to seed |
753 | | - # the handle; a 404 (unknown / aged-out id) surfaces as a |
754 | | - # NotFound KernelError mapped to ``ProgrammingError`` below via |
755 | | - # ``_wrap_kernel_exception``. |
| 758 | + # If this process does not hold the owning handle (fresh cursor, |
| 759 | + # restarted process, already re-fetched), ``attach_async_statement`` |
| 760 | + # issues a GetStatementStatus to seed the handle; a 404 (unknown |
| 761 | + # / aged-out id) surfaces as a NotFound KernelError mapped to |
| 762 | + # ``ProgrammingError`` below via ``_wrap_kernel_exception``. |
756 | 763 | if self._kernel_session is None: |
757 | 764 | raise InterfaceError("get_execution_result requires an open session.") |
| 765 | + with self._async_handles_lock: |
| 766 | + handle = ( |
| 767 | + None |
| 768 | + if command_id.guid in self._async_result_stream_started |
| 769 | + else self._async_handles.get(command_id.guid) |
| 770 | + ) |
| 771 | + uses_owning_handle = handle is not None |
| 772 | + if uses_owning_handle: |
| 773 | + self._async_result_stream_started.add(command_id.guid) |
758 | 774 | try: |
759 | | - handle = self._kernel_session.attach_async_statement(command_id.guid) |
| 775 | + if handle is None: |
| 776 | + handle = self._kernel_session.attach_async_statement(command_id.guid) |
760 | 777 | stream = handle.await_result() |
761 | 778 | except Exception as exc: |
| 779 | + if uses_owning_handle: |
| 780 | + with self._async_handles_lock: |
| 781 | + self._async_result_stream_started.discard(command_id.guid) |
762 | 782 | raise _wrap_kernel_exception("get_execution_result", exc) from exc |
763 | 783 | # ``KernelResultSet.__init__`` calls ``arrow_schema()`` which |
764 | 784 | # can raise — map that to PEP 249 too. |
|
0 commit comments