Skip to content

feat: auto-recover Reyden Thrift connections onto the kernel backend - #479

Open
rahuls-db wants to merge 6 commits into
mainfrom
feat/reyden-thrift-auto-recovery
Open

feat: auto-recover Reyden Thrift connections onto the kernel backend#479
rahuls-db wants to merge 6 commits into
mainfrom
feat/reyden-thrift-auto-recovery

Conversation

@rahuls-db

Copy link
Copy Markdown
Collaborator

Description

Ports the Reyden Thrift auto-recovery feature (already in the Python driver, databricks/databricks-sql-python#948) to the Go driver.

An unconfigured connection to a Reyden / Real-Time SQL warehouse defaults to the Thrift backend, which the SQL Gateway proxy rejects with SQLSTATE KP001. This change detects that rejection at OpenSession and transparently re-opens the session on the SEA/kernel backend, so no connection-parameter change is needed.

  • Detection: client.CheckStatus returns a distinct ErrReydenThriftUnsupported (via errors.Is) when the OpenSession TStatus carries SQLSTATE KP001 (matched on the SQLSTATE only).
  • Recovery: connector.openSessionWithReydenFallback catches the marker and re-opens once on the kernel backend. On a double failure the kernel error is surfaced with the original Thrift rejection preserved via errors.Join.
  • Pinning: a process-wide cache (internal/warehouse_cache) keyed by (host_lowercased, warehouse_id), ~6h TTL, sync.RWMutex-guarded, with opportunistic eviction; a pre-check skips the Thrift round-trip for a known-Reyden warehouse.
  • Guardrail: only the default path auto-recovers; an explicit WithUseKernel is always honored.

Build-tag caveat: the kernel backend is only linked in under -tags databricks_kernel + CGO_ENABLED=1. In a default build the fallback surfaces the not-compiled error joined with the Reyden rejection, rather than silently recovering — the same kernel-availability constraint the Python driver has with its optional [kernel] extra.

Testing

Unit tests drive the real openSessionWithReydenFallback via injected backend-factory seams (connector_reyden_test.go): KP001 detection, reactive recovery onto the kernel, cache pre-check skipping Thrift, cache marking, explicit-UseKernel guardrail, non-Reyden error pass-through, and double-failure errors.Join chaining; plus internal/warehouse_cache cache/extraction/expiry tests. go test (root + changed internal packages), gofmt, and go vet are clean.

Related

Design: "Simplifying Reyden Onboarding on Drivers" (Option A). Sibling PRs: Python databricks/databricks-sql-python#948; a Node port is in flight.


This PR was created with GitHub MCP.

An unconfigured connection to a Reyden / Real-Time SQL warehouse defaults to
the Thrift backend, which the SQL Gateway proxy rejects with SQLSTATE KP001.
Detect that rejection at OpenSession (CheckStatus) and transparently re-open
the session on the SEA/kernel backend, remembering the warehouse in a
process-wide cache keyed by (host, warehouse_id) with a ~6h TTL so later
connects skip the doomed Thrift attempt. Only the default path auto-recovers;
an explicit WithUseKernel is always honored. On a double failure the kernel
error is surfaced with the Thrift rejection preserved via errors.Join.

Recovery requires a databricks_kernel build, since the kernel backend is
otherwise not linked in (the fallback then surfaces the not-compiled error
joined with the Reyden rejection).

Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: Rahul Singhal <rahul.singhal@databricks.com>
skipDriverTelemetry read cfg.UseKernel, which stays false on the Reyden
auto-recovery path (the fallback opens the kernel without mutating cfg). A
recovered-kernel connection therefore kept the Go driver's telemetry active,
duplicating the kernel's own telemetry. Derive the skip decision from the
backend that actually opened instead. Mirrors the analogous fix in the Python
driver.

Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: Rahul Singhal <rahul.singhal@databricks.com>

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 1 Medium · 1 Low

Solid, well-tested port of the Reyden Thrift→kernel auto-recovery feature; the error-marker plumbing, cache, and guardrails are coherent and the production errors.Is chain checks out. Two non-blocking notes: a coverage gap where the recovery tests bypass the real Thrift error-wrapping path (Medium), and a cache-dependent inconsistency in the WithKernel*-without-WithUseKernel guardrail (Low). Nit: skipDriverTelemetry (connector.go:48) is now vestigial production code — replaced by shouldSkipDriverTelemetry in Connect and referenced only by connector_kernel_u2m_test.go; consider removing it and repointing that test.

Comment thread connector_reyden_test.go Outdated
Comment thread connector.go
CheckStatus is the shared status checker for every Thrift RPC, so mapping KP001
to the recoverable marker there gave it a wider blast radius than the recovery
logic (which only wraps session open): a stray KP001 on any other RPC would
have surfaced as ErrReydenThriftUnsupported with no handler. Keep CheckStatus
generic and add an OpenSession-scoped CheckOpenSessionStatus that does the KP001
mapping; only the OpenSession wrapper uses it. Every other RPC now surfaces a
KP001 as a plain error, unchanged from before.

Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: Rahul Singhal <rahul.singhal@databricks.com>

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 3 Low

Solid, well-tested port of the Reyden Thrift→kernel auto-recovery. The KP001 detection is correctly scoped to OpenSession, the marker's errors.Is chaining works through the production requestError wrapper, the cache is properly RWMutex-guarded and host-keyed, and no new third-party imports/leaks are introduced. Only 3 low-severity notes: a test-fidelity gap (tests inject the raw marker, not the wrapped form production emits), a now-test-only stale skipDriverTelemetry, and a guardrail bypass in the cache pre-check.

Comment thread connector_reyden_test.go
assert.True(t, kb.SessionValid(), "kernel session should be open")

// The rejection is remembered for future connects.
assert.True(t, warehouse_cache.IsKnownReyden(tt.host, tt.warehouseID),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Low — The reactive-recovery test injects the raw reydenThriftUnsupportedError as openSessionErr, but in production thrift.Backend.OpenSession wraps every OpenSession failure in NewRequestError(ctx, "error connecting: ...", err) before it reaches openSessionWithReydenFallback. So the connector actually calls errors.Is(err, ErrReydenThriftUnsupported) against a *requestErrorwithStackwithMessage → marker chain, not the bare marker.

The detection does still work (I traced requestError.UnwrapwithStack.UnwrapwithMessage.CausereydenThriftUnsupportedError.Is), so this is not a live bug. But no test exercises the wrapped form the connector sees in production — a future change to the unwrap chain or to how the Thrift backend wraps the error would silently break auto-recovery while all these tests stay green. Consider having the fake Thrift backend return dbsqlerrint.NewRequestError(context.Background(), "error connecting", dbsqlerrint.NewReydenThriftUnsupportedError(...)) so the test matches the production error shape.

Comment thread connector.go
cfg *config.Config
client *http.Client
kernelBackendFactory backendFactory // Seam for testing; nil in production
thriftBackendFactory thriftBackendFactory // Seam for testing; nil in production

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Low — skipDriverTelemetry(cfg *config.Config) is no longer used by production code — Connect now derives the skip decision from the active backend via shouldSkipDriverTelemetry(be) (connector.go:101). The only remaining references are in connector_kernel_u2m_test.go, so the function survives unused/staticcheck purely as test-only code that duplicates the now-superseded cfg.UseKernel logic.

Since the real decision moved to shouldSkipDriverTelemetry, this stale helper (and its test) can drift from actual behavior — e.g. it returns false for a Reyden auto-recovery onto the kernel, which is exactly the case the new logic was written to fix. Recommend deleting skipDriverTelemetry and repointing that test at shouldSkipDriverTelemetry.

(Anchored to the nearest changed line — see the description for the exact location.)

Comment thread connector.go
// returns the backend, session latency, and error.
func (c *connector) openSessionWithReydenFallback(ctx context.Context) (backend.Backend, int64, error) {
// Extract warehouse ID from HTTPPath for cache lookups.
warehouseID := warehouse_cache.ExtractWarehouseID(c.cfg.HTTPPath)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Low — The known-Reyden pre-check opens the kernel backend directly and never runs the KernelExperimental != nil guardrail that the default (non-UseKernel) path enforces at connector.go:846. As a result, the same config (no WithUseKernel, but a WithKernel* option set) produces two different outcomes depending on process-global cache state: a clear ErrRequiresKernelBackend error on a cache miss, but a silent kernel open on a cache hit.

This isn't harmful (the pre-check path does construct the kernel backend, so the experimental option is honored rather than ignored), but the nondeterministic branch-on-cache behavior is surprising and undocumented. Worth either applying the same guardrail in the pre-check branch, or adding a comment noting the intentional divergence.

…apped-marker tests

- connector: hoist the WithKernel*-without-WithUseKernel guardrail above the cache
  pre-check so the misconfiguration is rejected deterministically, regardless of
  process-global cache state (previously a warm cache let the pre-check open the
  kernel and silently bypass the guardrail).
- connector: delete the now-unused skipDriverTelemetry(cfg); the skip decision is
  derived from the active backend via shouldSkipDriverTelemetry(be). Remove its
  redundant test (shouldSkipDriverTelemetry is already covered).
- connector_reyden_test: inject the Reyden marker WRAPPED in NewRequestError, as
  thrift.Backend.OpenSession does in production, so the errors.Is unwrap chain the
  recovery relies on is pinned; add a test that the guardrail fires on a warm cache.

Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: Rahul Singhal <rahul.singhal@databricks.com>

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 1 Medium

Solid, well-tested port of the Reyden Thrift→kernel auto-recovery. The recovery logic, error-marker plumbing (errors.Is chain through NewRequestError), guardrail ordering, and cache keying all look correct. One medium concern: a stub-only test is not build-tagged and will fail under the databricks_kernel build. Nit: Cache.MarkReyden's sweep re-indexes the map (c.expiry[key].Before(now)) instead of ranging over key, deadline — harmless but slightly wasteful.

Comment thread connector_reyden_test.go Outdated
// In the default pure-Go build, newKernelBackend returns an error indicating
// the kernel backend is not compiled in. This naturally exercises the
// error-chaining path for double-failure scenarios.
cfg := config.WithDefaults()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium — TestReydenDefaultBuildKernelNotCompiled asserts that newKernelBackend returns an error wrapping ErrKernelNotCompiled (assert.Nil(be), assert.Error(err), errors.Is(err, ErrKernelNotCompiled)). That behavior is specific to the default-build stub (kernel_stub.go, //go:build !cgo || !databricks_kernel). This test file has no build constraint, so it is also compiled and run under -tags databricks_kernel + CGO_ENABLED=1, where newKernelBackend resolves to the real implementation in kernel_backend.go and will not return ErrKernelNotCompiled — the three assertions fail and make test under the kernel build breaks.

Every other kernel-behavior test in this package (kernel_stub_test.go, kernel_backend_test.go, etc.) is build-tagged accordingly. This test asserts stub-only behavior, so it should carry //go:build !cgo || !databricks_kernel (matching kernel_stub_test.go). The other tests in this file are backend-agnostic (they use the injected fakes), so only this one test needs to move to a tagged file or gain the constraint.

- warehouse_cache/cache.go: drop the redundant `match != nil` (staticcheck S1009:
  len(nil) is 0) and fix struct field gofmt alignment. Both failed golangci-lint.
- Move TestReydenDefaultBuildKernelNotCompiled into a //go:build !databricks_kernel
  file. It asserts newKernelBackend reports "not compiled", which only holds in the
  default build; under -tags databricks_kernel the real backend is linked in, so the
  test was failing the "Test (kernel backend)" CI job.

Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: Rahul Singhal <rahul.singhal@databricks.com>

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 1 Low

Looks good — solid, well-tested port of the Reyden auto-recovery feature. The errors.Is unwrap chain through NewRequestError is verified correct, the WithKernel*-without-WithUseKernel guardrail is preserved (now checked before the cache pre-check), and telemetry attribution correctly derives from the active backend rather than cfg.UseKernel. One low-severity behavioral note on the cache pre-check path (no Thrift fallback / stripped context on a kernel failure) is posted inline.

Comment thread connector.go Outdated
- connector: on the cache pre-check path, wrap a kernel backend-create/OpenSession
  failure with context noting the warehouse was cached as Reyden and Thrift was
  skipped. Uses %w so the underlying error (including a default-build
  ErrKernelNotCompiled) stays reachable via errors.Is; adds a test asserting both.
- warehouse_cache: range the sweep over (key, deadline) instead of re-indexing the
  map per key.

Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: Rahul Singhal <rahul.singhal@databricks.com>

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 1 Low

Looks good — a clean, well-tested port. The Reyden marker's errors.Is chain survives the production NewRequestError wrapping, shouldSkipDriverTelemetry correctly derives from the active backend (fixing recovered-onto-kernel attribution), and the warehouse_cache package is stdlib-only with sound locking/TTL semantics. One low-severity note inline about the pre-check not being gated on UseKernel. Nit (summary-only): the warehouse_cache package name uses an underscore, which is non-idiomatic Go — it won't fail CI since ST1003 is disabled in .golangci.yml, but warehousecache would match convention.

Comment thread connector.go
// than a stale subset that drifts as options are added.
if !c.cfg.UseKernel && c.cfg.KernelExperimental != nil {
return nil, 0, fmt.Errorf("databricks: a WithKernel* option %w; "+
"add WithUseKernel(true) or remove it", dbsqlerr.ErrRequiresKernelBackend)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Low — The cache pre-check fires regardless of c.cfg.UseKernel. When a caller explicitly sets WithUseKernel(true) for a warehouse that happens to be cached as Reyden, control enters this pre-check branch instead of the normal if c.cfg.UseKernel path below. Functionally the outcome is identical (kernel either way), but on a kernel failure the error is wrapped with a misleading message — "warehouse %s is cached as Reyden so Thrift was skipped, but the kernel backend could not be created" — even though Thrift was never in play for an explicit-kernel connection.

Consider gating the pre-check on !c.cfg.UseKernel so the auto-recovery messaging only applies to the default (Thrift) path, and explicit-kernel connections fall through to the plain getKernelBackend branch with its normal error surface.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant