jdbc4py — Operator Guide¶
This guide covers production deployment, security hardening, and operational configuration for jdbc4py. It is intended for infrastructure engineers and platform operators rather than application developers.
1. JDBC4PY_DRIVER_PATHS Security Model¶
What the variable does¶
JDBC4PY_DRIVER_PATHS is an operator-controlled environment variable that
extends the JDBC driver search path. It accepts a colon-separated list (on
Unix) or semicolon-separated list (on Windows) of filesystem locations. Each
entry is treated as either:
- A directory — jdbc4py scans it for
*.jarfiles matching the target engine's glob patterns (e.g.postgresql-*.jar). - A single
.jarfile — jdbc4py adds it to the JVM classpath directly.
# Unix
export JDBC4PY_DRIVER_PATHS=/opt/jdbc-drivers:/opt/custom/myengine-2.0.jar
# Windows
set JDBC4PY_DRIVER_PATHS=C:\jdbc-drivers;C:\custom\myengine-2.0.jar
Why this variable is privileged¶
Every path in JDBC4PY_DRIVER_PATHS is loaded unconditionally onto the JVM
classpath at startup. This is semantically equivalent to granting the process
java -cp <your-jar> permissions. An attacker who can write to this variable
can execute arbitrary code inside the JVM process by supplying a crafted JAR.
Treat JDBC4PY_DRIVER_PATHS as a root-equivalent configuration value.
Validation rules¶
jdbc4py validates every entry before adding it to the search path. Entries
that fail validation are silently skipped with a WARNING-level log
message; they never raise an exception (to preserve availability). Log the
startup output and alert on unexpected JDBC4PY_DRIVER_PATHS warnings.
| Rule | Reason |
|---|---|
| Entry must be an absolute path | Relative paths are resolved against the working directory, which may be attacker-controlled in some deployment models. |
| Entry must exist on disk at startup | A path that does not exist may be a sign of misconfiguration or a placeholder left by a failed deployment. |
If entry is a file, it must end in .jar |
Only Java Archive files are valid classpath entries. Rejecting other file types prevents accidental or malicious classpath poisoning with non-JAR payloads. |
Entry must not contain glob metacharacters (*, ?, [, ]) |
Glob expansion is never applied to this variable — metacharacters indicate a misconfiguration or injection attempt. |
Example log messages emitted for rejected entries:
WARNING jdbc4py.drivers: JDBC4PY_DRIVER_PATHS entry '/opt/*.jar' contains glob
metacharacters and was rejected for security reasons.
WARNING jdbc4py.drivers: JDBC4PY_DRIVER_PATHS entry 'relative/path' is not an
absolute path and was rejected. Use an absolute path.
WARNING jdbc4py.drivers: JDBC4PY_DRIVER_PATHS entry '/missing/driver.jar' does
not exist and was skipped.
WARNING jdbc4py.drivers: JDBC4PY_DRIVER_PATHS entry '/scripts/setup.sh' is a
file but does not end in .jar and was rejected.
Deployment guidance¶
Container / Kubernetes¶
Set the variable at the infrastructure layer, not in application code:
# Kubernetes Deployment
env:
- name: JDBC4PY_DRIVER_PATHS
valueFrom:
secretKeyRef:
name: jdbc-driver-config
key: driver_paths
Avoid allowing application pods to write to the environment of sibling pods.
In multi-tenant clusters, ensure that no tenant workload can set or override
JDBC4PY_DRIVER_PATHS for another tenant's process.
Systemd service¶
Do not use EnvironmentFile= pointing to a user-writable path.
CI / test environments¶
If you supply custom JARs in tests via JDBC4PY_DRIVER_PATHS, use absolute
paths in your CI matrix configuration and never derive them from user input.
Monitoring¶
Configure your log aggregation to alert on:
Any warning here at startup indicates either a misconfiguration that should be corrected before the process handles traffic, or an attempted injection that should trigger a security incident.
2. CLOB / Large LOB Configuration¶
The cap¶
The Java helper (JdbcBatchReader/JdbcArrowReader) imposes an upper bound on
the number of characters read from a CLOB or large text column per row. The
default is 64 million characters (≈128 MB UTF-16 on Java heap).
If a CLOB value exceeds the cap, it is silently truncated and a one-time
WARNING is emitted to the JVM log:
jdbc4py CLOB truncation: value exceeded <N> chars; truncating. Set
-Djdbc4py.clob.max_chars=<N> to adjust. (further truncations will not be
logged)
Adjusting the cap¶
Pass the JVM system property at JVM startup. The JVM can only be started once
per process, so this must be set before the first jdbc4py.connect() call.
import jpype
import jpype.imports
# Start JVM with a higher cap (256 M chars ≈ 512 MB heap)
jpype.startJVM(
"-Djdbc4py.clob.max_chars=268435456",
)
import jdbc4py
conn = jdbc4py.connect(engine="oracle", ...)
Alternatively, export the property via _JAVA_OPTIONS (affects all JVM
processes — use carefully in production):
Capacity planning¶
| Cap (chars) | Approximate Java heap per CLOB column per row |
|---|---|
| 64 M (default) | ~128 MB |
| 16 M | ~32 MB |
| 256 M | ~512 MB |
For batch reads (read_sql_arrow), the cap applies per cell, not per
batch. With batch_size=1000 rows and a 64 M-char cap, the worst-case
allocation is 1000 × 128 MB = 128 GB — this would OOM. If you are reading
wide CLOB tables, lower the cap or reduce the batch size.
3. Connection Pool Tuning¶
Key parameters¶
| Parameter | Default | Description |
|---|---|---|
min_size |
1 | Connections opened at startup and kept warm |
max_connections |
— | Hard cap on total connections; takes precedence over max_size |
max_size |
10 | Alias for max_connections |
max_idle_seconds |
300 | Evict a connection idle longer than this |
max_age_seconds |
3600 | Recycle a connection older than this |
checkout_timeout |
30 s | How long a thread waits for a connection before raising OperationalError |
validate_on_checkout |
True |
Run a ping query before handing out a connection |
validate_on_return |
False |
Run a ping query when a connection is returned to the pool |
Long-lived processes¶
For services that run for days or weeks, use max_age_seconds to recycle
connections before they go stale (validate_on_checkout is already on by
default):
pool = jdbc4py.ConnectionPool(
engine="postgres",
host="db.prod",
database="analytics",
max_connections=10,
max_age_seconds=1800, # recycle after 30 minutes
validate_on_checkout=True, # ping before use (default)
)
Circuit breaker¶
The pool maintains a circuit breaker that opens after repeated connection
failures. While open, checkout attempts fail fast with OperationalError
and only one recovery probe is let through per cooldown window (the cooldown
doubles on each consecutive failure, up to 30 s). The breaker resets once a
connection opens successfully. Monitor for circuit-breaker log messages.
4. JVM Thread Management¶
Thread attachment¶
Every Python thread that makes a JDBC call must be attached to the JVM. jdbc4py handles this automatically for:
- Threads created by
AsyncQuery(viaThreadPoolExecutor) - The deferred-close daemon thread spawned by
AsyncQuery.close()when cancellation times out Connection.__del__(on GC finalizer threads)
If you spawn raw Python threads that call jdbc4py APIs directly, the JPype
library will attempt auto-attachment. If it does not (older JPype versions),
you will see a RuntimeError: Failed to attach thread to JVM. Upgrade to
JPype ≥ 1.5 or attach manually:
import jpype
from jdbc4py.jvm import JVMManager
JVMManager._attach_thread(jpype)
try:
with conn.cursor() as cur:
cur.execute("select 1")
finally:
JVMManager._detach_thread(jpype)
GC finalizer safety¶
Connection.__del__ and AsyncQuery.__del__ are designed to be safe on GC
finalizer threads: they check jpype.isJVMStarted(), attach to the JVM if
needed, and silently skip cleanup if attachment fails rather than risking a
SIGSEGV from an unattached JNI call.
Do not rely on __del__ for deterministic cleanup. Always use
conn.close() or the context-manager (with jdbc4py.connect(...) as conn:).
5. Credential Handling¶
Automatic redaction¶
All exception messages and log entries pass through redact_jdbc_url(), which
replaces credential values with *** before they leave the jdbc4py boundary.
The following key names are redacted:
password, passwd, pwd, secret, token, credential, apikey,
api_key, authtoken, auth_token, bearer_token, oauth_token,
access_token, refresh_token, client_secret, app_secret,
private_key, access_key, ssl_key, ssl_cert,
aws_secret_access_key, aws_access_key_id,
db_password, db_passwd, connection_password
JDBC URL credentials¶
Never embed credentials directly in a JDBC URL passed to a log aggregator.
jdbc4py redacts them from exception messages, but the raw URL is stored in
ResolvedDriver.jdbc_url and may appear in tracebacks or repr() output from
third-party exception handlers.
Prefer credential-free URLs with a JDBC Properties object or your database's
IAM/Kerberos integration.
6. Observability Checklist¶
Enabling logging¶
jdbc4py logs through the standard library logging module under the jdbc4py
namespace (jdbc4py.connection, jdbc4py.pool, jdbc4py.jvm, …). Following
library convention, it attaches a NullHandler to the top-level jdbc4py
logger, so it emits nothing until your application configures logging:
import logging
logging.basicConfig(level=logging.INFO) # your app's handlers
logging.getLogger("jdbc4py").setLevel(logging.DEBUG) # turn jdbc4py up/down
Levels: DEBUG covers connection lifecycle, query timing, pool checkout/return,
and JVM thread attach/detach; WARNING covers operational problems worth
alerting on (below). jdbc4py never calls basicConfig() or attaches stream
handlers itself — handler and level policy stay entirely with your application.
Alert on these log patterns in production:
| Logger | Level | Pattern | Meaning |
|---|---|---|---|
jdbc4py.drivers |
WARNING |
JDBC4PY_DRIVER_PATHS |
Invalid driver path entry — misconfiguration or injection attempt |
jdbc4py.pool |
WARNING |
eviction thread did not stop |
Pool shutdown hung — investigate connection leak |
jdbc4py.async_query |
WARNING |
deferred cursor close failed |
Background cleanup error after query cancellation |
jdbc4py.async_query |
WARNING |
query still running after |
Query did not respond to cancel within 300 s |
jdbc4py.connection |
WARNING |
setQueryTimeout |
Driver rejected timeout hint — queries may run unbounded |
Java jdbc4py logger |
WARNING |
CLOB truncation |
CLOB data was silently truncated — adjust jdbc4py.clob.max_chars |