Skip to content

Statement control

Tags are JSON objects Hardcharger™ records with every session and statement in its instrumentation tables: who ran it, for which job, for which customer. Set them in SQL, from a driver, or per request (Warp-Query-Tag):

SET hardcharger.session_tag = '{"app": "nightly-etl", "team": "data-eng"}';
SET hardcharger.query_tag = '{"job": "load-orders", "run": 42}';
CREATE TABLE orders AS SELECT range AS id FROM range(1000);
SET hardcharger.query_tag = '';
Terminal window
$ export HARDCHARGER_PASSWORD='change-me'
$ for i in $(seq 50); do [ "$(hardcharger --host localhost --tls=false --username hardcharger --quiet --csv \
--command "SELECT count(*) FROM _hardcharger_instr.sql_statements WHERE query_tag IS NOT NULL" | tail -1)" = 2 ] && break; sleep 0.1; done
$ hardcharger --host localhost --tls=false --username hardcharger --csv --command \
"SELECT s.query_tag->>'job' AS job, s.sql_text FROM _hardcharger_instr.sql_statements s WHERE s.query_tag IS NOT NULL"
job,sql_text
load-orders,CREATE TABLE orders AS SELECT range AS id FROM range(1000)
load-orders,SET hardcharger.query_tag = ''

The statement that clears the tag still carries it. From Python, connect(session_tag=…, query_tag=…) and conn.set_query_tag(tag=…); from the CLI, --session-tag and --query-tag. A tag that isn’t a JSON object is refused. Over plain HTTP, the Warp-Query-Tag header tags one statement or upload; it must be a JSON object written in ASCII (other characters as \u escapes), at most 4,096 bytes, or the request gets 400 bad_request:

Terminal window
$ H='Content-Type: application/json'
$ TOKEN=$(curl -s localhost:31337/warp/v1/auth/token -H "$H" \
-d '{"grant_type": "password", "username": "hardcharger", "password": "change-me"}' | jq -r .access_token)
$ URL=localhost:31337/warp/v1/sessions/$(curl -s localhost:31337/warp/v1/sessions -H "Authorization: Bearer $TOKEN" -H "$H" -d '{}' | jq -r .session_id)
$ curl -s $URL/statements -H "Authorization: Bearer $TOKEN" -H "$H" -H 'Warp-Query-Tag: {"job": "caf\u00e9"}' \
-d '{"sql": "SELECT 1 AS one"}' | jq -r .state
succeeded
$ curl -s $URL/statements -H "Authorization: Bearer $TOKEN" -H "$H" -H 'Warp-Query-Tag: {"job": "café"}' \
-d '{"sql": "SELECT 1 AS one"}' | jq -c '{status, code, detail}'
{"status":400,"code":"bad_request","detail":"Warp-Query-Tag must be ASCII (write other characters as \\u escapes)"}

Three places set a statement’s time limit; the most specific wins:

Where Scope
query_timeout_seconds The server’s default (0: none)
SET [SESSION | GLOBAL] hardcharger.query_timeout = <seconds> A session, or every session
Warp-Statement-Timeout: <ms> (JDBC setQueryTimeout, ADBC adbc.warp.statement.timeout_seconds) One statement
from adbc_driver_hardcharger import dbapi as hardcharger
with hardcharger.connect(uri="hardcharger://localhost?tls=false", username="hardcharger", password="change-me",
settings={"query_timeout": 1}) as conn:
with conn.cursor() as cur:
try:
cur.execute(operation="SELECT count(*) FROM range(1000000000000) a")
cur.fetchall()
except hardcharger.Error as e:
print("statement_timeout" in str(e))
True

A statement that times out ends cancelled with error code statement_timeout (status 409, with cancel_reason: timeout). The server stops it in DuckDB; it doesn’t just stop waiting.

Every client cancels on the server: Ctrl-C in the CLI and in Python (cur.adbc_cancel() from another thread), Statement.cancel() in JDBC, a Go context ending. A client that hangs up in the middle of an unretained result cancels it too. A statement that changes data and has started writing runs to the end rather than being undone because its reader left.

An administrator can end any other session at once. hardcharger_sessions() lists them (users see only their own):

from adbc_driver_hardcharger import dbapi as hardcharger
def connect():
return hardcharger.connect(uri="hardcharger://localhost?tls=false", username="hardcharger", password="change-me")
with connect() as victim, connect() as admin:
session = victim.current_session()
with admin.cursor() as cur:
cur.execute(operation=f"KILL SESSION '{session}'")
print(cur.fetchall())
try:
with victim.cursor() as cur:
cur.execute(operation="SELECT 1")
except Exception as e:
print("session_ended" in str(e))
…
True

The killed session’s next request gets 401 session_ended with reason killed, and its running statement stops. KILL SESSION needs the administrator; you can’t kill your own session.

max_sessions caps non-administrator sessions; past it, opening a session fails with 429 too_many_sessions. Administrators are never capped, so you can always get in to look. session_idle_timeout (15 minutes) evicts sessions with no request and nothing running.

DuckDB runs each statement on many threads; a hundred heavy statements at once just fight over the cores and memory. The queue lets a fixed number run and holds the rest:

Option Default Meaning
max_concurrent_statements 0 Statements that run at once; more wait in the queue. 0: no queue.
max_queued_statements -1 Statements that may wait. -1: 8 × the concurrency; 0: unbounded.
max_queue_wait -1 Seconds a statement may wait. -1: 300; 0: forever.
admin_bypass_queue_default true Administrators skip the queue unless they SET hardcharger.bypass_queue = false.

A statement that finds the queue full, or waits longer than max_queue_wait, fails with 429 rate_limited and a message that says which, so a client can retry. Queued statements show queued in their statement document and in instrumentation, which records how long each waited. An administrator can change the limits live:

SET GLOBAL hardcharger.max_concurrent_statements = 4;
SET GLOBAL hardcharger.max_queue_wait = 60;
Terminal window
$ HARDCHARGER_PASSWORD='change-me' hardcharger --host localhost --tls=false --username hardcharger --csv --command \
"SELECT name, value FROM hardcharger_settings() WHERE name IN ('hardcharger.max_concurrent_statements', 'hardcharger.max_queue_wait') ORDER BY name"
name,value
hardcharger.max_concurrent_statements,4
hardcharger.max_queue_wait,60

RESET GLOBAL returns a limit to the value the server started with.

On the first SIGTERM or SIGINT, Hardcharger stops accepting new sessions and statements (503 server_draining, with Retry-After), reports not_serving on its health endpoint so load balancers move traffic away, and lets running statements finish for up to shutdown_grace_period_seconds (300). Then it cancels what is left. A second signal stops it at once. With graceful_shutdown=false, the first signal stops it at once. Both can be changed live with SET GLOBAL.

Stopping at once closes every session: running statements are cancelled, result streams are cut (clients see a failed stream, never a short one that looks complete), and a bulk upload whose body is still arriving is aborted and rolls back, partitioned uploads included. Nothing is half loaded, and the server exits with status 0.

In Kubernetes, set terminationGracePeriodSeconds a little above the grace period (guide).