DestroySession closes and removes a session.
(token string)
| 1204 | // ConfigureMainDB sets these once at warmup, but in cluster mode the |
| 1205 | // per-session evictConnFromPool call discards the settings along with |
| 1206 | // the conn — so for every session after the first we need to re-apply. |
| 1207 | // DuckDB rejects `SET GLOBAL` for these keys, so this is the only path. |
| 1208 | server.ApplyProfilingSettings(context.Background(), conn) |
| 1209 | |
| 1210 | // Apply initial resource limits if provided (optimizes handshake by avoiding |
| 1211 | // roundtrips from control plane). |
| 1212 | if memoryLimit != "" { |
| 1213 | if _, err := conn.ExecContext(context.Background(), fmt.Sprintf("SET memory_limit = '%s'", memoryLimit)); err != nil { |
| 1214 | slog.Warn("Failed to set initial memory_limit.", "user", username, "error", err) |
| 1215 | } |
| 1216 | } |
| 1217 | if threads > 0 { |
| 1218 | if _, err := conn.ExecContext(context.Background(), fmt.Sprintf("SET threads = %d", threads)); err != nil { |
| 1219 | slog.Warn("Failed to set initial threads.", "user", username, "error", err) |
| 1220 | } |
| 1221 | } |
| 1222 | |
| 1223 | // User-secret hygiene + replay (shared-warm / k8s mode only). DuckDB |
| 1224 | // secrets are instance-global, so a hot-idle worker reused by a different |
| 1225 | // user of the same org would otherwise see the previous user's secrets — |
| 1226 | // both persistent ones and non-persistent (plain/TEMPORARY CREATE SECRET) |
| 1227 | // ones that pass through to the worker. Wipe ALL user secrets first |
| 1228 | // (mandatory — this is the cross-user isolation step), then replay this |
| 1229 | // user's secrets from the control plane. Replay failures degrade to |
| 1230 | // warnings; a wipe failure fails the session because handing user A's |
| 1231 | // secrets to user B is not acceptable. |
| 1232 | var secretWarnings []string |
| 1233 | switch { |
| 1234 | case p.sharedWarmMode && p.maxSessions != 1: |
| 1235 | // The wipe and replay below are only safe because exactly one session |
| 1236 | // runs at a time (DuckDB secrets are instance-global; k8s workers are |
| 1237 | // spawned with DUCKGRES_DUCKDB_MAX_SESSIONS=1). Assert that here, at |
| 1238 | // the point of reliance, instead of trusting a flag set two layers |
| 1239 | // away: with any other cap, wiping would delete a concurrent |
| 1240 | // session's secrets mid-query. Skip hygiene entirely and scream. |
| 1241 | slog.Error("User secret hygiene requires max_sessions=1 on shared-warm workers; skipping wipe+replay.", |
| 1242 | "max_sessions", p.maxSessions, "user", username, "secrets", len(secretStatements)) |
| 1243 | if len(secretStatements) > 0 { |
| 1244 | secretWarnings = []string{"persistent secrets were NOT restored: worker session cap is misconfigured (requires max_sessions=1)"} |
| 1245 | } |
| 1246 | case p.sharedWarmMode: |
| 1247 | secretCtx, secretCancel := context.WithTimeout(context.Background(), userSecretOpTimeout) |
| 1248 | wiped, wipeErr := wipeUserSecrets(secretCtx, conn) |
| 1249 | if wipeErr != nil { |
| 1250 | secretCancel() |
| 1251 | _ = conn.Close() |
| 1252 | p.mu.Lock() |
| 1253 | p.reserved-- |
| 1254 | p.mu.Unlock() |
| 1255 | return nil, nil, fmt.Errorf("wipe user secrets before session start: %w", wipeErr) |
| 1256 | } |
| 1257 | if len(wiped) > 0 { |
| 1258 | slog.Info("Wiped user secrets left by previous session.", "user", username, "count", len(wiped)) |
| 1259 | } |
| 1260 | // Same isolation step for client-ATTACHed databases: they are |
| 1261 | // instance-global too, and an attached catalog keeps the credentials |
| 1262 | // it was built from even after the secret above is gone. Mandatory for |
| 1263 | // the same reason as the wipe — see detachUserCatalogs. |