Skip to content

Commit 08cbcfe

Browse files
fix(kernel): never return ErrBadConn on the execute/read path (H1)
toDriverError mapped a NetworkError/Unavailable status to driver.ErrBadConn on every path, including execute and result-read. On the statement path that is unsafe: a network failure surfaced *after* the SEA statement was sent may have committed server-side, and driver.ErrBadConn makes database/sql transparently re-run the whole statement — a silent duplicate write for a non-idempotent INSERT/UPDATE/MERGE. This violates Go's own ErrBadConn rule ("never return ErrBadConn if the server might have performed the operation"). Split classification by path: - toConnError (session lifecycle: open/close/config — nothing executed) keeps the bad-conn mapping so the pool evicts a dead session. Safe: no statement ran. - toStatementError (execute + result read) never returns ErrBadConn; it returns the KernelError unchanged (sqlstate preserved). This restores the contract the rest of the stack already honors: the kernel itself classifies ExecuteStatement as NonIdempotent (retried only on connect-phase failures, per src/client/retry.rs), and the Thrift backend marks ExecuteStatement non-retryable. The kernel has already exhausted its safe internal retries by the time Go sees the error, so a second database/sql-level retry was pure hazard. Tests: TestToStatementErrorNeverBadConn asserts no ErrBadConn for network/unavailable/unauthenticated on the statement path; TestToConnError keeps the session-path eviction behavior. Co-authored-by: Isaac Signed-off-by: Mani Kaustubh Mathur <mani.mathur@databricks.com>
1 parent f276304 commit 08cbcfe

5 files changed

Lines changed: 82 additions & 33 deletions

File tree

