Skip to content

Commit 709bedb

Browse files
committed
refactor(managed_client): dedupe run/result polling, group constants
Extract a single typed _poll() helper shared by the query-run and result-ready waits (removing two near-identical loops and the magic 0.5/0.3 sleep intervals), flatten _query_database_scoped via _await_query_run, and hoist the class constants to the top. No behavior change; verified by the unit suite and a full prod e2e re-run.
1 parent db0ff57 commit 709bedb

1 file changed

Lines changed: 52 additions & 31 deletions

File tree

hotdata_framework/managed_client.py

Lines changed: 52 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
import time
1111
from collections.abc import Callable
12-
from typing import Any, TypeVar
12+
from typing import Any, Protocol, TypeVar
1313

1414
import pyarrow as pa
1515
from hotdata.api.query_api import QueryApi
@@ -30,6 +30,16 @@
3030
T = TypeVar("T")
3131

3232

33+
class _StatusResponse(Protocol):
34+
"""Async resources (query runs, results) expose a status and error message."""
35+
36+
status: str
37+
error_message: str | None
38+
39+
40+
S = TypeVar("S", bound=_StatusResponse)
41+
42+
3343
class ManagedDatabaseClient:
3444
"""Managed-database client with bounded retries over hotdata-framework.
3545
@@ -39,6 +49,10 @@ class ManagedDatabaseClient:
3949
database lifecycle.
4050
"""
4151

52+
_QUERY_TIMEOUT_SECONDS = 300.0
53+
_POLL_INTERVAL_SECONDS = 0.4
54+
_MAX_BACKOFF_SECONDS = 30.0
55+
4256
def __init__(
4357
self,
4458
*,
@@ -100,7 +114,27 @@ def operation() -> pa.Table | None:
100114

101115
return self._request_with_retry(operation)
102116

103-
_QUERY_TIMEOUT_SECONDS = 300.0
117+
def _poll(
118+
self,
119+
fetch: Callable[[], S],
120+
*,
121+
is_ready: Callable[[S], bool],
122+
describe: str,
123+
) -> S:
124+
"""Poll ``fetch`` until ``is_ready`` is satisfied, or raise on failure/timeout.
125+
126+
``failed``/``cancelled`` statuses raise ``RuntimeError``; exceeding
127+
:attr:`_QUERY_TIMEOUT_SECONDS` raises ``TimeoutError``.
128+
"""
129+
deadline = time.monotonic() + self._QUERY_TIMEOUT_SECONDS
130+
while time.monotonic() < deadline:
131+
obj = fetch()
132+
if obj.status in ("failed", "cancelled"):
133+
raise RuntimeError(obj.error_message or f"{describe} {obj.status}")
134+
if is_ready(obj):
135+
return obj
136+
time.sleep(self._POLL_INTERVAL_SECONDS)
137+
raise TimeoutError(f"{describe} timed out after {self._QUERY_TIMEOUT_SECONDS}s")
104138

105139
def _query_database_scoped(self, sql: str, *, database_id: str) -> str | None:
106140
raw = QueryApi(self._runtime.api).query(
@@ -113,40 +147,29 @@ def _query_database_scoped(self, sql: str, *, database_id: str) -> str | None:
113147
# inline preview returns, so wait for ``ready`` before the caller
114148
# fetches it as Arrow.
115149
return self._wait_result_ready(raw.result_id)
116-
117150
if isinstance(raw, AsyncQueryResponse):
118-
runs = QueryRunsApi(self._runtime.api)
119-
deadline = time.monotonic() + self._QUERY_TIMEOUT_SECONDS
120-
result_id: str | None = None
121-
while time.monotonic() < deadline:
122-
run = runs.get_query_run(raw.query_run_id)
123-
if run.status == "succeeded":
124-
result_id = run.result_id
125-
break
126-
if run.status in ("failed", "cancelled"):
127-
raise RuntimeError(run.error_message or f"Query {run.status}")
128-
time.sleep(0.5)
129-
else:
130-
raise TimeoutError(
131-
f"Managed database query timed out after {self._QUERY_TIMEOUT_SECONDS}s"
132-
)
133-
return self._wait_result_ready(result_id)
134-
151+
return self._wait_result_ready(self._await_query_run(raw.query_run_id))
135152
return None
136153

154+
def _await_query_run(self, query_run_id: str) -> str | None:
155+
runs = QueryRunsApi(self._runtime.api)
156+
run = self._poll(
157+
lambda: runs.get_query_run(query_run_id),
158+
is_ready=lambda r: r.status == "succeeded",
159+
describe="Query",
160+
)
161+
return run.result_id
162+
137163
def _wait_result_ready(self, result_id: str | None) -> str | None:
138164
if result_id is None:
139165
return None
140166
results = ResultsApi(self._runtime.api)
141-
deadline = time.monotonic() + self._QUERY_TIMEOUT_SECONDS
142-
while time.monotonic() < deadline:
143-
r = results.get_result(result_id)
144-
if r.status == "ready":
145-
return result_id
146-
if r.status in ("failed", "cancelled"):
147-
raise RuntimeError(r.error_message or f"Result {r.status}")
148-
time.sleep(0.3)
149-
raise TimeoutError(f"Result {result_id} not ready after {self._QUERY_TIMEOUT_SECONDS}s")
167+
self._poll(
168+
lambda: results.get_result(result_id),
169+
is_ready=lambda r: r.status == "ready",
170+
describe=f"Result {result_id}",
171+
)
172+
return result_id
150173

151174
def fetch_table_rows(self, *, database: str, schema: str, table: str) -> list[dict[str, Any]]:
152175
result = self.fetch_table(database=database, schema=schema, table=table)
@@ -172,8 +195,6 @@ def load_managed_table(
172195
)
173196
)
174197

175-
_MAX_BACKOFF_SECONDS = 30.0
176-
177198
def _request_with_retry(self, operation: Callable[[], T]) -> T:
178199
for attempt in range(1, self._max_retries + 1):
179200
try:

0 commit comments

Comments
 (0)