Skip to content

Commit 9d19911

Browse files
committed
Use owning async handle for kernel query state
1 parent c90439d commit 9d19911

2 files changed

Lines changed: 62 additions & 14 deletions

File tree

src/databricks/sql/backend/kernel/client.py

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -256,13 +256,12 @@ def __init__(
256256
# fire-and-forget ``close_statement``, which would kill the
257257
# still-running async query the moment the handle is dropped. We
258258
# retain it (and its parent ``Statement``) here so the live query
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.
259+
# survives until an explicit close. ``get_query_state`` and
260+
# ``get_execution_result`` use this owning handle before result
261+
# streaming starts so kernel async statement telemetry is
262+
# finalized on the original ``ExecuteStatementAsync`` telemetry
263+
# object, then fall back to attach-by-id for re-fetch /
264+
# cross-process cases.
266265
self._async_handles: Dict[str, Any] = {}
267266
self._async_result_stream_started: Set[str] = set()
268267
# Parent ``Statement`` objects kept alive alongside async handles.
@@ -689,18 +688,28 @@ def close_command(self, command_id: CommandId) -> None:
689688
pass
690689

691690
def get_query_state(self, command_id: CommandId) -> CommandState:
692-
# Server is the source of truth for async command state. Re-attach
693-
# to the statement by its id and read the state the server reports
694-
# — no connector-side state to drift. SEA keys GetStatementStatus
695-
# purely on the id, so a statement the connector no longer holds a
696-
# handle for (or never held — a different process) is still
697-
# queryable. CLOSED comes straight from the server: after a
691+
# Server is the source of truth for async command state. Use the
692+
# retained owning handle before result streaming starts so kernel
693+
# async statement telemetry is finalized on the original
694+
# ExecuteStatementAsync telemetry object. Once result streaming
695+
# has been claimed (or when this connector never held the handle
696+
# — cross-process / fresh-cursor cases), re-attach to the
697+
# statement by id. SEA keys GetStatementStatus purely on the id,
698+
# so a statement the connector no longer holds a handle for is
699+
# still queryable. CLOSED comes straight from the server: after a
698700
# statement is closed (DELETE) the server still returns 200
699701
# state=CLOSED until the result TTL elapses.
700702
if self._kernel_session is None:
701703
raise InterfaceError("get_query_state requires an open session.")
704+
with self._async_handles_lock:
705+
handle = (
706+
None
707+
if command_id.guid in self._async_result_stream_started
708+
else self._async_handles.get(command_id.guid)
709+
)
702710
try:
703-
handle = self._kernel_session.attach_async_statement(command_id.guid)
711+
if handle is None:
712+
handle = self._kernel_session.attach_async_statement(command_id.guid)
704713
state, failure = handle.status()
705714
except Exception as exc:
706715
if _is_not_found(exc):

tests/unit/test_kernel_client.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,45 @@ def test_get_query_state_propagates_non_not_found_error():
799799
c.get_query_state(cid)
800800

801801

802+
def test_get_query_state_uses_retained_owning_handle_before_result_stream():
803+
"""In-process status polling uses the retained submitting handle so
804+
kernel async statement telemetry stays attached to the original
805+
ExecuteStatementAsync telemetry object."""
806+
c = _make_client()
807+
c._kernel_session = MagicMock()
808+
handle = MagicMock()
809+
handle.status.return_value = ("Running", None)
810+
cid = CommandId.from_sea_statement_id("async-status-owning")
811+
c._async_handles[cid.guid] = handle
812+
813+
assert c.get_query_state(cid) == CommandState.RUNNING
814+
815+
c._kernel_session.attach_async_statement.assert_not_called()
816+
handle.status.assert_called_once_with()
817+
818+
819+
def test_get_query_state_attaches_by_id_after_result_stream_started():
820+
"""Once get_execution_result has claimed the owning handle for result
821+
streaming, status polling falls back to attach-by-id."""
822+
c = _make_client()
823+
c._kernel_session = MagicMock()
824+
owning_handle = MagicMock()
825+
attached_handle = MagicMock()
826+
attached_handle.status.return_value = ("Succeeded", None)
827+
c._kernel_session.attach_async_statement.return_value = attached_handle
828+
cid = CommandId.from_sea_statement_id("async-status-attached")
829+
c._async_handles[cid.guid] = owning_handle
830+
c._async_result_stream_started.add(cid.guid)
831+
832+
assert c.get_query_state(cid) == CommandState.SUCCEEDED
833+
834+
owning_handle.status.assert_not_called()
835+
c._kernel_session.attach_async_statement.assert_called_once_with(
836+
"async-status-attached"
837+
)
838+
attached_handle.status.assert_called_once_with()
839+
840+
802841
def test_get_execution_result_uses_retained_owning_handle_first():
803842
"""The first in-process result fetch uses the retained submitting
804843
handle so the kernel finalizes the original async statement telemetry."""

0 commit comments

Comments
 (0)