internal/backend/kernel/backend.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ func (k *KernelBackend) OpenSession(ctx context.Context) error {
8383

8484
var cfg *C.KernelSessionConfig
8585
if err := call(func() C.KernelStatusCode { return C.kernel_session_config_new(&cfg) }); err != nil {
86-
return fmt.Errorf("kernel: config_new: %w", toDriverError(err))
86+
return fmt.Errorf("kernel: config_new: %w", toConnError(err))
8787
}
8888
// kernel_session_open consumes the config on EVERY path — success and
8989
// failure alike (it reclaims the box up front). So we free the config
@@ -107,15 +107,15 @@ func (k *KernelBackend) OpenSession(ctx context.Context) error {
107107
if err := call(func() C.KernelStatusCode {
108108
return C.kernel_session_config_set_warehouse(cfg, host.c, wh.c)
109109
}); err != nil {
110-
return fmt.Errorf("kernel: set_warehouse: %w", toDriverError(err))
110+
return fmt.Errorf("kernel: set_warehouse: %w", toConnError(err))
111111
}
112112
} else {
113113
path := newCStr(k.cfg.HTTPPath)
114114
defer path.free()
115115
if err := call(func() C.KernelStatusCode {
116116
return C.kernel_session_config_set_http_path(cfg, host.c, path.c)
117117
}); err != nil {
118-
return fmt.Errorf("kernel: set_http_path: %w", toDriverError(err))
118+
return fmt.Errorf("kernel: set_http_path: %w", toConnError(err))
119119
}
120120
}
121121

@@ -124,7 +124,7 @@ func (k *KernelBackend) OpenSession(ctx context.Context) error {
124124
if err := call(func() C.KernelStatusCode {
125125
return C.kernel_session_config_set_auth_pat(cfg, tok.c)
126126
}); err != nil {
127-
return fmt.Errorf("kernel: set_auth_pat: %w", toDriverError(err))
127+
return fmt.Errorf("kernel: set_auth_pat: %w", toConnError(err))
128128
}
129129

130130
// TLS: crypto/tls's InsecureSkipVerify accepts any server cert, so relax both
@@ -135,12 +135,12 @@ func (k *KernelBackend) OpenSession(ctx context.Context) error {
135135
if err := call(func() C.KernelStatusCode {
136136
return C.kernel_session_config_set_tls_allow_self_signed(cfg, C.bool(true))
137137
}); err != nil {
138-
return fmt.Errorf("kernel: set_tls_allow_self_signed: %w", toDriverError(err))
138+
return fmt.Errorf("kernel: set_tls_allow_self_signed: %w", toConnError(err))
139139
}
140140
if err := call(func() C.KernelStatusCode {
141141
return C.kernel_session_config_set_tls_skip_hostname_verification(cfg, C.bool(true))
142142
}); err != nil {
143-
return fmt.Errorf("kernel: set_tls_skip_hostname_verification: %w", toDriverError(err))
143+
return fmt.Errorf("kernel: set_tls_skip_hostname_verification: %w", toConnError(err))
144144
}
145145
}
146146

@@ -154,7 +154,7 @@ func (k *KernelBackend) OpenSession(ctx context.Context) error {
154154
if err := call(func() C.KernelStatusCode {
155155
return C.kernel_session_config_set_proxy(cfg, url.c, nil, nil, nil)
156156
}); err != nil {
157-
return fmt.Errorf("kernel: set_proxy: %w", toDriverError(err))
157+
return fmt.Errorf("kernel: set_proxy: %w", toConnError(err))
158158
}
159159
}
160160

@@ -169,7 +169,7 @@ func (k *KernelBackend) OpenSession(ctx context.Context) error {
169169
ck.free()
170170
cv.free()
171171
if errSet != nil {
172-
return fmt.Errorf("kernel: set_session_conf[%s]: %w", key, toDriverError(errSet))
172+
return fmt.Errorf("kernel: set_session_conf[%s]: %w", key, toConnError(errSet))
173173
}
174174
}
175175

@@ -180,7 +180,7 @@ func (k *KernelBackend) OpenSession(ctx context.Context) error {
180180
err := call(func() C.KernelStatusCode { return C.kernel_session_open(cfg, &sess) })
181181
consumed = true
182182
if err != nil {
183-
return fmt.Errorf("kernel: session_open: %w", toDriverError(err))
183+
return fmt.Errorf("kernel: session_open: %w", toConnError(err))
184184
}
185185
k.session = sess
186186
k.valid = true
@@ -201,7 +201,7 @@ func (k *KernelBackend) CloseSession(ctx context.Context) error {
201201
err := call(func() C.KernelStatusCode { return C.kernel_session_close(k.session) })
202202
k.session = nil
203203
k.valid = false
204-
return toDriverError(err)
204+
return toConnError(err)
205205
}
206206

207207
// SessionValid backs conn.IsValid → pool eviction. No I/O; inspects state

internal/backend/kernel/cgo.go

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -202,12 +202,13 @@ func isBadConnection(code int) bool {
202202
}
203203
}
204204

205-
// toDriverError classifies a kernel error for the pool: a status that means the
206-
// session is unusable is wrapped as a bad-connection error (which identifies as
207-
// driver.ErrBadConn, so database/sql evicts the conn), matching how the Thrift
208-
// backend surfaces connection loss. Other kernel errors — and plain
209-
// (non-KernelError) errors — are returned unchanged, carrying their sqlstate.
210-
func toDriverError(err error) error {
205+
// toConnError classifies a kernel error on a SESSION-lifecycle path (open/close/
206+
// config, where nothing has executed): a status that means the session is unusable
207+
// is wrapped as a bad-connection error, which identifies as driver.ErrBadConn so
208+
// database/sql evicts the conn from the pool. Safe here because no statement ran,
209+
// so there is nothing for database/sql to unsafely re-run. Other kernel errors —
210+
// and plain (non-KernelError) errors — are returned unchanged, carrying sqlstate.
211+
func toConnError(err error) error {
211212
if err == nil {
212213
return nil
213214
}
@@ -221,6 +222,28 @@ func toDriverError(err error) error {
221222
return ke
222223
}
223224

225+
// toStatementError classifies a kernel error on the STATEMENT path (execute and
226+
// result read). It NEVER returns driver.ErrBadConn: once a statement has been
227+
// sent, a network/unavailable failure surfaced afterward may have committed
228+
// server-side, and driver.ErrBadConn would make database/sql transparently
229+
// re-run the statement — a silent duplicate write for a non-idempotent
230+
// INSERT/UPDATE/MERGE. This mirrors the kernel's own retry contract
231+
// (ExecuteStatement is NonIdempotent, retried only on connect-phase failures) and
232+
// the Thrift backend (ExecuteStatement is non-retryable), and honors Go's
233+
// driver.ErrBadConn rule ("never return ErrBadConn if the server might have
234+
// performed the operation"). The kernel has already exhausted its safe internal
235+
// retries by the time we see the error. Returns the KernelError (or plain error)
236+
// unchanged, carrying sqlstate.
237+
func toStatementError(err error) error {
238+
if err == nil {
239+
return nil
240+
}
241+
if ke, ok := err.(*KernelError); ok {
242+
return ke
243+
}
244+
return err
245+
}
246+
224247
// cStr wraps C.CString with a guaranteed free. The kernel copies strings into
225248
// owned Rust memory on receipt, so freeing immediately after the call is safe.
226249
// Use: cs := newCStr(s); defer cs.free(); ...C.fn(cs.c)...

internal/backend/kernel/kernel_test.go

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,24 +28,50 @@ func TestIsBadConnection(t *testing.T) {
2828
}
2929
}
3030

31-
// toDriverError wraps a session-unusable KernelError as driver.ErrBadConn (so
32-
// database/sql evicts the conn) and leaves other errors, and their sqlstate,
33-
// intact.
34-
func TestToDriverError(t *testing.T) {
35-
if toDriverError(nil) != nil {
31+
// toConnError (session-lifecycle path) wraps a session-unusable KernelError as
32+
// driver.ErrBadConn so database/sql evicts the conn, and leaves other errors and
33+
// their sqlstate intact.
34+
func TestToConnError(t *testing.T) {
35+
if toConnError(nil) != nil {
3636
t.Fatal("nil should map to nil")
3737
}
3838

3939
badConn := &KernelError{Code: statusUnavailable, Message: "gone"}
40-
if !errors.Is(toDriverError(badConn), driver.ErrBadConn) {
41-
t.Errorf("unavailable kernel error should identify as driver.ErrBadConn")
40+
if !errors.Is(toConnError(badConn), driver.ErrBadConn) {
41+
t.Errorf("unavailable kernel error on the session path should identify as driver.ErrBadConn")
4242
}
4343

4444
sqlErr := &KernelError{Code: statusSqlError, Message: "boom", SQLState: "42703"}
45-
got := toDriverError(sqlErr)
46-
ke, ok := got.(*KernelError)
45+
ke, ok := toConnError(sqlErr).(*KernelError)
4746
if !ok {
48-
t.Fatalf("sql error should remain a *KernelError, got %T", got)
47+
t.Fatalf("sql error should remain a *KernelError, got %T", toConnError(sqlErr))
48+
}
49+
if ke.SQLState != "42703" {
50+
t.Errorf("sqlstate lost: got %q", ke.SQLState)
51+
}
52+
}
53+
54+
// toStatementError (execute/read path) must NEVER return driver.ErrBadConn — even
55+
// for a network/unavailable status — so database/sql cannot transparently re-run a
56+
// statement that may have already executed server-side (silent duplicate write).
57+
func TestToStatementErrorNeverBadConn(t *testing.T) {
58+
if toStatementError(nil) != nil {
59+
t.Fatal("nil should map to nil")
60+
}
61+
62+
for _, code := range []int{statusUnavailable, statusNetworkError, statusUnauthenticated} {
63+
err := toStatementError(&KernelError{Code: code, Message: "post-execute failure"})
64+
if errors.Is(err, driver.ErrBadConn) {
65+
t.Errorf("statement-path error (code=%d) must NOT identify as driver.ErrBadConn "+
66+
"(would let database/sql re-run a possibly-committed statement)", code)
67+
}
68+
}
69+
70+
// sqlstate still preserved.
71+
sqlErr := &KernelError{Code: statusSqlError, Message: "boom", SQLState: "42703"}
72+
ke, ok := toStatementError(sqlErr).(*KernelError)
73+
if !ok {
74+
t.Fatalf("sql error should remain a *KernelError, got %T", toStatementError(sqlErr))
4975
}
5076
if ke.SQLState != "42703" {
5177
t.Errorf("sqlstate lost: got %q", ke.SQLState)

internal/backend/kernel/operation.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ func (k *KernelBackend) execute(ctx context.Context, req backend.ExecRequest) (b
4040
if err := call(func() C.KernelStatusCode {
4141
return C.kernel_session_new_statement(k.session, &stmt)
4242
}); err != nil {
43-
return &kernelOp{}, fmt.Errorf("kernel: new_statement: %w", toDriverError(err))
43+
return &kernelOp{}, fmt.Errorf("kernel: new_statement: %w", toStatementError(err))
4444
}
4545

4646
sql := newCStr(req.Query)
@@ -49,7 +49,7 @@ func (k *KernelBackend) execute(ctx context.Context, req backend.ExecRequest) (b
4949
}); err != nil {
5050
sql.free()
5151
C.kernel_statement_close(stmt)
52-
return &kernelOp{}, fmt.Errorf("kernel: set_sql: %w", toDriverError(err))
52+
return &kernelOp{}, fmt.Errorf("kernel: set_sql: %w", toStatementError(err))
5353
}
5454
sql.free()
5555

@@ -131,7 +131,7 @@ func (k *KernelBackend) execute(ctx context.Context, req backend.ExecRequest) (b
131131
}
132132
klog("Execute failed: %v", execErr)
133133
op.close()
134-
return op, fmt.Errorf("kernel: execute: %w", toDriverError(execErr))
134+
return op, fmt.Errorf("kernel: execute: %w", toStatementError(execErr))
135135
}
136136
op.exec = exec
137137
// Capture the modified-row count now, while exec is live — the operation is
@@ -187,7 +187,7 @@ func (o *kernelOp) Results(ctx context.Context, callbacks *dbsqlrows.TelemetryCa
187187
// Operation.Close on a Results error — so close the handles here to avoid
188188
// leaking the statement / executed handle (and its server operation).
189189
o.close()
190-
return nil, fmt.Errorf("kernel: get_result_stream: %w", toDriverError(err))
190+
return nil, fmt.Errorf("kernel: get_result_stream: %w", toStatementError(err))
191191
}
192192
return newKernelRows(ctx, o, stream, callbacks)
193193
}
@@ -235,5 +235,5 @@ func (o *kernelOp) close() bool {
235235
// already carries the sqlstate (see KernelError), so this returns cause as-is
236236
// (nil when cause is nil), matching the neutral contract.
237237
func (o *kernelOp) ExecutionError(ctx context.Context, cause error) error {
238-
return toDriverError(cause)
238+
return toStatementError(cause)
239239
}

internal/backend/kernel/rows.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func newKernelRows(ctx context.Context, op *kernelOp, stream *C.kernel_result_st
5959
return C.kernel_result_stream_get_schema(stream, &csch)
6060
}); err != nil {
6161
r.Close()
62-
return nil, fmt.Errorf("kernel: get_schema: %w", toDriverError(err))
62+
return nil, fmt.Errorf("kernel: get_schema: %w", toStatementError(err))
6363
}
6464
sch, err := cdata.ImportCArrowSchema((*cdata.CArrowSchema)(unsafe.Pointer(&csch)))
6565
if err != nil {
@@ -151,7 +151,7 @@ func (r *kernelRows) nextBatch() error {
151151
if err := call(func() C.KernelStatusCode {
152152
return C.kernel_result_stream_next_batch(r.stream, &carr, &csch)
153153
}); err != nil {
154-
return fmt.Errorf("kernel: next_batch: %w", toDriverError(err))
154+
return fmt.Errorf("kernel: next_batch: %w", toStatementError(err))
155155
}
156156
if carr.release == nil {
157157
r.eof = true

0 commit comments

Comments
 (0)