diff --git a/KERNEL_REV b/KERNEL_REV index 65457d79..e838aedf 100644 --- a/KERNEL_REV +++ b/KERNEL_REV @@ -1 +1 @@ -80b68e1eef3b613910183a50dfa4dace854d50dd +fcc459bbf3f39bf57e2ee02f14b99c0ec7a70123 diff --git a/lib/sea/SeaAuth.ts b/lib/sea/SeaAuth.ts index a9d9d116..4c53cdb5 100644 --- a/lib/sea/SeaAuth.ts +++ b/lib/sea/SeaAuth.ts @@ -91,6 +91,20 @@ export interface SeaSessionDefaults { * integer within the napi `u32` range by `buildSeaConnectionOptions`. */ maxConnections?: number; + /** + * Retry/backoff tuning forwarded to the kernel (which owns the retry loop + * on the SEA path). These mirror the driver's `ClientConfig` retry knobs — + * the same ones the Thrift `HttpRetryPolicy` uses — converted from the + * connector's milliseconds to the kernel's whole seconds, so a single + * retry config governs both backends. Unset ⇒ kernel default policy. + * Map onto the napi `ConnectionOptions.retry{Min,Max}WaitSecs` / + * `retryMaxAttempts` / `retryOverallTimeoutSecs` (see `buildSeaRetryOptions`). + */ + retryMinWaitSecs?: number; + retryMaxWaitSecs?: number; + /** **Total** attempts (kernel converts to retries-after-first internally). */ + retryMaxAttempts?: number; + retryOverallTimeoutSecs?: number; } /** @@ -274,6 +288,33 @@ export function buildSeaTlsOptions(options: ConnectionOptions): SeaTlsOptions { * - `HiveDriverError` for unsupported auth modes / Azure-direct / * custom persistence / ambiguous combinations. */ +/** + * Convert the driver's `ClientConfig` retry knobs (milliseconds, total-attempt + * count) into the kernel's `ConnectionOptions` retry kwargs (whole seconds). + * The kernel owns the retry loop on the SEA path, so forwarding these keeps SEA + * and Thrift governed by one retry config. `retryMaxAttempts` is a TOTAL attempt + * count on both sides (the kernel converts to retries-after-first internally), + * so it passes through directly. Sub-second delays round to the nearest second + * (the kernel's granularity); all values are clamped into the napi `u32` range. + */ +export function buildSeaRetryOptions(config: { + retryMaxAttempts: number; + retriesTimeout: number; + retryDelayMin: number; + retryDelayMax: number; +}): Required< + Pick +> { + const msToSecs = (ms: number): number => Math.min(MAX_U32, Math.max(0, Math.round(ms / 1000))); + const clampU32 = (n: number): number => Math.min(MAX_U32, Math.max(0, Math.trunc(n))); + return { + retryMinWaitSecs: msToSecs(config.retryDelayMin), + retryMaxWaitSecs: msToSecs(config.retryDelayMax), + retryMaxAttempts: clampU32(config.retryMaxAttempts), + retryOverallTimeoutSecs: msToSecs(config.retriesTimeout), + }; +} + export function buildSeaConnectionOptions(options: ConnectionOptions): SeaNativeConnectionOptions { const { authType } = options as { authType?: string }; diff --git a/lib/sea/SeaBackend.ts b/lib/sea/SeaBackend.ts index 6f1bd5f0..3ee00288 100644 --- a/lib/sea/SeaBackend.ts +++ b/lib/sea/SeaBackend.ts @@ -21,7 +21,7 @@ import { LogLevel } from '../contracts/IDBSQLLogger'; import HiveDriverError from '../errors/HiveDriverError'; import { getSeaNative, SeaNativeBinding, SeaConnection } from './SeaNativeLoader'; import { decodeNapiKernelError } from './SeaErrorMapping'; -import { buildSeaConnectionOptions, SeaNativeConnectionOptions } from './SeaAuth'; +import { buildSeaConnectionOptions, buildSeaRetryOptions, SeaNativeConnectionOptions } from './SeaAuth'; import { installKernelLogBridge } from './SeaLogging'; import SeaSessionBackend from './SeaSessionBackend'; @@ -85,7 +85,14 @@ export default class SeaBackend implements IBackend { // Validate PAT auth + capture the napi-binding option shape. // Any non-PAT mode (or a missing/empty token) throws here, before // we ever touch the native binding. - this.nativeOptions = buildSeaConnectionOptions(options); + // Forward the driver's retry config to the kernel, which owns the retry + // loop on the SEA path. This keeps SEA and Thrift governed by one retry + // config (the same `ClientConfig` knobs the Thrift `HttpRetryPolicy` reads), + // converted from the connector's milliseconds to the kernel's whole seconds. + this.nativeOptions = { + ...buildSeaConnectionOptions(options), + ...buildSeaRetryOptions(this.context.getConfig()), + }; // Bridge the Rust kernel's `tracing` logs into the SAME `DBSQLLogger` the // driver logs through, so logs from all three layers (driver, napi shim, diff --git a/native/sea/index.d.ts b/native/sea/index.d.ts index 7232a947..3413e141 100644 --- a/native/sea/index.d.ts +++ b/native/sea/index.d.ts @@ -350,6 +350,34 @@ export interface ConnectionOptions { * `User-Agent` entry here. */ customHeaders?: Array + /** + * Retry/backoff tuning — all optional. An unset field keeps the kernel's + * built-in policy (1s/60s exponential backoff, 6 total attempts, 900s + * budget). Mirrors the pyo3 binding's `retry_*` kwargs so the Node.js + * driver can forward the same retry knobs the Python connector does. + * + * Lower bound of the exponential backoff (also clamps a server + * `Retry-After`). Maps onto [`HttpConfig::retry_min_wait`]. + */ + retryMinWaitSecs?: number + /** + * Upper bound of the exponential backoff. Maps onto + * [`HttpConfig::retry_max_wait`]. + */ + retryMaxWaitSecs?: number + /** + * **Total** number of attempts (matching the connector's + * `_retry_stop_after_attempts_count` and JDBC count semantics). The + * kernel's [`HttpConfig::retry_max_retries`] counts retries *after* the + * first attempt, so this is converted with `max(0, attempts - 1)` in + * [`build_http_config`] — `0` / `1` both mean a single attempt, no retry. + */ + retryMaxAttempts?: number + /** + * Overall retry budget in whole seconds. Maps onto + * [`HttpConfig::overall_timeout`]. + */ + retryOverallTimeoutSecs?: number } /** * Open a Databricks SQL session and return an opaque `Connection` diff --git a/tests/unit/sea/connectionOptions.test.ts b/tests/unit/sea/connectionOptions.test.ts index 4869bd16..cfbc4b0b 100644 --- a/tests/unit/sea/connectionOptions.test.ts +++ b/tests/unit/sea/connectionOptions.test.ts @@ -13,7 +13,7 @@ // limitations under the License. import { expect } from 'chai'; -import { buildSeaConnectionOptions, buildSeaTlsOptions } from '../../../lib/sea/SeaAuth'; +import { buildSeaConnectionOptions, buildSeaTlsOptions, buildSeaRetryOptions } from '../../../lib/sea/SeaAuth'; import { ConnectionOptions } from '../../../lib/contracts/IDBSQLClient'; import HiveDriverError from '../../../lib/errors/HiveDriverError'; @@ -21,7 +21,7 @@ const PAT = { host: 'h.databricks.com', path: '/sql/1.0/warehouses/abc', token: // Cast helper: the SEA connection-tuning/TLS options live on the internal // surface, so tests build untyped option literals. -const opts = (extra: Record) => ({ ...PAT, ...extra } as unknown as ConnectionOptions); +const opts = (extra: Record) => ({ ...PAT, ...extra }) as unknown as ConnectionOptions; describe('SeaAuth connection options — intervalsAsString default', () => { it('always sets intervalsAsString:true (thrift-compatible interval rendering)', () => { @@ -119,3 +119,43 @@ describe('SeaAuth TLS options (buildSeaTlsOptions)', () => { expect(native.checkServerCertificate).to.equal(false); }); }); + +describe('SeaAuth retry options — buildSeaRetryOptions', () => { + // The driver's ClientConfig retry defaults (ms / total-attempt count). + const defaults = { + retryMaxAttempts: 5, + retriesTimeout: 15 * 60 * 1000, + retryDelayMin: 1000, + retryDelayMax: 60 * 1000, + }; + + it('converts the connector ms knobs to the kernel whole-second kwargs', () => { + const r = buildSeaRetryOptions(defaults); + expect(r.retryMinWaitSecs).to.equal(1); // 1000ms + expect(r.retryMaxWaitSecs).to.equal(60); // 60000ms + expect(r.retryOverallTimeoutSecs).to.equal(900); // 15min + }); + + it('passes retryMaxAttempts through as a TOTAL attempt count (kernel converts to retries)', () => { + expect(buildSeaRetryOptions({ ...defaults, retryMaxAttempts: 5 }).retryMaxAttempts).to.equal(5); + expect(buildSeaRetryOptions({ ...defaults, retryMaxAttempts: 0 }).retryMaxAttempts).to.equal(0); + }); + + it('rounds sub-second delays to the nearest second (kernel granularity)', () => { + const r = buildSeaRetryOptions({ ...defaults, retryDelayMin: 1500, retryDelayMax: 2400 }); + expect(r.retryMinWaitSecs).to.equal(2); // 1.5s → 2 + expect(r.retryMaxWaitSecs).to.equal(2); // 2.4s → 2 + }); + + it('clamps negative/garbage inputs into the napi u32 range', () => { + const r = buildSeaRetryOptions({ + retryMaxAttempts: -3, + retriesTimeout: -1, + retryDelayMin: -1000, + retryDelayMax: 0, + }); + expect(r.retryMaxAttempts).to.equal(0); + expect(r.retryMinWaitSecs).to.equal(0); + expect(r.retryOverallTimeoutSecs).to.equal(0); + }); +});