Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion KERNEL_REV
Original file line number Diff line number Diff line change
@@ -1 +1 @@
e8d60c93b7459947d2e3e4113f62e6bda37fde8e
dd810d6d0a179886b923c6e22dc785ddca16ebef
4 changes: 4 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,10 @@ static authenticators, and custom M2M OAuth scopes
Unity Catalog volume) is rejected at execute. WithMaxRows is accepted but inert (the
kernel manages fetching below the C ABI).

Kernel HTTP requests inherit the driver's ClientTimeout (900s by default). If
that value is zero, the kernel uses its own 120s default; zero is neither
unlimited nor an immediate timeout.

OAuth U2M is interactive: on a cache miss, connecting launches the system browser and
blocks until login completes or the kernel's ~120s callback timeout expires. Because
the C ABI can't interrupt session open mid-call, a connection-context deadline is not
Expand Down
26 changes: 26 additions & 0 deletions internal/backend/kernel/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,10 @@ func (k *KernelBackend) OpenSession(ctx context.Context) error {
return err
}

if err := k.applyRequestTimeout(cfg); err != nil {
return err
}

// Retry / backoff policy (WithRetries). See applyRetry.
if err := k.applyRetry(cfg); err != nil {
return err
Expand Down Expand Up @@ -357,6 +361,16 @@ func (k *KernelBackend) applyProxy(cfg *C.KernelSessionConfig) error {
return nil
}

func (k *KernelBackend) applyRequestTimeout(cfg *C.KernelSessionConfig) error {
timeoutMs := requestTimeoutMilliseconds(k.cfg.RequestTimeout)
if err := call(func() C.KernelStatusCode {
return C.kernel_session_config_set_request_timeout(cfg, C.uint64_t(timeoutMs))
}); err != nil {
return fmt.Errorf("kernel: set_request_timeout: %w", toConnError(err))
}
return nil
}

// applyRetry forwards the driver's HTTP retry / backoff policy to the session
// config. A no-op when Config.Retry is nil, so the kernel's own default policy
// (exponential backoff with jitter, 5 retries, 1s..60s, 900s budget) is preserved
Expand Down Expand Up @@ -541,6 +555,18 @@ func trySetTokenCacheConfig(auth Auth, enabled bool) error {
return k.setAuth(cfg)
}

// trySetRequestTimeout exercises the request-timeout C setter without opening a
// network session. It is used only by the tagged kernel tests.
func trySetRequestTimeout(cfg Config) error {
var c *C.KernelSessionConfig
if err := call(func() C.KernelStatusCode { return C.kernel_session_config_new(&c) }); err != nil {
return fmt.Errorf("config_new: %w", err)
}
defer C.kernel_session_config_free(c)
k := &KernelBackend{cfg: cfg}
return k.applyRequestTimeout(c)
}

// applyInitialNamespace runs USE CATALOG / USE SCHEMA to select the configured
// initial namespace, since the kernel C ABI exposes no catalog/schema setter.
// Identifiers are backtick-quoted (quoteIdent) so arbitrary names are safe. A
Expand Down
17 changes: 17 additions & 0 deletions internal/backend/kernel/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ type Config struct {
// attributed to this driver (not the kernel's built-in UA). Empty leaves it unset.
UserAgent string

// RequestTimeout is the total HTTP request deadline, from connect through
// response-body completion. Zero selects the kernel's 120s default; it is
// neither unlimited nor an immediate timeout.
RequestTimeout time.Duration

// SessionConf carries server-bound session confs verbatim — the same map the
// Thrift backend forwards (STATEMENT_TIMEOUT, QUERY_TAGS, TIMEZONE, …).
SessionConf map[string]string
Expand Down Expand Up @@ -105,3 +110,15 @@ type RetryConfig struct {
// default (900s). Mirrors the pyo3/napi retry_overall_timeout knob.
OverallTimeout time.Duration
}

// requestTimeoutMilliseconds returns the value sent to the C ABI. Zero selects
// the kernel default; positive sub-millisecond values round up to 1ms.
func requestTimeoutMilliseconds(timeout time.Duration) int64 {
if timeout <= 0 {
return 0
}
if timeout < time.Millisecond {
return 1
}
return timeout.Milliseconds()
}
26 changes: 26 additions & 0 deletions internal/backend/kernel/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package kernel

import (
"testing"
"time"
)

func TestRequestTimeoutMilliseconds(t *testing.T) {
for _, tc := range []struct {
name string
timeout time.Duration
want int64
}{
{"zero", 0, 0},
{"negative", -time.Second, 0},
{"sub-millisecond", time.Nanosecond, 1},
{"fractional milliseconds", 1500 * time.Microsecond, 1},
{"seconds", 12 * time.Second, 12_000},
} {
t.Run(tc.name, func(t *testing.T) {
if got := requestTimeoutMilliseconds(tc.timeout); got != tc.want {
t.Errorf("requestTimeoutMilliseconds(%v) = %d, want %d", tc.timeout, got, tc.want)
}
})
}
}
17 changes: 17 additions & 0 deletions internal/backend/kernel/kernel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,23 @@ func TestSetTokenCache(t *testing.T) {
}
}

func TestSetRequestTimeout(t *testing.T) {
for _, tc := range []struct {
name string
timeout time.Duration
}{
{"default", 900 * time.Second},
{"zero keeps kernel default", 0},
{"sub-millisecond rounds up", time.Nanosecond},
} {
t.Run(tc.name, func(t *testing.T) {
if err := trySetRequestTimeout(Config{RequestTimeout: tc.timeout}); err != nil {
t.Errorf("applyRequestTimeout(%s) = %v, want nil", tc.name, err)
}
})
}
}

// TestKernelLogLevel and TestResolveKernelLogArg — the pure level-resolution tests —
// live in the untagged logging_level_test.go so they run under CGO_ENABLED=0. The
// tests below exercise klog/klogCtx and so need the cgo build.
Expand Down
2 changes: 2 additions & 0 deletions kernel_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ func buildKernelConfig(cfg *config.Config, kauth kernel.Auth) kernel.Config {
WarehouseID: cfg.WarehouseID,
Auth: kauth,
Location: cfg.Location,
// ClientTimeout is the Go HTTP client's total request deadline.
RequestTimeout: cfg.ClientTimeout,
// Same UA the Thrift path sends, so query history attributes both alike.
UserAgent: client.BuildUserAgent(cfg),
// Initial namespace: no kernel config setter, so the kernel backend applies
Expand Down
3 changes: 3 additions & 0 deletions kernel_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,9 @@ func TestBuildKernelConfig(t *testing.T) {
if kc.Auth.Mode != kauth.Mode || kc.Auth.Token != kauth.Token {
t.Errorf("auth not forwarded: got %+v, want %+v", kc.Auth, kauth)
}
if kc.RequestTimeout != c.ClientTimeout {
t.Errorf("RequestTimeout = %v, want ClientTimeout %v", kc.RequestTimeout, c.ClientTimeout)
}
// UserAgent must be the driver's composed UA, non-empty — else query
// history mis-attributes SEA-path queries to the kernel's built-in UA.
if want := client.BuildUserAgent(c); kc.UserAgent == "" || kc.UserAgent != want {
Expand Down
23 changes: 20 additions & 3 deletions kernel_telemetry.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package dbsql

import (
"time"

"github.com/databricks/databricks-sql-go/internal/backend/kernel"
"github.com/databricks/databricks-sql-go/internal/config"
"github.com/databricks/databricks-sql-go/telemetry"
Expand All @@ -20,9 +22,9 @@ import (
// to actually emit it. Pure Go so it is unit-testable in the default build.
//
// It reports only what the kernel path genuinely applies: EnableArrow is always
// true (the kernel returns Arrow results), UseProxy reflects whether a proxy was
// resolved (env-var or WithKernelProxy), and it does not claim direct-results or a
// socket timeout the kernel C ABI has no knob for.
// true, UseProxy reflects the resolved proxy, and SocketTimeout reports the
// configured ClientTimeout in the receiver schema's seconds unit. It does not
// claim direct-results.
func kernelConnectionTelemetry(cfg *config.Config) *telemetry.DriverConnectionParameters {
params := &telemetry.DriverConnectionParameters{
HTTPPath: cfg.HTTPPath,
Expand All @@ -40,6 +42,7 @@ func kernelConnectionTelemetry(cfg *config.Config) *telemetry.DriverConnectionPa
},
UseProxy: kernelUsesProxy(cfg),
EnableMetricViewMeta: cfg.EnableMetricViewMetadata,
SocketTimeout: kernelSocketTimeoutSeconds(cfg.ClientTimeout),
}
if qt := cfg.SessionParams["QUERY_TAGS"]; qt != "" {
params.QueryTags = qt
Expand Down Expand Up @@ -67,6 +70,20 @@ func kernelConnectionTelemetry(cfg *config.Config) *telemetry.DriverConnectionPa
return params
}

func kernelSocketTimeoutSeconds(timeout time.Duration) int64 {
Comment thread
vuanhphung marked this conversation as resolved.
if timeout <= 0 {
return 0
}
seconds := timeout / time.Second
if timeout%time.Second >= time.Second/2 {
seconds++
}
if seconds == 0 {
return 1
}
return int64(seconds)
}

// kernelUsesProxy reports whether the kernel connection will route through a
// proxy: either an explicit WithKernelProxy or an environment-resolved one for
// this endpoint. Mirrors resolveKernelProxy's proxy-source precedence.
Expand Down
26 changes: 26 additions & 0 deletions kernel_telemetry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ func TestKernelConnectionTelemetry(t *testing.T) {
if p.HostInfo == nil || p.HostInfo.HostURL != "example.cloud.databricks.com" || p.HostInfo.Port != 443 {
t.Errorf("HostInfo = %+v, want host/port populated", p.HostInfo)
}
if p.SocketTimeout != 900 {
t.Errorf("SocketTimeout = %d, want 900", p.SocketTimeout)
}
})

t.Run("query tags + metric-view are reflected", func(t *testing.T) {
Expand All @@ -60,6 +63,29 @@ func TestKernelConnectionTelemetry(t *testing.T) {
}
})

t.Run("request timeout reports receiver-schema seconds", func(t *testing.T) {
cfg := config.WithDefaults()
for _, tc := range []struct {
name string
timeout time.Duration
want int64
}{
{"zero", 0, 0},
{"negative", -time.Second, 0},
{"positive sub-second floors to one", time.Nanosecond, 1},
{"fractional second rounds down", 1499 * time.Millisecond, 1},
{"half second rounds up", 1500 * time.Millisecond, 2},
{"whole seconds", 12 * time.Second, 12},
} {
t.Run(tc.name, func(t *testing.T) {
cfg.ClientTimeout = tc.timeout
if got := kernelConnectionTelemetry(cfg).SocketTimeout; got != tc.want {
t.Errorf("SocketTimeout = %d, want %d", got, tc.want)
}
})
}
})

t.Run("explicit WithKernelProxy marks UseProxy", func(t *testing.T) {
cfg := config.WithDefaults()
cfg.AccessToken = "dapi-x"
Expand Down
Loading