aminghadersohi commented on code in PR #45845:
URL: https://github.com/apache/superset/pull/45845#discussion_r4430505171


##########
superset/db_engine_specs/impala.py:
##########
@@ -136,30 +139,43 @@ def execute(
     @classmethod
     def fetch_data(cls, cursor: Any, limit: int | None = None) -> 
list[tuple[Any, ...]]:
         """Wait for asynchronous operations using the public cursor API."""
-        if callable(getattr(cursor, "execute_async", None)):
-            from impala.error import Error
-
-            deadline = time.monotonic() + app.config["SQLLAB_TIMEOUT"]
+        try:
+            if callable(getattr(cursor, "execute_async", None)):
+                from impala.error import Error
+
+                deadline = time.monotonic() + app.config["SQLLAB_TIMEOUT"]
+                try:
+                    while cursor.is_executing():
+                        if time.monotonic() >= deadline:
+                            cls._cancel_operation(cursor)
+                            raise TimeoutError(
+                                "Timed out waiting for the Impala operation"
+                            )
+                        time.sleep(0.1)
+                    if cursor.execution_failed():
+                        # Fetching surfaces the driver's detailed asynchronous 
error,
+                        # including errors on statements that produce no 
result set.
+                        cursor.fetchall()
+                except Error as ex:
+                    raise cls.get_dbapi_mapped_exception(ex) from ex
+            return super().fetch_data(cursor, limit)
+        except TIME_LIMIT_EXCEPTIONS:
+            # The operation is released before the time limit propagates so
+            # it does not keep running on the coordinator.
             try:
-                while cursor.is_executing():
-                    if time.monotonic() >= deadline:
-                        cursor.cancel_operation()
-                        raise TimeoutError("Timed out waiting for the Impala 
operation")
-                    time.sleep(0.1)
-                if cursor.execution_failed():
-                    # Fetching surfaces the driver's detailed asynchronous 
error,
-                    # including errors on statements that produce no result 
set.
-                    cursor.fetchall()
-            except Error as ex:
-                raise cls.get_dbapi_mapped_exception(ex) from ex
-        return super().fetch_data(cursor, limit)
+                cursor.cancel_operation()
+            except TIME_LIMIT_EXCEPTIONS:
+                raise
+            except Exception:  # pylint: disable=broad-except
+                logger.warning("cancel_operation() failed after a time limit")
+            raise

Review Comment:
   My two earlier replies on this thread were wrong. Their tests mocked 
`reconnect()` and only checked call order, so they missed the real problem: 
impyla's `HS2Service.reconnect()` only closes and reopens the socket or HTTP 
connection, while thrift's `TBufferedTransport` and thrift_sasl's 
`TSaslClientTransport` keep their private read and write buffers across 
`close()`/`open()`. The unread rest of the interrupted reply was still parsed 
as the CancelOperation reply.
   
   Standalone repro with the installed thrift 0.24.0 (real `TSocket` + 
`TBufferedTransport`, local server sends `OLD-REPLY-...` on the first 
connection and `NEW-REPLY` on the second; read 4 bytes, close, open, read 9):
   
   ```
   read before interrupt: b'OLD-'
   first read after reconnect: b'REPLY-OLD'
   ```
   
   Fix in ca8cbd0680a8a1d5a3748aa8759737cb34b0b5ee: 
`_cancel_operation_after_time_limit` calls a new `_reset_transport`, which 
walks the impyla transport stack (`TBufferedTransport` for NOSASL and HTTP, 
`TSaslClientTransport` for PLAIN/LDAP/GSSAPI, then `TSocket` or 
`ImpalaHttpClient`), checks every buffer attribute exists before changing 
anything, closes the transport, replaces every read and write buffer with an 
empty one, and reopens it. If the stack is not recognised or the reset raises, 
no cancel RPC is sent on it: a warning is logged and the time-limit exception 
still propagates. I chose this over a second connection because `fetch_data` 
only has the cursor, and over skipping the cancel because the operation would 
keep running on the coordinator.
   
   Regression tests, in the new 
`tests/unit_tests/db_engine_specs/test_impala_transport.py`. They use impyla's 
own `connect()` against a local HiveServer2 stand-in over real binary 
(`TBufferedTransport`), SASL PLAIN (`TSaslClientTransport`) and HTTP 
(`ImpalaHttpClient`) transports, raise the time limit right after a reply 
header is read, and assert exactly one CancelOperation for the right operation 
handle, a CloseOperation after it, a new connection for binary and SASL, and no 
cancel/close warning:
   - `test_handle_cursor_cancels_on_a_clean_transport_after_interrupted_reply` 
[binary, sasl, http] x [status, get_log] x [SoftTimeLimitExceeded, 
SupersetTimeoutException]
   - `test_fetch_data_cancels_on_a_clean_transport_after_interrupted_reply` 
[binary, sasl, http] x [status (is_executing poll), rows (FetchResults)] x 
[SoftTimeLimitExceeded, SupersetTimeoutException]
   - `test_reset_transport_refuses_an_unknown_transport`
   
   Against impala.py at 24860720185c06394e8eb4ccf5c0f4a30cea21b2 all 25 fail: 
on binary and SASL the CancelOperation read blocks until the test's 2 s socket 
timeout (`TTransportException: read timeout`) and impyla sends it again, and 
over HTTP `cancel_operation()` fails. With this commit they pass (25 passed in 
5.20s). Mock tests in `test_impala.py` now assert reset-before-cancel 
(`test_fetch_data_resets_transport_before_cancelling_an_interrupted_fetch`, 
`test_handle_cursor_resets_transport_before_cancelling_an_interrupted_poll`) 
and that no cancel RPC is sent when the reset fails 
(`test_time_limit_skips_cancel_rpcs_without_a_clean_transport`). test_impala, 
test_impala_pending, test_impala_transport, test_hive, test_presto: 273 passed. 
The docs paragraph in `configuring-superset.mdx` now says what is actually done.
   
   Siblings checked:
   - superset/db_engine_specs/impala.py:163 — fixed in 
ca8cbd0680a8a1d5a3748aa8759737cb34b0b5ee (`fetch_data` time-limit handler, 
status poll and row fetch)
   - superset/db_engine_specs/impala.py:350 — fixed in 
ca8cbd0680a8a1d5a3748aa8759737cb34b0b5ee (`handle_cursor` time-limit handler, 
status and log polls)
   - superset/db_engine_specs/impala.py:150 — not affected: the `fetch_data` 
deadline cancel runs after `is_executing()` returned, so no reply is half read
   - superset/db_engine_specs/impala.py:319 — not affected: the `handle_cursor` 
stop cancel runs after `cursor.status()` returned, so no reply is half read
   - superset/sql_lab.py:375 — not affected: the `SoftTimeLimitExceeded` 
handler sends no RPC, and closing the impyla connection afterwards only closes 
the transport (`HiveServer2Connection.close()`), while the cursor is already 
closed so its `__del__` sends nothing
   - superset/db_engine_specs/hive.py:465 — not affected: the `fetch_logs` 
time-limit branch re-raises without sending an RPC (pyhive's connection close 
is covered under Remaining)
   - superset/db_engine_specs/hive.py:459 — not affected: the stop cancel runs 
after `cursor.poll()` returned, not on a time-limit path
   - superset/db_engine_specs/presto.py:1437 — not affected: the stop cancel 
runs on an HTTP client with no shared Thrift buffers, not on a time-limit path
   
   Remaining: (1) not run against a real Impala. Untested there: whether the 
coordinator accepts CancelOperation for the same session on the reopened binary 
connection (impyla's own retries rely on this; if the coordinator closed the 
session when the old connection dropped, it cancels the query itself and the 
cancel RPC fails with a logged warning), and the GSSAPI/SASL-with-QOP case, 
which uses the same `TSaslClientTransport` buffers but was not exercised. (2) 
Hive, not changed in this PR and found by reading the code, not by a test: 
pyhive's `Connection.close()` sends CloseSession on the same transport when 
`get_raw_connection` exits, so a Hive time limit inside a reply (for example 
inside `cursor.poll()`, which is the same on master) can read the same stale 
bytes.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to