Skip to content

Commit e0928cc

Browse files
EDsCODEclaude
andcommitted
Add statement timeout support
Adds query_timeout configuration option that cancels queries exceeding the specified duration. Prevents runaway queries from blocking connections indefinitely. Changes: - Add QueryTimeout to server.Config struct - Add query_timeout to YAML config and DUCKGRES_QUERY_TIMEOUT env var - Add queryContext() helper returning context with timeout - Update all db.Query/db.Exec calls to use context variants - Return SQLSTATE 57014 (query_canceled) on timeout - Add duckgres_query_timeouts_total Prometheus metric - Add PLAN.md to .gitignore for local planning docs Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 08e8094 commit e0928cc

File tree

4 files changed

+209
-32
lines changed

4 files changed

+209
-32
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,6 @@ Thumbs.db
2727
*.test
2828
*.out
2929
coverage.html
30+
31+
# Local planning docs
32+
PLAN.md

main.go

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@ import (
1818

1919
// FileConfig represents the YAML configuration file structure
2020
type FileConfig struct {
21-
Host string `yaml:"host"`
22-
Port int `yaml:"port"`
23-
DataDir string `yaml:"data_dir"`
24-
TLS TLSConfig `yaml:"tls"`
25-
Users map[string]string `yaml:"users"`
26-
RateLimit RateLimitFileConfig `yaml:"rate_limit"`
27-
Extensions []string `yaml:"extensions"`
28-
DuckLake DuckLakeFileConfig `yaml:"ducklake"`
21+
Host string `yaml:"host"`
22+
Port int `yaml:"port"`
23+
DataDir string `yaml:"data_dir"`
24+
TLS TLSConfig `yaml:"tls"`
25+
Users map[string]string `yaml:"users"`
26+
RateLimit RateLimitFileConfig `yaml:"rate_limit"`
27+
Extensions []string `yaml:"extensions"`
28+
DuckLake DuckLakeFileConfig `yaml:"ducklake"`
29+
QueryTimeout string `yaml:"query_timeout"` // e.g., "30s", "5m"
2930
}
3031

3132
type TLSConfig struct {
@@ -109,12 +110,13 @@ func main() {
109110
fmt.Fprintf(os.Stderr, "Options:\n")
110111
flag.PrintDefaults()
111112
fmt.Fprintf(os.Stderr, "\nEnvironment variables:\n")
112-
fmt.Fprintf(os.Stderr, " DUCKGRES_CONFIG Path to YAML config file\n")
113-
fmt.Fprintf(os.Stderr, " DUCKGRES_HOST Host to bind to (default: 0.0.0.0)\n")
114-
fmt.Fprintf(os.Stderr, " DUCKGRES_PORT Port to listen on (default: 5432)\n")
115-
fmt.Fprintf(os.Stderr, " DUCKGRES_DATA_DIR Directory for DuckDB files (default: ./data)\n")
116-
fmt.Fprintf(os.Stderr, " DUCKGRES_CERT TLS certificate file (default: ./certs/server.crt)\n")
117-
fmt.Fprintf(os.Stderr, " DUCKGRES_KEY TLS private key file (default: ./certs/server.key)\n")
113+
fmt.Fprintf(os.Stderr, " DUCKGRES_CONFIG Path to YAML config file\n")
114+
fmt.Fprintf(os.Stderr, " DUCKGRES_HOST Host to bind to (default: 0.0.0.0)\n")
115+
fmt.Fprintf(os.Stderr, " DUCKGRES_PORT Port to listen on (default: 5432)\n")
116+
fmt.Fprintf(os.Stderr, " DUCKGRES_DATA_DIR Directory for DuckDB files (default: ./data)\n")
117+
fmt.Fprintf(os.Stderr, " DUCKGRES_CERT TLS certificate file (default: ./certs/server.crt)\n")
118+
fmt.Fprintf(os.Stderr, " DUCKGRES_KEY TLS private key file (default: ./certs/server.key)\n")
119+
fmt.Fprintf(os.Stderr, " DUCKGRES_QUERY_TIMEOUT Maximum query execution time (e.g., 30s, 5m)\n")
118120
fmt.Fprintf(os.Stderr, "\nPrecedence: CLI flags > environment variables > config file > defaults\n")
119121
}
120122

@@ -194,6 +196,15 @@ func main() {
194196
cfg.Extensions = fileCfg.Extensions
195197
}
196198

199+
// Apply query timeout config
200+
if fileCfg.QueryTimeout != "" {
201+
if d, err := time.ParseDuration(fileCfg.QueryTimeout); err == nil {
202+
cfg.QueryTimeout = d
203+
} else {
204+
slog.Warn("Invalid query_timeout duration: " + err.Error())
205+
}
206+
}
207+
197208
// Apply DuckLake config
198209
if fileCfg.DuckLake.MetadataStore != "" {
199210
cfg.DuckLake.MetadataStore = fileCfg.DuckLake.MetadataStore
@@ -282,6 +293,13 @@ func main() {
282293
if v := os.Getenv("DUCKGRES_DUCKLAKE_S3_PROFILE"); v != "" {
283294
cfg.DuckLake.S3Profile = v
284295
}
296+
if v := os.Getenv("DUCKGRES_QUERY_TIMEOUT"); v != "" {
297+
if d, err := time.ParseDuration(v); err == nil {
298+
cfg.QueryTimeout = d
299+
} else {
300+
slog.Warn("Invalid DUCKGRES_QUERY_TIMEOUT: " + err.Error())
301+
}
302+
}
285303

286304
// Apply CLI flags (highest priority)
287305
if *host != "" {

0 commit comments

Comments
 (0)