Skip to content

Commit 8d80062

Browse files
Divyansh-dbsd-db
andauthored
Fall back to remote runtime on Spark Connect when the legacy namespace is unavailable (#1469)
## Summary Importing `databricks.sdk.runtime` on a Spark Connect runtime (e.g. shared-access-mode clusters) no longer raises `CONTEXT_UNAVAILABLE_FOR_REMOTE_CLIENT` at import time. When the legacy user namespace cannot be materialized, the import now logs a warning and falls back to the existing Spark Connect-compatible remote implementation, so `WorkspaceClient()` construction succeeds on such clusters. Fixes #1463. Carries forward @sd-db's work from #1464 (closed because fork PRs in this repo cannot run tests). ## Why `WorkspaceClient.__init__` eagerly builds `dbutils` via `_make_dbutils`, which on a cluster does `from databricks.sdk.runtime import dbutils`. That import calls `UserNamespaceInitializer.getOrCreate().get_namespace_globals()`, materializing a legacy `SparkContext`. On a Spark Connect cluster this raises `CONTEXT_UNAVAILABLE_FOR_REMOTE_CLIENT` — a `pyspark.errors.PySparkRuntimeError`, not an `ImportError` — so the existing `except ImportError:` does not catch it and the error escapes the import, crashing `WorkspaceClient` construction before any API call. This is what databricks/dbt-databricks#1252 hits in Python models on shared clusters. The existing `except ImportError` branch is already the Spark Connect-compatible path (it builds `spark` via `DatabricksSession` and `dbutils` via `RemoteDbUtils`), so this PR routes the materialization failure there. A complementary follow-up — making `WorkspaceClient.dbutils` lazy via a `cached_property` so consumers that never touch it skip the build entirely — is noted in #1463 as a separate discussion since it touches generated code. Related issue #986 (off-cluster eager `RemoteDbUtils` auth failure) is the symmetric case and is intentionally not addressed here; the lazy-dbutils follow-up would unify both. ## What changed ### Behavioral changes On a Spark Connect runtime, importing `databricks.sdk.runtime` now logs a `WARNING` and uses the remote implementation instead of raising at import time. When `dbruntime` is absent (off-cluster) or the namespace materializes successfully (classic runtime), behavior is unchanged. ### Internal changes `databricks/sdk/runtime/__init__.py`: the runtime-namespace block is restructured into a single `try` with sibling `except ImportError` (existing — "not in a classic runtime") and `except Exception` (new — Spark Connect / `CONTEXT_UNAVAILABLE_FOR_REMOTE_CLIENT`, logged) clauses, plus an `if not _use_runtime_namespace:` guard over the existing — unchanged — OSS/remote block. The catch is intentionally broad rather than typed on `PySparkRuntimeError` to avoid pulling `pyspark` in at SDK import time just to narrow the exception type; the inline comment notes this. ## How is this tested? New `tests/test_runtime.py` simulates a Spark Connect runtime by injecting a fake `dbruntime` whose `get_namespace_globals()` raises `CONTEXT_UNAVAILABLE_FOR_REMOTE_CLIENT`, and asserts that: - reloading `databricks.sdk.runtime` survives the failure and falls back (`is_local_implementation is True`, `dbutils is not None`) - `WorkspaceClient(config=…)` constructs without raising — the direct reproduction of the reported failure Verified red→green locally. Full unit test suite (2098 tests) passes with no regressions. --------- Signed-off-by: Shubham Dhal <shubham.dhal@databricks.com> Signed-off-by: Divyansh Vijayvergia <171924202+Divyansh-db@users.noreply.github.com> Co-authored-by: Shubham Dhal <shubham.dhal@databricks.com>
1 parent d01f89a commit 8d80062

3 files changed

Lines changed: 82 additions & 2 deletions

File tree

NEXT_CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
### Bug Fixes
1010

11+
* Fall back to the remote runtime implementation when the legacy user namespace cannot be materialized. On Spark Connect runtimes (e.g. shared-access-mode clusters), importing `databricks.sdk.runtime` — which happens when constructing a `WorkspaceClient` on such a cluster — tried to build a legacy `SparkContext` and raised `CONTEXT_UNAVAILABLE_FOR_REMOTE_CLIENT` at import time. It now logs a warning and falls back to the Spark Connect-compatible remote implementation instead of crashing.
1112
* Cache tokens minted by `DatabricksOidcTokenSource` (Workload Identity Federation / account-wide token federation). Previously a fresh `/oidc/v1/token` exchange was performed on every authenticated API call, adding latency, amplifying transient federation-policy errors, and hitting OIDC token-endpoint rate limits. The token source now reuses the cached token until it is stale or expired, fetching a fresh ID token on each refresh to support rotation.
1213

1314
### Documentation

databricks/sdk/runtime/__init__.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,10 @@ def inner() -> Dict[str, str]:
9393
return None, None
9494

9595

96+
# Internal implementation
97+
# Separated from above for backward compatibility
98+
_use_runtime_namespace = False
9699
try:
97-
# Internal implementation
98-
# Separated from above for backward compatibility
99100
from dbruntime import UserNamespaceInitializer
100101

101102
userNamespaceGlobals = UserNamespaceInitializer.getOrCreate().get_namespace_globals()
@@ -105,7 +106,23 @@ def inner() -> Dict[str, str]:
105106
continue
106107
_globals[var] = userNamespaceGlobals[var]
107108
is_local_implementation = False
109+
_use_runtime_namespace = True
108110
except ImportError:
111+
# Not running inside a classic Databricks runtime; fall back to the OSS implementation below.
112+
pass
113+
except Exception as e:
114+
# On Spark Connect runtimes (e.g. shared-access-mode clusters), materializing the
115+
# legacy user namespace builds a SparkContext, which is unavailable in remote clients
116+
# and raises CONTEXT_UNAVAILABLE_FOR_REMOTE_CLIENT. Treat this like "not in a classic
117+
# runtime" and fall back to the OSS/remote implementation below, which is Spark
118+
# Connect-compatible. Without this, importing databricks.sdk.runtime (and therefore
119+
# constructing a WorkspaceClient on such a cluster) raises at import time. The catch
120+
# is broad rather than typed on PySparkRuntimeError so the SDK does not need to import
121+
# pyspark just to narrow the exception type; any other unexpected failure here is also
122+
# safer surfaced as a warning + remote fallback than as a constructor crash.
123+
logger.warning(f"Runtime namespace unavailable, falling back to remote implementation: {e}")
124+
125+
if not _use_runtime_namespace:
109126
# OSS implementation
110127
is_local_implementation = True
111128

tests/test_runtime.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""Tests for the import-time behavior of ``databricks.sdk.runtime``."""
2+
3+
import sys
4+
import types
5+
6+
import pytest
7+
8+
from databricks.sdk.dbutils import RemoteDbUtils
9+
10+
11+
@pytest.fixture
12+
def spark_connect_runtime(monkeypatch):
13+
"""``dbruntime`` is importable, but materializing the legacy user namespace raises
14+
``CONTEXT_UNAVAILABLE_FOR_REMOTE_CLIENT`` — the Spark Connect failure mode."""
15+
16+
class _Initializer:
17+
@staticmethod
18+
def getOrCreate():
19+
class _Namespace:
20+
def get_namespace_globals(self):
21+
raise RuntimeError(
22+
"[CONTEXT_UNAVAILABLE_FOR_REMOTE_CLIENT] Calls to SparkContext are "
23+
"not supported on a Spark Connect cluster. Use spark instead."
24+
)
25+
26+
return _Namespace()
27+
28+
fake = types.ModuleType("dbruntime")
29+
fake.UserNamespaceInitializer = _Initializer
30+
monkeypatch.setitem(sys.modules, "dbruntime", fake)
31+
32+
# The remote fallback constructs ``RemoteDbUtils()``, which initializes a default
33+
# ``Config``; hermetic PAT credentials keep the fallback from failing for unrelated
34+
# auth reasons (see databricks-sdk-py#986).
35+
monkeypatch.setenv("DATABRICKS_HOST", "https://test.cloud.databricks.com")
36+
monkeypatch.setenv("DATABRICKS_TOKEN", "test-token")
37+
38+
# Force ``databricks.sdk.runtime`` to re-execute its module body on next import so it
39+
# picks up the fake ``dbruntime``. Earlier tests (e.g. test_notebook_oauth.py) cache a
40+
# fake module here directly via ``sys.modules`` without going through the import
41+
# machinery, which leaves the ``runtime`` attribute on ``databricks.sdk`` unset —
42+
# dropping the cached entry repairs that on the next real import. ``monkeypatch``
43+
# restores the prior value on teardown.
44+
monkeypatch.delitem(sys.modules, "databricks.sdk.runtime", raising=False)
45+
46+
47+
def test_runtime_import_falls_back_on_spark_connect(spark_connect_runtime):
48+
"""Regression for dbt-databricks#1252: import survives the namespace failure."""
49+
import databricks.sdk.runtime as runtime
50+
51+
assert runtime.is_local_implementation is True
52+
assert isinstance(runtime.dbutils, RemoteDbUtils)
53+
54+
55+
def test_workspace_client_constructs_on_spark_connect(spark_connect_runtime, config):
56+
"""Regression for dbt-databricks#1252: ``WorkspaceClient.__init__`` eagerly builds
57+
dbutils via ``databricks.sdk.runtime`` and must not raise on Spark Connect."""
58+
from databricks.sdk import WorkspaceClient
59+
60+
ws = WorkspaceClient(config=config)
61+
62+
assert ws is not None

0 commit comments

Comments
 (0)