|
35 | 35 | ProgrammingError, |
36 | 36 | TransactionError, |
37 | 37 | DatabaseError, |
| 38 | + ReydenThriftUnsupportedError, |
| 39 | +) |
| 40 | +from databricks.sql.backend.reyden_warehouse_cache import ( |
| 41 | + extract_warehouse_id, |
| 42 | + is_known_reyden, |
| 43 | + mark_reyden, |
38 | 44 | ) |
39 | 45 |
|
40 | 46 | from databricks.sql.backend.databricks_client import DatabricksClient |
|
66 | 72 | from databricks.sql.session import Session |
67 | 73 | from databricks.sql.backend.types import CommandId, BackendType, CommandState, SessionId |
68 | 74 |
|
69 | | -from databricks.sql.auth.common import ClientContext |
| 75 | +from databricks.sql.auth.common import AuthType, ClientContext |
70 | 76 | from databricks.sql.common.unified_http_client import UnifiedHttpClient |
71 | 77 | from databricks.sql.common.http import HttpMethod |
72 | 78 |
|
@@ -399,24 +405,30 @@ def read(self) -> Optional[OAuthToken]: |
399 | 405 | self.http_client = UnifiedHttpClient(client_context) |
400 | 406 |
|
401 | 407 | try: |
402 | | - self.session = Session( |
| 408 | + self.session = self._open_session_with_reyden_fallback( |
403 | 409 | server_hostname, |
404 | 410 | http_path, |
405 | | - self.http_client, |
406 | 411 | http_headers, |
407 | 412 | session_configuration, |
408 | 413 | catalog, |
409 | 414 | schema, |
410 | 415 | _use_arrow_native_complex_types, |
411 | | - **kwargs, |
| 416 | + kwargs, |
412 | 417 | ) |
413 | | - self.session.open() |
414 | 418 | except Exception as e: |
415 | 419 | # Respect user's telemetry preference even during connection failure. |
416 | | - # For use_kernel connections the kernel owns telemetry, so suppress |
417 | | - # the wrapper-side failure log to avoid wrapper-vs-kernel duplication. |
418 | | - enable_telemetry = kwargs.get("enable_telemetry", True) and not kwargs.get( |
419 | | - "use_kernel", False |
| 420 | + # For a kernel connection the kernel owns telemetry, so suppress the |
| 421 | + # wrapper-side failure log to avoid wrapper-vs-kernel duplication. |
| 422 | + # Read the backend from the session that actually failed rather than |
| 423 | + # the caller's kwargs: on the Reyden auto-recovery path we retry on |
| 424 | + # the kernel via a kwargs copy, so the original kwargs still says |
| 425 | + # Thrift. If the kernel never got constructed (e.g. its wheel is |
| 426 | + # missing), self.session is the Thrift session and we still log. |
| 427 | + attempted_kernel = getattr( |
| 428 | + getattr(self, "session", None), "use_kernel", False |
| 429 | + ) |
| 430 | + enable_telemetry = ( |
| 431 | + kwargs.get("enable_telemetry", True) and not attempted_kernel |
420 | 432 | ) |
421 | 433 | TelemetryClientFactory.connection_failure_log( |
422 | 434 | error_name="Exception", |
@@ -512,6 +524,106 @@ def read(self) -> Optional[OAuthToken]: |
512 | 524 | session_id=self.get_session_id_hex(), |
513 | 525 | ) |
514 | 526 |
|
| 527 | + def _open_session_with_reyden_fallback( |
| 528 | + self, |
| 529 | + server_hostname: str, |
| 530 | + http_path: str, |
| 531 | + http_headers, |
| 532 | + session_configuration, |
| 533 | + catalog, |
| 534 | + schema, |
| 535 | + _use_arrow_native_complex_types, |
| 536 | + kwargs: dict, |
| 537 | + ) -> Session: |
| 538 | + """Open a ``Session``, transparently recovering onto the kernel backend |
| 539 | + when a Reyden / Real-Time warehouse rejects the default Thrift protocol. |
| 540 | +
|
| 541 | + Reyden warehouses reject a Thrift ``OpenSession`` (SQLSTATE ``KP001``); |
| 542 | + the kernel (SEA) backend is the supported path. Auto-recovery applies |
| 543 | + only when the caller did not pick a backend explicitly (neither |
| 544 | + ``use_kernel`` nor ``use_sea``). On a rejection the warehouse is |
| 545 | + remembered so later connections skip the doomed Thrift attempt. |
| 546 | + """ |
| 547 | + |
| 548 | + def build_session(session_kwargs: dict) -> Session: |
| 549 | + # Assign self.session before open() so a failed open still leaves the |
| 550 | + # attempted session on the connection — __del__ and the failure |
| 551 | + # telemetry log both rely on self.session being present. |
| 552 | + self.session = Session( |
| 553 | + server_hostname, |
| 554 | + http_path, |
| 555 | + self.http_client, |
| 556 | + http_headers, |
| 557 | + session_configuration, |
| 558 | + catalog, |
| 559 | + schema, |
| 560 | + _use_arrow_native_complex_types, |
| 561 | + **session_kwargs, |
| 562 | + ) |
| 563 | + self.session.open() |
| 564 | + return self.session |
| 565 | + |
| 566 | + def kernel_recovery_kwargs() -> dict: |
| 567 | + # Kwargs for re-opening on the kernel. The Thrift path treats an |
| 568 | + # unset auth_type as databricks-oauth (see get_auth_provider); the |
| 569 | + # kernel path has no such fallback and rejects auth_type=None unless |
| 570 | + # a credential shape (PAT / OAuth M2M) is present. Mirror the Thrift |
| 571 | + # default so a bare OAuth-U2M connection recovers instead of failing |
| 572 | + # with NotSupportedError. Skip the injection when a credential shape |
| 573 | + # is already present — the kernel routes on it regardless of |
| 574 | + # auth_type, and forcing databricks-oauth alongside an M2M secret or |
| 575 | + # a credentials_provider would change that routing. |
| 576 | + recovery_kwargs = {**kwargs, "use_kernel": True} |
| 577 | + has_credential_shape = ( |
| 578 | + recovery_kwargs.get("access_token") |
| 579 | + or recovery_kwargs.get("oauth_client_secret") |
| 580 | + or recovery_kwargs.get("oauth_jwt_key_file") |
| 581 | + or recovery_kwargs.get("credentials_provider") |
| 582 | + ) |
| 583 | + if recovery_kwargs.get("auth_type") is None and not has_credential_shape: |
| 584 | + recovery_kwargs["auth_type"] = AuthType.DATABRICKS_OAUTH.value |
| 585 | + return recovery_kwargs |
| 586 | + |
| 587 | + # An explicit backend choice is always honored — auto-recovery engages |
| 588 | + # only on the default (Thrift) path. |
| 589 | + explicit_backend = kwargs.get("use_kernel", False) or kwargs.get( |
| 590 | + "use_sea", False |
| 591 | + ) |
| 592 | + if explicit_backend: |
| 593 | + return build_session(kwargs) |
| 594 | + |
| 595 | + warehouse_id = extract_warehouse_id(http_path) |
| 596 | + |
| 597 | + # Pre-check: a warehouse already seen to reject Thrift opens straight on |
| 598 | + # the kernel, skipping the doomed Thrift OpenSession round-trip. |
| 599 | + if warehouse_id and is_known_reyden(server_hostname, warehouse_id): |
| 600 | + logger.info( |
| 601 | + "Warehouse %s on %s is known to require the kernel backend; " |
| 602 | + "opening on the kernel and skipping Thrift.", |
| 603 | + warehouse_id, |
| 604 | + server_hostname, |
| 605 | + ) |
| 606 | + return build_session(kernel_recovery_kwargs()) |
| 607 | + |
| 608 | + try: |
| 609 | + return build_session(kwargs) |
| 610 | + except ReydenThriftUnsupportedError as thrift_ex: |
| 611 | + logger.info( |
| 612 | + "Thrift is not supported for this Reyden/Real-Time warehouse; " |
| 613 | + "transparently re-opening the session on the kernel backend." |
| 614 | + ) |
| 615 | + # Remember the rejection regardless of the retry's outcome — the |
| 616 | + # warehouse is Reyden either way, so future connects should skip |
| 617 | + # Thrift; a kernel failure below is a separate, orthogonal problem. |
| 618 | + if warehouse_id: |
| 619 | + mark_reyden(server_hostname, warehouse_id) |
| 620 | + try: |
| 621 | + return build_session(kernel_recovery_kwargs()) |
| 622 | + except Exception as kernel_ex: |
| 623 | + # Surface the kernel failure (the actionable one) while keeping |
| 624 | + # the original Thrift rejection in the chain for diagnosis. |
| 625 | + raise kernel_ex from thrift_ex |
| 626 | + |
515 | 627 | def _set_use_inline_params_with_warning(self, value: Union[bool, str]): |
516 | 628 | """Valid values are True, False, and "silent" |
517 | 629 |
|
|
0 commit comments