diff --git a/.schema/pgdog.schema.json b/.schema/pgdog.schema.json index c8bb7a87e..576e6858a 100644 --- a/.schema/pgdog.schema.json +++ b/.schema/pgdog.schema.json @@ -409,12 +409,15 @@ }, "idle_timeout": { "description": "Overrides the `idle_timeout` setting. Idle server connections exceeding this timeout will be closed automatically.\n\n", - "type": [ - "integer", - "null" + "anyOf": [ + { + "$ref": "#/$defs/TimeValue" + }, + { + "type": "null" + } ], - "format": "uint64", - "minimum": 0 + "default": null }, "lb_weight": { "description": "Used for weighted load balancing.", @@ -426,12 +429,15 @@ }, "lock_timeout": { "description": "This setting configures the `lock_timeout` connection parameter on all connections to Postgres for this database.\nAborts any statement that waits longer than the specified duration to acquire a lock.\nUnlike `statement_timeout`, this only counts time spent waiting for locks, not execution time.\n\n", - "type": [ - "integer", - "null" + "anyOf": [ + { + "$ref": "#/$defs/TimeValue" + }, + { + "type": "null" + } ], - "format": "uint64", - "minimum": 0 + "default": null }, "min_pool_size": { "description": "Overrides the `min_pool_size` setting. The connection pool will maintain at minimum this many connections.\n\n", @@ -500,21 +506,27 @@ }, "server_lifetime": { "description": "Overrides the `server_lifetime` setting. Server connections older than this will be closed when returned to the pool.\n\n", - "type": [ - "integer", - "null" + "anyOf": [ + { + "$ref": "#/$defs/TimeValue" + }, + { + "type": "null" + } ], - "format": "uint64", - "minimum": 0 + "default": null }, "server_lifetime_jitter": { "description": "Overrides the `server_lifetime_jitter` setting for this database.\n\n", - "type": [ - "integer", - "null" + "anyOf": [ + { + "$ref": "#/$defs/TimeValue" + }, + { + "type": "null" + } ], - "format": "uint64", - "minimum": 0 + "default": null }, "shard": { "description": "The shard number for this database. Only required if your database contains more than one shard. Shard numbers start at 0.\n\n", @@ -525,12 +537,15 @@ }, "statement_timeout": { "description": "This setting configures the `statement_timeout` connection parameter on all connections to Postgres for this database.\n\n", - "type": [ - "integer", - "null" + "anyOf": [ + { + "$ref": "#/$defs/TimeValue" + }, + { + "type": "null" + } ], - "format": "uint64", - "minimum": 0 + "default": null }, "user": { "description": "Name of the PostgreSQL user to connect with when creating backend connections from PgDog to Postgres. If not set, this defaults to `name` in users.toml.\n\n", @@ -546,25 +561,6 @@ "host" ] }, - "Duration": { - "type": "object", - "properties": { - "nanos": { - "type": "integer", - "format": "uint32", - "minimum": 0 - }, - "secs": { - "type": "integer", - "format": "uint64", - "minimum": 0 - } - }, - "required": [ - "secs", - "nanos" - ] - }, "FlexibleType": { "description": "A sharding key value that can be an integer, UUID, or string.", "anyOf": [ @@ -599,10 +595,8 @@ }, "ban_replica_lag": { "description": "Ban a replica from serving read queries if its replication lag (in milliseconds) exceeds this threshold.\n\n", - "type": "integer", - "format": "uint64", - "default": 9223372036854775807, - "minimum": 0 + "$ref": "#/$defs/TimeValue", + "default": 9223372036854775807 }, "ban_replica_lag_bytes": { "description": "Ban a replica from serving read queries if its replication lag (in bytes) exceeds this threshold.\n\n", @@ -613,10 +607,8 @@ }, "ban_timeout": { "description": "Connection pools blocked from serving traffic due to an error will be placed back into active rotation after this long.\n\n_Default:_ `300000`\n\n", - "type": "integer", - "format": "uint64", - "default": 300000, - "minimum": 0 + "$ref": "#/$defs/TimeValue", + "default": 300000 }, "broadcast_address": { "description": "Broadcast IP address used for multi-instance coordination (e.g., schema cache invalidation across nodes).", @@ -636,10 +628,8 @@ }, "checkout_timeout": { "description": "Maximum amount of time a client is allowed to wait for a connection from the pool.\n\n_Default:_ `5000`\n\n", - "type": "integer", - "format": "uint64", - "default": 5000, - "minimum": 0 + "$ref": "#/$defs/TimeValue", + "default": 5000 }, "client_connection_recovery": { "description": "Controls whether to disconnect clients upon encountering connection pool errors.\n\n**Note:** Set this to `drop` if your clients are async / use pipelining mode.\n\n_Default:_ `recover`\n\n", @@ -648,31 +638,23 @@ }, "client_idle_in_transaction_timeout": { "description": "Close client connections that have been idle inside a transaction for this amount of time.\n\n", - "type": "integer", - "format": "uint64", - "default": 9223372036854775807, - "minimum": 0 + "$ref": "#/$defs/TimeValue", + "default": 9223372036854775807 }, "client_idle_timeout": { "description": "Close client connections that have been idle, i.e., haven't sent any queries, for this amount of time.\n\n", - "type": "integer", - "format": "uint64", - "default": 9223372036854775807, - "minimum": 0 + "$ref": "#/$defs/TimeValue", + "default": 9223372036854775807 }, "client_login_timeout": { "description": "Maximum amount of time new clients have to complete authentication.\n\n_Default:_ `60000`\n\n", - "type": "integer", - "format": "uint64", - "default": 60000, - "minimum": 0 + "$ref": "#/$defs/TimeValue", + "default": 60000 }, "connect_attempt_delay": { "description": "Amount of time to wait between connection attempt retries.\n\n_Default:_ `0`\n\n", - "type": "integer", - "format": "uint64", - "default": 0, - "minimum": 0 + "$ref": "#/$defs/TimeValue", + "default": 0 }, "connect_attempts": { "description": "Maximum number of retries for Postgres server connection attempts.\n\n_Default:_ `1`\n\n", @@ -683,10 +665,8 @@ }, "connect_timeout": { "description": "Maximum amount of time to allow for PgDog to create a connection to Postgres.\n\n_Default:_ `5000`\n\n", - "type": "integer", - "format": "uint64", - "default": 5000, - "minimum": 0 + "$ref": "#/$defs/TimeValue", + "default": 5000 }, "connection_recovery": { "description": "Controls if server connections are recovered or dropped if a client abruptly disconnects.\n\n_Default:_ `recover`\n\n", @@ -700,10 +680,8 @@ }, "cutover_last_transaction_delay": { "description": "Time (in milliseconds) since the last transaction on any table in the publication before PgDog will swap the configuration during a cutover.\n\n_Default:_ `1000`\n\n", - "type": "integer", - "format": "uint64", - "default": 1000, - "minimum": 0 + "$ref": "#/$defs/TimeValue", + "default": 1000 }, "cutover_replication_lag_threshold": { "description": "Replication lag (in bytes) that must be reached before PgDog will swap the configuration during a cutover.\n\n_Default:_ `0`\n\n", @@ -719,10 +697,8 @@ }, "cutover_timeout": { "description": "Maximum amount of time (in milliseconds) to wait for the cutover thresholds to be met. If exceeded, PgDog will take the action specified by `cutover_timeout_action`.\n\n_Default:_ `30000`\n\n", - "type": "integer", - "format": "uint64", - "default": 30000, - "minimum": 0 + "$ref": "#/$defs/TimeValue", + "default": 30000 }, "cutover_timeout_action": { "description": "Action to take when `cutover_timeout` is exceeded.\n\n_Default:_ `abort`\n\n", @@ -745,13 +721,15 @@ }, "dns_ttl": { "description": "Overrides the TTL set on DNS records received from DNS servers.\n\n", - "type": [ - "integer", - "null" + "anyOf": [ + { + "$ref": "#/$defs/TimeValue" + }, + { + "type": "null" + } ], - "format": "uint64", - "default": null, - "minimum": 0 + "default": null }, "dry_run": { "description": "Enable the query parser in single-shard deployments and record its decisions.\n\n_Default:_ `false`\n\n", @@ -765,10 +743,8 @@ }, "healthcheck_interval": { "description": "Frequency of healthchecks performed by PgDog to ensure connections provided to clients from the pool are working.\n\n_Default:_ `30000`\n\n", - "type": "integer", - "format": "uint64", - "default": 30000, - "minimum": 0 + "$ref": "#/$defs/TimeValue", + "default": 30000 }, "healthcheck_port": { "description": "Enable load balancer HTTP health checks with the HTTP server running on this port.\n\n", @@ -782,10 +758,8 @@ }, "healthcheck_timeout": { "description": "Maximum amount of time to wait for a healthcheck query to complete.\n\n_Default:_ `5000`\n\n", - "type": "integer", - "format": "uint64", - "default": 5000, - "minimum": 0 + "$ref": "#/$defs/TimeValue", + "default": 5000 }, "host": { "description": "The IP address of the local network interface PgDog will bind to listen for connections.\n\n**Note:** This setting cannot be changed at runtime.\n\n_Default:_ `0.0.0.0`\n\n", @@ -794,24 +768,18 @@ }, "idle_healthcheck_delay": { "description": "Delay running idle healthchecks at PgDog startup to give databases (and pools) time to spin up.\n\n_Default:_ `5000`\n\n", - "type": "integer", - "format": "uint64", - "default": 5000, - "minimum": 0 + "$ref": "#/$defs/TimeValue", + "default": 5000 }, "idle_healthcheck_interval": { "description": "Frequency of healthchecks performed by PgDog on idle connections.\nSet to `0` to disable idle healthchecks.\n\n_Default:_ `30000`\n\n", - "type": "integer", - "format": "uint64", - "default": 30000, - "minimum": 0 + "$ref": "#/$defs/TimeValue", + "default": 30000 }, "idle_timeout": { "description": "Close server connections that have been idle, i.e., haven't served a single client transaction, for this amount of time.\n\n_Default:_ `60000`\n\n", - "type": "integer", - "format": "uint64", - "default": 60000, - "minimum": 0 + "$ref": "#/$defs/TimeValue", + "default": 60000 }, "load_balancing_strategy": { "description": "Which strategy to use for load balancing read queries.\n\n_Default:_ `random`\n\n", @@ -837,10 +805,8 @@ }, "log_dedup_window": { "description": "Window, in milliseconds, over which to deduplicate identical log messages. Set to `0` to disable throttling.\n\n**Note:** When enabled, identical messages (same level, target, and body) that exceed `log_dedup_threshold` within this window are suppressed and replaced with a single summary line at the end of the window.\n\n_Default:_ `0`\n\n", - "type": "integer", - "format": "uint64", - "default": 0, - "minimum": 0 + "$ref": "#/$defs/TimeValue", + "default": 0 }, "log_disconnections": { "description": "If enabled, log every time a user disconnects from PgDog.\n\n_Default:_ `true`\n\n", @@ -859,12 +825,15 @@ }, "log_min_duration_parse": { "description": "Minimum parse duration in milliseconds that triggers a warning log with the query text.\nQueries whose parsing takes longer than this value are logged at WARN level.\nSet to `0` or omit to disable.\n\n_Default:_ `None` (disabled)", - "type": [ - "integer", - "null" + "anyOf": [ + { + "$ref": "#/$defs/TimeValue" + }, + { + "type": "null" + } ], - "format": "uint64", - "minimum": 0 + "default": null }, "log_query_sample_length": { "description": "Maximum number of characters of the query text included in log messages.\n\n_Default:_ `1000`", @@ -875,24 +844,18 @@ }, "lsn_check_delay": { "description": "For how long to delay checking for replication delay.\n\n", - "type": "integer", - "format": "uint64", - "default": 9223372036854775807, - "minimum": 0 + "$ref": "#/$defs/TimeValue", + "default": 9223372036854775807 }, "lsn_check_interval": { "description": "How frequently to run the replication delay check.\n\n_Default:_ `5000`\n\n", - "type": "integer", - "format": "uint64", - "default": 5000, - "minimum": 0 + "$ref": "#/$defs/TimeValue", + "default": 5000 }, "lsn_check_timeout": { "description": "Maximum amount of time allowed for the replication delay query to return a result.\n\n_Default:_ `5000`\n\n", - "type": "integer", - "format": "uint64", - "default": 5000, - "minimum": 0 + "$ref": "#/$defs/TimeValue", + "default": 5000 }, "min_pool_size": { "description": "Default minimum number of connections per database pool to keep open at all times.\n\n_Default:_ `1`\n\n", @@ -1025,10 +988,8 @@ }, "query_timeout": { "description": "Maximum amount of time to wait for a Postgres query to finish executing.\n\n", - "type": "integer", - "format": "uint64", - "default": 9223372036854775807, - "minimum": 0 + "$ref": "#/$defs/TimeValue", + "default": 9223372036854775807 }, "read_write_split": { "description": "How to handle the separation of read and write queries.\n\n_Default:_ `include_primary`\n\n", @@ -1066,10 +1027,8 @@ }, "resharding_copy_retry_min_delay": { "description": "Base delay in milliseconds between table copy retries.\nEach successive attempt doubles the delay, capped at 32×.\n_Default:_ `1000`", - "type": "integer", - "format": "uint64", - "default": 1000, - "minimum": 0 + "$ref": "#/$defs/TimeValue", + "default": 1000 }, "resharding_parallel_copies": { "description": "How many parallel copies to launch, irrespective of the number of available replicas.", @@ -1087,55 +1046,45 @@ }, "resharding_replication_retry_min_delay": { "description": "Delay in milliseconds between replication subscriber retry attempts.\n\n_Default:_ `1000`\n\n", - "type": "integer", - "format": "uint64", - "default": 1000, - "minimum": 0 + "$ref": "#/$defs/TimeValue", + "default": 1000 }, "rollback_timeout": { "description": "How long to allow for `ROLLBACK` queries to run on server connections with unfinished transactions.\n\n_Default:_ `5000`\n\n", - "type": "integer", - "format": "uint64", - "default": 5000, - "minimum": 0 + "$ref": "#/$defs/TimeValue", + "default": 5000 }, "server_lifetime": { "description": "Maximum amount of time a server connection is allowed to exist.\n\n_Default:_ `86400000`\n\n", - "type": "integer", - "format": "uint64", - "default": 86400000, - "minimum": 0 + "$ref": "#/$defs/TimeValue", + "default": 86400000 }, "server_lifetime_jitter": { "description": "Maximum random adjustment applied to `server_lifetime` per backend\nconnection, in milliseconds. Each connection's effective lifetime\nis sampled uniformly from `[server_lifetime - jitter,\nserver_lifetime + jitter]` once at creation time, breaking up\nsynchronized cohorts that would otherwise expire together.\n\n_Default:_ `0` (no jitter; existing behavior).\n\n", - "type": "integer", - "format": "uint64", - "default": 0, - "minimum": 0 + "$ref": "#/$defs/TimeValue", + "default": 0 }, "shutdown_termination_timeout": { "description": "How long to wait for active connections to be forcibly terminated after `shutdown_timeout` expires.\n\n**Note:** If set, PgDog will send `CANCEL` requests to PostgreSQL for any remaining active queries before tearing down connection pools.\n\n", - "type": [ - "integer", - "null" + "anyOf": [ + { + "$ref": "#/$defs/TimeValue" + }, + { + "type": "null" + } ], - "format": "uint64", - "default": null, - "minimum": 0 + "default": null }, "shutdown_timeout": { "description": "How long to wait for active clients to finish transactions when shutting down.\n\n_Default:_ `60000`\n\n", - "type": "integer", - "format": "uint64", - "default": 60000, - "minimum": 0 + "$ref": "#/$defs/TimeValue", + "default": 60000 }, "stats_period": { "description": "How often to calculate averages shown in `SHOW STATS` admin command and the Prometheus metrics.\n\n_Default:_ `15000`\n\n", - "type": "integer", - "format": "uint64", - "default": 15000, - "minimum": 0 + "$ref": "#/$defs/TimeValue", + "default": 15000 }, "system_catalogs": { "description": "Changes how system catalog tables (like `pg_database`, `pg_class`, etc.) are treated by the query router.\n\n_Default:_ `omnisharded_sticky`\n\n", @@ -1209,10 +1158,8 @@ }, "two_phase_commit_wal_checkpoint_interval": { "description": "How often, in seconds, to write a checkpoint record to the two-phase commit WAL and garbage-collect old segments.\n\n_Default:_ `60`\n\n", - "type": "integer", - "format": "uint64", - "default": 60, - "minimum": 0 + "$ref": "#/$defs/TimeValue", + "default": 60 }, "two_phase_commit_wal_dir": { "description": "Directory where the two-phase commit write-ahead log is stored.\n\n**Note:** This setting cannot be changed at runtime. PgDog acquires an exclusive `flock` on `/.lock` at startup. If the directory cannot be created or written to, or another PgDog process already holds the lock, the WAL is disabled and a warning is logged: 2PC will continue to function but will not be durable across restarts.\n\n_Default:_ `./pgdog_wal`\n\n", @@ -1224,10 +1171,8 @@ }, "two_phase_commit_wal_fsync_interval": { "description": "How long, in milliseconds, the two-phase commit WAL writer waits to coalesce concurrent appends into a single fsync.\n\n**Note:** Setting this to `0` disables waiting for additional appends; records already queued in the channel when the writer wakes are still batched into one fsync. Higher values trade per-transaction commit latency for fewer fsyncs under load.\n\n_Default:_ `2`\n\n", - "type": "integer", - "format": "uint64", - "default": 2, - "minimum": 0 + "$ref": "#/$defs/TimeValue", + "default": 2 }, "two_phase_commit_wal_segment_size": { "description": "Maximum size, in bytes, of a single two-phase commit WAL segment file before it is rotated.\n\n_Default:_ `16777216` (16 MiB)\n\n", @@ -1503,10 +1448,8 @@ }, "push_interval": { "description": "How often, in milliseconds, to push metrics to the OTLP endpoint.\n\n_Default:_ `10000`\n\nEnv: `OTEL_METRIC_EXPORT_INTERVAL`", - "type": "integer", - "format": "uint64", - "default": 10000, - "minimum": 0 + "$ref": "#/$defs/TimeValue", + "default": 10000 } }, "additionalProperties": false @@ -1737,11 +1680,11 @@ "properties": { "check_interval": { "description": "How often to check replica lag, in milliseconds.\n\n_Default:_ `1000`", - "$ref": "#/$defs/Duration" + "$ref": "#/$defs/TimeValue" }, "max_age": { "description": "Maximum allowed replication lag before a replica is banned, in milliseconds.\n\n_Default:_ `25`", - "$ref": "#/$defs/Duration" + "$ref": "#/$defs/TimeValue" } }, "required": [ @@ -2164,12 +2107,15 @@ }, "interval": { "description": "Time between successive keep-alive probes. Milliseconds.\n\n", - "type": [ - "integer", - "null" + "anyOf": [ + { + "$ref": "#/$defs/TimeValue" + }, + { + "type": "null" + } ], - "format": "uint64", - "minimum": 0 + "default": null }, "keepalive": { "description": "Enable TCP keep-alive probing on idle client and server connections.\n\n**Note:** Not all networks support TCP keep-alive. Disable if you observe increased connection drops.\n\n_Default:_ `true`\n\n", @@ -2187,25 +2133,46 @@ }, "time": { "description": "How long a connection must be idle before keep-alive probes begin. Milliseconds.\n\n_Default:_ system default (2 hours)\n\n", - "type": [ - "integer", - "null" + "anyOf": [ + { + "$ref": "#/$defs/TimeValue" + }, + { + "type": "null" + } ], - "format": "uint64", - "minimum": 0 + "default": null }, "user_timeout": { "description": "Close connections with unacknowledged data after this duration (`TCP_USER_TIMEOUT`). Milliseconds.\n\n**Note:** Linux only.\n\n", - "type": [ - "integer", - "null" + "anyOf": [ + { + "$ref": "#/$defs/TimeValue" + }, + { + "type": "null" + } ], - "format": "uint64", - "minimum": 0 + "default": null } }, "additionalProperties": false }, + "TimeValue": { + "description": "Time value: a number, in the unit documented on the field, or a duration\nstring built from `ms`, `s`, `m`, `h` and `d` components, e.g. `\"1h5m15s\"`.", + "anyOf": [ + { + "description": "Number of milliseconds, or seconds for fields documented in seconds.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + { + "description": "Duration string, e.g. `\"250ms\"`, `\"5s\"`, `\"1h5m15s\"`.", + "type": "string" + } + ] + }, "TlsVerifyMode": { "description": "TLS verification mode for connections to Postgres servers.\n\n", "oneOf": [ @@ -2276,12 +2243,15 @@ }, "client_token_ttl": { "description": "Seconds before the Vault client token expires to trigger a re-login.\n\n_Default:_ `60`", - "type": [ - "integer", - "null" + "anyOf": [ + { + "$ref": "#/$defs/TimeValue" + }, + { + "type": "null" + } ], - "format": "uint64", - "minimum": 0 + "default": null }, "kubernetes_jwt_path": { "description": "Kubernetes auth: path to the service account JWT.\n\n_Default:_ `/var/run/secrets/kubernetes.io/serviceaccount/token`", diff --git a/.schema/users.schema.json b/.schema/users.schema.json index ef595d9f8..64afda78d 100644 --- a/.schema/users.schema.json +++ b/.schema/users.schema.json @@ -98,6 +98,21 @@ } ] }, + "TimeValue": { + "description": "Time value: a number, in the unit documented on the field, or a duration\nstring built from `ms`, `s`, `m`, `h` and `d` components, e.g. `\"1h5m15s\"`.", + "anyOf": [ + { + "description": "Number of milliseconds, or seconds for fields documented in seconds.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + { + "description": "Duration string, e.g. `\"250ms\"`, `\"5s\"`, `\"1h5m15s\"`.", + "type": "string" + } + ] + }, "User": { "description": "User allowed to connect to pgDog.\nA user entry in `users.toml`, controlling which users are allowed to connect to PgDog.\n\n", "type": "object", @@ -136,21 +151,27 @@ }, "idle_timeout": { "description": "Overrides [`idle_timeout`](https://docs.pgdog.dev/configuration/pgdog.toml/general/#idle_timeout) for this user. Server connections that have been idle for this long, without affecting [`min_pool_size`](https://docs.pgdog.dev/configuration/pgdog.toml/general/#min_pool_size), will be closed.\n\n", - "type": [ - "integer", - "null" + "anyOf": [ + { + "$ref": "#/$defs/TimeValue" + }, + { + "type": "null" + } ], - "format": "uint64", - "minimum": 0 + "default": null }, "lock_timeout": { "description": "Lock timeout.\n\nSets the `lock_timeout` on all server connections at connection creation.\nAborts any statement that waits longer than the specified duration to acquire a lock.\nUnlike `statement_timeout`, this only counts time spent waiting for locks, not execution time.\nRecommended for replication destination connections to prevent cross-shard deadlocks\nfrom hanging indefinitely.\n\n**Note:** Nothing is preventing the user from manually changing this setting at runtime,\ne.g., by running `SET lock_timeout TO 0`;\n\n", - "type": [ - "integer", - "null" + "anyOf": [ + { + "$ref": "#/$defs/TimeValue" + }, + { + "type": "null" + } ], - "format": "uint64", - "minimum": 0 + "default": null }, "min_pool_size": { "description": "Overrides [`min_pool_size`](https://docs.pgdog.dev/configuration/pgdog.toml/general/#min_pool_size) for this user. Opens at least this many connections on pooler startup and keeps them open despite [`idle_timeout`](https://docs.pgdog.dev/configuration/pgdog.toml/general/#idle_timeout).\n\n", @@ -245,21 +266,27 @@ }, "server_lifetime": { "description": "Server connections older than this (in milliseconds) will be closed when returned to the pool.", - "type": [ - "integer", - "null" + "anyOf": [ + { + "$ref": "#/$defs/TimeValue" + }, + { + "type": "null" + } ], - "format": "uint64", - "minimum": 0 + "default": null }, "server_lifetime_jitter": { "description": "Maximum random adjustment applied to `server_lifetime` per backend connection (milliseconds).\nOverrides the database-level and general-level `server_lifetime_jitter` setting for this user.", - "type": [ - "integer", - "null" + "anyOf": [ + { + "$ref": "#/$defs/TimeValue" + }, + { + "type": "null" + } ], - "format": "uint64", - "minimum": 0 + "default": null }, "server_password": { "description": "Which password to connect with when creating backend connections from PgDog to PostgreSQL. By default, the password configured in `password` is used. This setting allows you to override this configuration and use a different password, decoupling server passwords from user passwords given to clients.\n\n**Note:** Values specified in `pgdog.toml` take priority over this configuration.\n\n", @@ -284,12 +311,15 @@ }, "statement_timeout": { "description": "Statement timeout.\n\nSets the `statement_timeout` on all server connections at connection creation. This allows you to set a reasonable default for each user without modifying `postgresql.conf` or using `ALTER USER`.\n\n**Note:** Nothing is preventing the user from manually changing this setting at runtime, e.g., by running `SET statement_timeout TO 0`;\n\n", - "type": [ - "integer", - "null" + "anyOf": [ + { + "$ref": "#/$defs/TimeValue" + }, + { + "type": "null" + } ], - "format": "uint64", - "minimum": 0 + "default": null }, "two_phase_commit": { "description": "Overrides [`two_phase_commit`](https://docs.pgdog.dev/configuration/pgdog.toml/general/#two_phase_commit) for this user.\n\n", diff --git a/example.pgdog.toml b/example.pgdog.toml index d110dd659..f3d83bb6c 100644 --- a/example.pgdog.toml +++ b/example.pgdog.toml @@ -4,6 +4,9 @@ # # Most settings have reasonable defaults. # +# Time settings accept a number, in the unit documented for the setting, or a +# duration string built from "ms", "s", "m", "h" and "d", e.g. "5s", "1h5m15s". +# # General settings. # diff --git a/pgdog-config/src/core.rs b/pgdog-config/src/core.rs index bc0b6f929..c6c7eee55 100644 --- a/pgdog-config/src/core.rs +++ b/pgdog-config/src/core.rs @@ -700,6 +700,104 @@ mod tests { use std::time::Duration; use tempfile::NamedTempFile; + #[test] + fn test_human_durations() { + let source = r#" +[general] +ban_timeout = "5m" +checkout_timeout = "2s500ms" +query_timeout = "1h5m15s" +shutdown_termination_timeout = "30s" +idle_timeout = 60000 +two_phase_commit_wal_checkpoint_interval = "2m" + +[[databases]] +name = "production" +host = "127.0.0.1" +database_name = "postgres" +statement_timeout = "1m30s" + +[tcp] +time = "1s" +interval = "500ms" +user_timeout = 1000 + +[replica_lag] +check_interval = "1s" +max_age = "25ms" + +[otel] +push_interval = "10s" + +[vault] +url = "http://127.0.0.1:8200" +auth_method = "kubernetes" +client_token_ttl = "5m" +"#; + + let config: Config = toml::from_str(source).unwrap(); + let general = &config.general; + + assert_eq!(general.ban_timeout, 300_000); + assert_eq!(general.checkout_timeout, 2_500); + assert_eq!(general.query_timeout, 3_915_000); + assert_eq!(general.shutdown_termination_timeout, Some(30_000)); + assert_eq!(general.idle_timeout, 60_000); + assert_eq!(general.two_phase_commit_wal_checkpoint_interval, 120); + + assert_eq!(config.databases[0].statement_timeout, Some(90_000)); + assert_eq!(config.tcp.time(), Some(Duration::from_secs(1))); + assert_eq!(config.tcp.interval(), Some(Duration::from_millis(500))); + assert_eq!(config.tcp.user_timeout(), Some(Duration::from_secs(1))); + + let replica_lag = config.replica_lag.unwrap(); + assert_eq!(replica_lag.check_interval, Duration::from_secs(1)); + assert_eq!(replica_lag.max_age, Duration::from_millis(25)); + + assert_eq!(config.otel.push_interval, 10_000); + assert_eq!(config.vault.unwrap().client_token_ttl, Some(300)); + } + + #[test] + fn test_human_durations_users() { + let source = r#" +[[users]] +name = "alice" +database = "production" +statement_timeout = "10s" +idle_timeout = "1h" +server_lifetime = "1d" +server_lifetime_jitter = 5000 +"#; + + let users: Users = toml::from_str(source).unwrap(); + let user = &users.users[0]; + + assert_eq!(user.statement_timeout, Some(10_000)); + assert_eq!(user.idle_timeout, Some(3_600_000)); + assert_eq!(user.server_lifetime, Some(86_400_000)); + assert_eq!(user.server_lifetime_jitter, Some(5_000)); + } + + #[test] + fn test_human_durations_invalid() { + let source = r#" +[general] +ban_timeout = "5 minutes" +"#; + + let err = toml::from_str::(source).unwrap_err().to_string(); + assert!(err.contains("is not a valid duration"), "{}", err); + + let source = r#" +[general] +two_phase_commit_wal_checkpoint_interval = "1500ms" +"#; + + let err = toml::from_str::(source).unwrap_err().to_string(); + assert!(err.contains("whole number of seconds"), "{}", err); + } + #[test] fn test_basic() { let pgdog_source = r#" diff --git a/pgdog-config/src/database.rs b/pgdog-config/src/database.rs index bb31809f0..1e87d1ffa 100644 --- a/pgdog-config/src/database.rs +++ b/pgdog-config/src/database.rs @@ -178,16 +178,22 @@ pub struct Database { /// This setting configures the `statement_timeout` connection parameter on all connections to Postgres for this database. /// /// + #[serde(default, deserialize_with = "crate::duration::millis_optional")] + #[schemars(with = "Option")] pub statement_timeout: Option, /// This setting configures the `lock_timeout` connection parameter on all connections to Postgres for this database. /// Aborts any statement that waits longer than the specified duration to acquire a lock. /// Unlike `statement_timeout`, this only counts time spent waiting for locks, not execution time. /// /// + #[serde(default, deserialize_with = "crate::duration::millis_optional")] + #[schemars(with = "Option")] pub lock_timeout: Option, /// Overrides the `idle_timeout` setting. Idle server connections exceeding this timeout will be closed automatically. /// /// + #[serde(default, deserialize_with = "crate::duration::millis_optional")] + #[schemars(with = "Option")] pub idle_timeout: Option, /// Sets the `default_transaction_read_only` connection parameter to `on` on all server connections to this database. Clients can still override it with `SET`. /// @@ -196,10 +202,14 @@ pub struct Database { /// Overrides the `server_lifetime` setting. Server connections older than this will be closed when returned to the pool. /// /// + #[serde(default, deserialize_with = "crate::duration::millis_optional")] + #[schemars(with = "Option")] pub server_lifetime: Option, /// Overrides the `server_lifetime_jitter` setting for this database. /// /// + #[serde(default, deserialize_with = "crate::duration::millis_optional")] + #[schemars(with = "Option")] pub server_lifetime_jitter: Option, /// Used for resharding only; this database will not serve regular traffic. #[serde(default)] diff --git a/pgdog-config/src/duration.rs b/pgdog-config/src/duration.rs new file mode 100644 index 000000000..e7cfae5b0 --- /dev/null +++ b/pgdog-config/src/duration.rs @@ -0,0 +1,252 @@ +//! Human-readable time values, e.g. `"5s"` or `"1h5m15s"`. +//! +//! Time settings accept either a plain number, in the unit documented on the +//! field, or a duration string built from `ms`, `s`, `m`, `h` and `d` components. + +use std::time::Duration; + +use schemars::JsonSchema; +use serde::Deserialize; +use serde::de::{self, Deserializer}; +use thiserror::Error; + +#[derive(Debug, Error, PartialEq)] +pub enum Error { + #[error(r#""{0}" is not a valid duration, expected a number or e.g. "250ms", "5s", "1h5m15s""#)] + Invalid(String), + + #[error(r#"duration "{0}" is too large"#)] + Overflow(String), + + #[error(r#"duration "{0}" is not a whole number of seconds"#)] + NotWholeSeconds(String), +} + +const UNITS: &[(&str, u64)] = &[ + ("ms", 1), + ("s", 1_000), + ("m", 60 * 1_000), + ("h", 60 * 60 * 1_000), + ("d", 24 * 60 * 60 * 1_000), +]; + +/// Units are required; use [`parse_millis`] or [`parse_seconds`] to also accept bare numbers. +pub fn parse(value: &str) -> Result { + let input = value.trim(); + if input.is_empty() { + return Err(Error::Invalid(input.into())); + } + + let mut rest = input; + let mut millis = 0_u64; + + while !rest.is_empty() { + let (number, tail) = rest.split_at( + rest.find(|c: char| !c.is_ascii_digit()) + .unwrap_or(rest.len()), + ); + let tail = tail.trim_start(); + let (unit, tail) = tail.split_at( + tail.find(|c: char| !c.is_ascii_alphabetic()) + .unwrap_or(tail.len()), + ); + + let number = number + .trim() + .parse::() + .map_err(|_| Error::Invalid(input.into()))?; + let (_, multiplier) = UNITS + .iter() + .find(|(name, _)| *name == unit) + .ok_or_else(|| Error::Invalid(input.into()))?; + + millis = number + .checked_mul(*multiplier) + .and_then(|value| millis.checked_add(value)) + .ok_or_else(|| Error::Overflow(input.into()))?; + rest = tail.trim_start(); + } + + Ok(Duration::from_millis(millis)) +} + +pub fn parse_millis(value: &str) -> Result { + if let Ok(millis) = value.trim().parse::() { + return Ok(millis); + } + + Ok(parse(value)?.as_millis() as u64) +} + +pub fn parse_seconds(value: &str) -> Result { + if let Ok(seconds) = value.trim().parse::() { + return Ok(seconds); + } + + let duration = parse(value)?; + if duration.subsec_millis() != 0 { + return Err(Error::NotWholeSeconds(value.trim().into())); + } + + Ok(duration.as_secs()) +} + +/// Time value: a number, in the unit documented on the field, or a duration +/// string built from `ms`, `s`, `m`, `h` and `d` components, e.g. `"1h5m15s"`. +#[derive(Deserialize, JsonSchema)] +#[serde(untagged)] +pub enum TimeValue { + /// Number of milliseconds, or seconds for fields documented in seconds. + Number(#[schemars(with = "u64")] i64), + /// Duration string, e.g. `"250ms"`, `"5s"`, `"1h5m15s"`. + Text(String), +} + +impl TimeValue { + fn resolve(self, parse: fn(&str) -> Result) -> Result { + match self { + TimeValue::Number(number) => u64::try_from(number) + .map_err(|_| E::custom(format!("duration cannot be negative: {}", number))), + TimeValue::Text(text) => parse(&text).map_err(E::custom), + } + } +} + +pub fn millis<'de, D: Deserializer<'de>>(deserializer: D) -> Result { + TimeValue::deserialize(deserializer)?.resolve(parse_millis) +} + +pub fn millis_optional<'de, D: Deserializer<'de>>( + deserializer: D, +) -> Result, D::Error> { + Option::::deserialize(deserializer)? + .map(|value| value.resolve(parse_millis)) + .transpose() +} + +pub fn seconds<'de, D: Deserializer<'de>>(deserializer: D) -> Result { + TimeValue::deserialize(deserializer)?.resolve(parse_seconds) +} + +pub fn seconds_optional<'de, D: Deserializer<'de>>( + deserializer: D, +) -> Result, D::Error> { + Option::::deserialize(deserializer)? + .map(|value| value.resolve(parse_seconds)) + .transpose() +} + +#[cfg(test)] +mod test { + use super::*; + use crate::util::human_duration; + + #[test] + fn test_parse_single_component() { + assert_eq!(parse("250ms"), Ok(Duration::from_millis(250))); + assert_eq!(parse("5s"), Ok(Duration::from_secs(5))); + assert_eq!(parse("15m"), Ok(Duration::from_secs(900))); + assert_eq!(parse("2h"), Ok(Duration::from_secs(7200))); + assert_eq!(parse("3d"), Ok(Duration::from_secs(259_200))); + assert_eq!(parse("0s"), Ok(Duration::ZERO)); + } + + #[test] + fn test_parse_compound() { + assert_eq!(parse("1h5m15s"), Ok(Duration::from_secs(3915))); + assert_eq!(parse("1h 5m 15s"), Ok(Duration::from_secs(3915))); + assert_eq!( + parse(" 1d12h30m500ms "), + Ok(Duration::from_millis(131_400_500)) + ); + assert_eq!(parse("5s5s"), Ok(Duration::from_secs(10))); + assert_eq!(parse("15s1h"), Ok(Duration::from_secs(3615))); + } + + #[test] + fn test_parse_invalid() { + for value in [ + "", " ", "5", "s", "5x", "1.5s", "5s!", "-5s", "5 s x", "ms", + ] { + assert!(matches!(parse(value), Err(Error::Invalid(_))), "{}", value); + } + + assert!(matches!( + parse("99999999999999999999s"), + Err(Error::Invalid(_)) + )); + assert!(matches!( + parse("9999999999999999d"), + Err(Error::Overflow(_)) + )); + } + + #[test] + fn test_parse_millis() { + assert_eq!(parse_millis("5000"), Ok(5000)); + assert_eq!(parse_millis("5s"), Ok(5000)); + assert_eq!(parse_millis("0"), Ok(0)); + assert!(parse_millis("5 000").is_err()); + } + + #[test] + fn test_parse_seconds() { + assert_eq!(parse_seconds("60"), Ok(60)); + assert_eq!(parse_seconds("1m"), Ok(60)); + assert_eq!(parse_seconds("1h5m15s"), Ok(3915)); + assert_eq!( + parse_seconds("500ms"), + Err(Error::NotWholeSeconds("500ms".into())) + ); + } + + #[test] + fn test_optional_null_and_missing() { + #[derive(Deserialize)] + struct Settings { + #[serde(default, deserialize_with = "millis_optional")] + timeout: Option, + } + + let missing: Settings = serde_json::from_str("{}").unwrap(); + assert_eq!(missing.timeout, None); + + let null: Settings = serde_json::from_str(r#"{"timeout": null}"#).unwrap(); + assert_eq!(null.timeout, None); + + let set: Settings = serde_json::from_str(r#"{"timeout": "5s"}"#).unwrap(); + assert_eq!(set.timeout, Some(5_000)); + } + + #[test] + fn test_deserialize_errors() { + #[derive(Deserialize, Debug)] + struct Settings { + #[serde(deserialize_with = "millis")] + timeout: u64, + } + + let parsed: Settings = toml::from_str(r#"timeout = "5s""#).unwrap(); + assert_eq!(parsed.timeout, 5_000); + + let err = toml::from_str::(r#"timeout = -1"#) + .unwrap_err() + .to_string(); + assert!(err.contains("duration cannot be negative: -1"), "{}", err); + + let err = toml::from_str::(r#"timeout = "5 minutes""#) + .unwrap_err() + .to_string(); + assert!(err.contains("is not a valid duration"), "{}", err); + } + + #[test] + fn test_human_duration_round_trip() { + for millis in [ + 0, 1, 999, 1_000, 1_500, 60_000, 300_000, 3_600_000, 86_400_000, + ] { + let duration = Duration::from_millis(millis); + assert_eq!(parse(&human_duration(duration)), Ok(duration)); + } + } +} diff --git a/pgdog-config/src/general.rs b/pgdog-config/src/general.rs index 927dc9004..67683eb84 100644 --- a/pgdog-config/src/general.rs +++ b/pgdog-config/src/general.rs @@ -9,6 +9,7 @@ use std::str::FromStr; use std::time::Duration; use crate::UniqueIdFunction; +use crate::duration; use crate::pooling::ConnectionRecovery; use crate::{ CopyFormat, CutoverTimeoutAction, LoadSchema, QueryParserEngine, QueryParserLevel, @@ -136,7 +137,11 @@ pub struct General { /// _Default:_ `30000` /// /// - #[serde(default = "General::healthcheck_interval")] + #[serde( + default = "General::healthcheck_interval", + deserialize_with = "crate::duration::millis" + )] + #[schemars(with = "crate::duration::TimeValue")] pub healthcheck_interval: u64, /// Frequency of healthchecks performed by PgDog on idle connections. @@ -145,7 +150,11 @@ pub struct General { /// _Default:_ `30000` /// /// - #[serde(default = "General::idle_healthcheck_interval")] + #[serde( + default = "General::idle_healthcheck_interval", + deserialize_with = "crate::duration::millis" + )] + #[schemars(with = "crate::duration::TimeValue")] pub idle_healthcheck_interval: u64, /// Delay running idle healthchecks at PgDog startup to give databases (and pools) time to spin up. @@ -153,7 +162,11 @@ pub struct General { /// _Default:_ `5000` /// /// - #[serde(default = "General::idle_healthcheck_delay")] + #[serde( + default = "General::idle_healthcheck_delay", + deserialize_with = "crate::duration::millis" + )] + #[schemars(with = "crate::duration::TimeValue")] pub idle_healthcheck_delay: u64, /// Maximum amount of time to wait for a healthcheck query to complete. @@ -161,7 +174,11 @@ pub struct General { /// _Default:_ `5000` /// /// - #[serde(default = "General::healthcheck_timeout")] + #[serde( + default = "General::healthcheck_timeout", + deserialize_with = "crate::duration::millis" + )] + #[schemars(with = "crate::duration::TimeValue")] pub healthcheck_timeout: u64, /// Enable load balancer HTTP health checks with the HTTP server running on this port. @@ -174,13 +191,21 @@ pub struct General { /// _Default:_ `300000` /// /// - #[serde(default = "General::ban_timeout")] + #[serde( + default = "General::ban_timeout", + deserialize_with = "crate::duration::millis" + )] + #[schemars(with = "crate::duration::TimeValue")] pub ban_timeout: u64, /// Ban a replica from serving read queries if its replication lag (in milliseconds) exceeds this threshold. /// /// - #[serde(default = "General::ban_replica_lag")] + #[serde( + default = "General::ban_replica_lag", + deserialize_with = "crate::duration::millis" + )] + #[schemars(with = "crate::duration::TimeValue")] pub ban_replica_lag: u64, /// Ban a replica from serving read queries if its replication lag (in bytes) exceeds this threshold. @@ -194,7 +219,11 @@ pub struct General { /// _Default:_ `5000` /// /// - #[serde(default = "General::rollback_timeout")] + #[serde( + default = "General::rollback_timeout", + deserialize_with = "crate::duration::millis" + )] + #[schemars(with = "crate::duration::TimeValue")] pub rollback_timeout: u64, /// Which strategy to use for load balancing read queries. @@ -274,7 +303,11 @@ pub struct General { /// _Default:_ `60000` /// /// - #[serde(default = "General::default_shutdown_timeout")] + #[serde( + default = "General::default_shutdown_timeout", + deserialize_with = "crate::duration::millis" + )] + #[schemars(with = "crate::duration::TimeValue")] pub shutdown_timeout: u64, /// How long to wait for active connections to be forcibly terminated after `shutdown_timeout` expires. @@ -282,7 +315,11 @@ pub struct General { /// **Note:** If set, PgDog will send `CANCEL` requests to PostgreSQL for any remaining active queries before tearing down connection pools. /// /// - #[serde(default = "General::default_shutdown_termination_timeout")] + #[serde( + default = "General::default_shutdown_termination_timeout", + deserialize_with = "crate::duration::millis_optional" + )] + #[schemars(with = "Option")] pub shutdown_termination_timeout: Option, /// Broadcast IP address used for multi-instance coordination (e.g., schema cache invalidation across nodes). @@ -305,6 +342,8 @@ pub struct General { /// Set to `0` or omit to disable. /// /// _Default:_ `None` (disabled) + #[serde(default, deserialize_with = "crate::duration::millis_optional")] + #[schemars(with = "Option")] pub log_min_duration_parse: Option, /// Maximum number of characters of the query text included in log messages. @@ -403,7 +442,11 @@ pub struct General { /// _Default:_ `5000` /// /// - #[serde(default = "General::default_connect_timeout")] + #[serde( + default = "General::default_connect_timeout", + deserialize_with = "crate::duration::millis" + )] + #[schemars(with = "crate::duration::TimeValue")] pub connect_timeout: u64, /// Maximum number of retries for Postgres server connection attempts. @@ -419,13 +462,21 @@ pub struct General { /// _Default:_ `0` /// /// - #[serde(default = "General::default_connect_attempt_delay")] + #[serde( + default = "General::default_connect_attempt_delay", + deserialize_with = "crate::duration::millis" + )] + #[schemars(with = "crate::duration::TimeValue")] pub connect_attempt_delay: u64, /// Maximum amount of time to wait for a Postgres query to finish executing. /// /// - #[serde(default = "General::default_query_timeout")] + #[serde( + default = "General::default_query_timeout", + deserialize_with = "crate::duration::millis" + )] + #[schemars(with = "crate::duration::TimeValue")] pub query_timeout: u64, /// Maximum amount of time a client is allowed to wait for a connection from the pool. @@ -433,7 +484,11 @@ pub struct General { /// _Default:_ `5000` /// /// - #[serde(default = "General::checkout_timeout")] + #[serde( + default = "General::checkout_timeout", + deserialize_with = "crate::duration::millis" + )] + #[schemars(with = "crate::duration::TimeValue")] pub checkout_timeout: u64, /// Maximum amount of time new clients have to complete authentication. @@ -441,7 +496,11 @@ pub struct General { /// _Default:_ `60000` /// /// - #[serde(default = "General::client_login_timeout")] + #[serde( + default = "General::client_login_timeout", + deserialize_with = "crate::duration::millis" + )] + #[schemars(with = "crate::duration::TimeValue")] pub client_login_timeout: u64, /// Enable the query parser in single-shard deployments and record its decisions. @@ -457,19 +516,31 @@ pub struct General { /// _Default:_ `60000` /// /// - #[serde(default = "General::idle_timeout")] + #[serde( + default = "General::idle_timeout", + deserialize_with = "crate::duration::millis" + )] + #[schemars(with = "crate::duration::TimeValue")] pub idle_timeout: u64, /// Close client connections that have been idle, i.e., haven't sent any queries, for this amount of time. /// /// - #[serde(default = "General::default_client_idle_timeout")] + #[serde( + default = "General::default_client_idle_timeout", + deserialize_with = "crate::duration::millis" + )] + #[schemars(with = "crate::duration::TimeValue")] pub client_idle_timeout: u64, /// Close client connections that have been idle inside a transaction for this amount of time. /// /// - #[serde(default = "General::default_client_idle_in_transaction_timeout")] + #[serde( + default = "General::default_client_idle_in_transaction_timeout", + deserialize_with = "crate::duration::millis" + )] + #[schemars(with = "crate::duration::TimeValue")] pub client_idle_in_transaction_timeout: u64, /// Maximum amount of time a server connection is allowed to exist. @@ -477,7 +548,11 @@ pub struct General { /// _Default:_ `86400000` /// /// - #[serde(default = "General::server_lifetime")] + #[serde( + default = "General::server_lifetime", + deserialize_with = "crate::duration::millis" + )] + #[schemars(with = "crate::duration::TimeValue")] pub server_lifetime: u64, /// Maximum random adjustment applied to `server_lifetime` per backend @@ -489,7 +564,11 @@ pub struct General { /// _Default:_ `0` (no jitter; existing behavior). /// /// - #[serde(default = "General::server_lifetime_jitter")] + #[serde( + default = "General::server_lifetime_jitter", + deserialize_with = "crate::duration::millis" + )] + #[schemars(with = "crate::duration::TimeValue")] pub server_lifetime_jitter: u64, /// How many transactions can wait while the mirror database processes previous requests. @@ -523,7 +602,8 @@ pub struct General { /// Overrides the TTL set on DNS records received from DNS servers. /// /// - #[serde(default)] + #[serde(default, deserialize_with = "crate::duration::millis_optional")] + #[schemars(with = "Option")] pub dns_ttl: Option, /// Enables support for pub/sub and configures the size of the background task queue. @@ -571,7 +651,8 @@ pub struct General { /// _Default:_ `0` /// /// - #[serde(default)] + #[serde(default, deserialize_with = "crate::duration::millis")] + #[schemars(with = "crate::duration::TimeValue")] pub log_dedup_window: u64, /// Number of identical log messages allowed within `log_dedup_window` before further duplicates are suppressed. Set to `0` to disable throttling. @@ -623,7 +704,11 @@ pub struct General { /// _Default:_ `2` /// /// - #[serde(default = "General::two_phase_commit_wal_fsync_interval")] + #[serde( + default = "General::two_phase_commit_wal_fsync_interval", + deserialize_with = "crate::duration::millis" + )] + #[schemars(with = "crate::duration::TimeValue")] pub two_phase_commit_wal_fsync_interval: u64, /// How often, in seconds, to write a checkpoint record to the two-phase commit WAL and garbage-collect old segments. @@ -631,7 +716,11 @@ pub struct General { /// _Default:_ `60` /// /// - #[serde(default = "General::two_phase_commit_wal_checkpoint_interval")] + #[serde( + default = "General::two_phase_commit_wal_checkpoint_interval", + deserialize_with = "crate::duration::seconds" + )] + #[schemars(with = "crate::duration::TimeValue")] pub two_phase_commit_wal_checkpoint_interval: u64, /// Enable expanded (`\x`) output for `EXPLAIN` results returned by PgDog's built-in query plan aggregation. @@ -643,7 +732,11 @@ pub struct General { /// _Default:_ `15000` /// /// - #[serde(default = "General::stats_period")] + #[serde( + default = "General::stats_period", + deserialize_with = "crate::duration::millis" + )] + #[schemars(with = "crate::duration::TimeValue")] pub stats_period: u64, /// Controls if server connections are recovered or dropped if a client abruptly disconnects. @@ -669,7 +762,11 @@ pub struct General { /// _Default:_ `5000` /// /// - #[serde(default = "General::lsn_check_interval")] + #[serde( + default = "General::lsn_check_interval", + deserialize_with = "crate::duration::millis" + )] + #[schemars(with = "crate::duration::TimeValue")] pub lsn_check_interval: u64, /// Maximum amount of time allowed for the replication delay query to return a result. @@ -677,13 +774,21 @@ pub struct General { /// _Default:_ `5000` /// /// - #[serde(default = "General::lsn_check_timeout")] + #[serde( + default = "General::lsn_check_timeout", + deserialize_with = "crate::duration::millis" + )] + #[schemars(with = "crate::duration::TimeValue")] pub lsn_check_timeout: u64, /// For how long to delay checking for replication delay. /// /// - #[serde(default = "General::lsn_check_delay")] + #[serde( + default = "General::lsn_check_delay", + deserialize_with = "crate::duration::millis" + )] + #[schemars(with = "crate::duration::TimeValue")] pub lsn_check_delay: u64, /// Minimum ID for unique ID generator. @@ -731,7 +836,11 @@ pub struct General { /// Base delay in milliseconds between table copy retries. /// Each successive attempt doubles the delay, capped at 32×. /// _Default:_ `1000` - #[serde(default = "General::resharding_copy_retry_min_delay")] + #[serde( + default = "General::resharding_copy_retry_min_delay", + deserialize_with = "crate::duration::millis" + )] + #[schemars(with = "crate::duration::TimeValue")] pub resharding_copy_retry_min_delay: u64, /// Maximum number of consecutive replication-subscriber errors tolerated before @@ -750,7 +859,11 @@ pub struct General { /// _Default:_ `1000` /// /// - #[serde(default = "General::resharding_replication_retry_min_delay")] + #[serde( + default = "General::resharding_replication_retry_min_delay", + deserialize_with = "crate::duration::millis" + )] + #[schemars(with = "crate::duration::TimeValue")] pub resharding_replication_retry_min_delay: u64, /// Automatically reload the schema cache used by PgDog to route queries upon detecting DDL statements. @@ -792,7 +905,11 @@ pub struct General { /// _Default:_ `1000` /// /// - #[serde(default = "General::cutover_last_transaction_delay")] + #[serde( + default = "General::cutover_last_transaction_delay", + deserialize_with = "crate::duration::millis" + )] + #[schemars(with = "crate::duration::TimeValue")] pub cutover_last_transaction_delay: u64, /// Maximum amount of time (in milliseconds) to wait for the cutover thresholds to be met. If exceeded, PgDog will take the action specified by `cutover_timeout_action`. @@ -800,7 +917,11 @@ pub struct General { /// _Default:_ `30000` /// /// - #[serde(default = "General::cutover_timeout")] + #[serde( + default = "General::cutover_timeout", + deserialize_with = "crate::duration::millis" + )] + #[schemars(with = "crate::duration::TimeValue")] pub cutover_timeout: u64, /// Action to take when `cutover_timeout` is exceeded. @@ -938,6 +1059,18 @@ impl General { .unwrap_or(default) } + fn env_millis(env_var: &str) -> Option { + env::var(env_var) + .ok() + .and_then(|value| duration::parse_millis(&value).ok()) + } + + fn env_seconds(env_var: &str) -> Option { + env::var(env_var) + .ok() + .and_then(|value| duration::parse_seconds(&value).ok()) + } + fn env_string_or_default(env_var: &str, default: &str) -> String { env::var(env_var).unwrap_or_else(|_| default.to_string()) } @@ -989,7 +1122,7 @@ impl General { } fn healthcheck_interval() -> u64 { - Self::env_or_default("PGDOG_HEALTHCHECK_INTERVAL", 30_000) + Self::env_millis("PGDOG_HEALTHCHECK_INTERVAL").unwrap_or(30_000) } fn reload_schema_on_ddl() -> bool { @@ -997,11 +1130,11 @@ impl General { } fn idle_healthcheck_interval() -> u64 { - Self::env_or_default("PGDOG_IDLE_HEALTHCHECK_INTERVAL", 30_000) + Self::env_millis("PGDOG_IDLE_HEALTHCHECK_INTERVAL").unwrap_or(30_000) } fn idle_healthcheck_delay() -> u64 { - Self::env_or_default("PGDOG_IDLE_HEALTHCHECK_DELAY", 5_000) + Self::env_millis("PGDOG_IDLE_HEALTHCHECK_DELAY").unwrap_or(5_000) } fn healthcheck_port() -> Option { @@ -1013,15 +1146,12 @@ impl General { } fn ban_timeout() -> u64 { - Self::env_or_default( - "PGDOG_BAN_TIMEOUT", - Duration::from_secs(300).as_millis() as u64, - ) + Self::env_millis("PGDOG_BAN_TIMEOUT").unwrap_or(Duration::from_secs(300).as_millis() as u64) } fn ban_replica_lag() -> u64 { // Use i64::MAX to ensure TOML serialization compatibility (TOML only supports i64) - Self::env_or_default("PGDOG_BAN_REPLICA_LAG", i64::MAX as u64) + Self::env_millis("PGDOG_BAN_REPLICA_LAG").unwrap_or(i64::MAX as u64) } fn ban_replica_lag_bytes() -> u64 { @@ -1044,11 +1174,11 @@ impl General { } fn cutover_last_transaction_delay() -> u64 { - Self::env_or_default("PGDOG_CUTOVER_LAST_TRANSACTION_DELAY", 1_000) // 1 second + Self::env_millis("PGDOG_CUTOVER_LAST_TRANSACTION_DELAY").unwrap_or(1_000) // 1 second } fn cutover_timeout() -> u64 { - Self::env_or_default("PGDOG_CUTOVER_TIMEOUT", 30_000) + Self::env_millis("PGDOG_CUTOVER_TIMEOUT").unwrap_or(30_000) // 30 seconds } @@ -1057,7 +1187,7 @@ impl General { } fn rollback_timeout() -> u64 { - Self::env_or_default("PGDOG_ROLLBACK_TIMEOUT", 5_000) + Self::env_millis("PGDOG_ROLLBACK_TIMEOUT").unwrap_or(5_000) } fn two_phase_commit_wal_dir() -> Option { @@ -1069,46 +1199,34 @@ impl General { } fn two_phase_commit_wal_fsync_interval() -> u64 { - Self::env_or_default("PGDOG_TWO_PHASE_COMMIT_WAL_FSYNC_INTERVAL", 2) + Self::env_millis("PGDOG_TWO_PHASE_COMMIT_WAL_FSYNC_INTERVAL").unwrap_or(2) } fn two_phase_commit_wal_checkpoint_interval() -> u64 { - Self::env_or_default("PGDOG_TWO_PHASE_COMMIT_WAL_CHECKPOINT_INTERVAL", 60) + Self::env_seconds("PGDOG_TWO_PHASE_COMMIT_WAL_CHECKPOINT_INTERVAL").unwrap_or(60) } fn idle_timeout() -> u64 { - Self::env_or_default( - "PGDOG_IDLE_TIMEOUT", - Duration::from_secs(60).as_millis() as u64, - ) + Self::env_millis("PGDOG_IDLE_TIMEOUT").unwrap_or(Duration::from_secs(60).as_millis() as u64) } fn client_login_timeout() -> u64 { - Self::env_or_default( - "PGDOG_CLIENT_LOG_TIMEOUT", - Duration::from_secs(60).as_millis() as u64, - ) + Self::env_millis("PGDOG_CLIENT_LOG_TIMEOUT") + .unwrap_or(Duration::from_secs(60).as_millis() as u64) } fn default_client_idle_timeout() -> u64 { - Self::env_or_default( - "PGDOG_CLIENT_IDLE_TIMEOUT", - crate::MAX_DURATION.as_millis() as u64, - ) + Self::env_millis("PGDOG_CLIENT_IDLE_TIMEOUT") + .unwrap_or(crate::MAX_DURATION.as_millis() as u64) } fn default_client_idle_in_transaction_timeout() -> u64 { - Self::env_or_default( - "PGDOG_CLIENT_IDLE_IN_TRANSACTION_TIMEOUT", - crate::MAX_DURATION.as_millis() as u64, - ) + Self::env_millis("PGDOG_CLIENT_IDLE_IN_TRANSACTION_TIMEOUT") + .unwrap_or(crate::MAX_DURATION.as_millis() as u64) } fn default_query_timeout() -> u64 { - Self::env_or_default( - "PGDOG_QUERY_TIMEOUT", - crate::MAX_DURATION.as_millis() as u64, - ) + Self::env_millis("PGDOG_QUERY_TIMEOUT").unwrap_or(crate::MAX_DURATION.as_millis() as u64) } pub fn query_timeout(&self) -> Duration { @@ -1143,7 +1261,7 @@ impl General { } fn default_shutdown_timeout() -> u64 { - Self::env_or_default("PGDOG_SHUTDOWN_TIMEOUT", 60_000) + Self::env_millis("PGDOG_SHUTDOWN_TIMEOUT").unwrap_or(60_000) } fn default_system_catalogs() -> SystemCatalogsBehavior { @@ -1167,19 +1285,19 @@ impl General { } fn resharding_replication_retry_min_delay() -> u64 { - Self::env_or_default("PGDOG_RESHARDING_REPLICATION_RETRY_MIN_DELAY", 1000) + Self::env_millis("PGDOG_RESHARDING_REPLICATION_RETRY_MIN_DELAY").unwrap_or(1000) } fn default_shutdown_termination_timeout() -> Option { - Self::env_option("PGDOG_SHUTDOWN_TERMINATION_TIMEOUT") + Self::env_millis("PGDOG_SHUTDOWN_TERMINATION_TIMEOUT") } fn default_connect_timeout() -> u64 { - Self::env_or_default("PGDOG_CONNECT_TIMEOUT", 5_000) + Self::env_millis("PGDOG_CONNECT_TIMEOUT").unwrap_or(5_000) } fn default_connect_attempt_delay() -> u64 { - Self::env_or_default("PGDOG_CONNECT_ATTEMPT_DELAY", 0) + Self::env_millis("PGDOG_CONNECT_ATTEMPT_DELAY").unwrap_or(0) } fn connect_attempts() -> u64 { @@ -1191,18 +1309,15 @@ impl General { } fn lsn_check_timeout() -> u64 { - Self::env_or_default("PGDOG_LSN_CHECK_TIMEOUT", 5_000) + Self::env_millis("PGDOG_LSN_CHECK_TIMEOUT").unwrap_or(5_000) } fn lsn_check_interval() -> u64 { - Self::env_or_default("PGDOG_LSN_CHECK_INTERVAL", 5_000) + Self::env_millis("PGDOG_LSN_CHECK_INTERVAL").unwrap_or(5_000) } fn lsn_check_delay() -> u64 { - Self::env_or_default( - "PGDOG_LSN_CHECK_DELAY", - crate::MAX_DURATION.as_millis() as u64, - ) + Self::env_millis("PGDOG_LSN_CHECK_DELAY").unwrap_or(crate::MAX_DURATION.as_millis() as u64) } pub fn lsn_checks_enabled(&self) -> bool { @@ -1262,7 +1377,7 @@ impl General { } fn default_log_min_duration_parse() -> Option { - Self::env_option("PGDOG_LOG_MIN_DURATION_PARSE") + Self::env_millis("PGDOG_LOG_MIN_DURATION_PARSE") } pub fn log_min_duration_parse(&self) -> Option { @@ -1290,7 +1405,7 @@ impl General { } fn default_dns_ttl() -> Option { - Self::env_option("PGDOG_DNS_TTL") + Self::env_millis("PGDOG_DNS_TTL") } pub fn pub_sub_channel_size() -> usize { @@ -1314,17 +1429,13 @@ impl General { } fn healthcheck_timeout() -> u64 { - Self::env_or_default( - "PGDOG_HEALTHCHECK_TIMEOUT", - Duration::from_secs(5).as_millis() as u64, - ) + Self::env_millis("PGDOG_HEALTHCHECK_TIMEOUT") + .unwrap_or(Duration::from_secs(5).as_millis() as u64) } fn checkout_timeout() -> u64 { - Self::env_or_default( - "PGDOG_CHECKOUT_TIMEOUT", - Duration::from_secs(5).as_millis() as u64, - ) + Self::env_millis("PGDOG_CHECKOUT_TIMEOUT") + .unwrap_or(Duration::from_secs(5).as_millis() as u64) } fn load_schema() -> LoadSchema { @@ -1368,14 +1479,12 @@ impl General { } pub fn server_lifetime() -> u64 { - Self::env_or_default( - "PGDOG_SERVER_LIFETIME", - Duration::from_secs(3600 * 24).as_millis() as u64, - ) + Self::env_millis("PGDOG_SERVER_LIFETIME") + .unwrap_or(Duration::from_secs(3600 * 24).as_millis() as u64) } pub fn server_lifetime_jitter() -> u64 { - Self::env_or_default("PGDOG_SERVER_LIFETIME_JITTER", 0) + Self::env_millis("PGDOG_SERVER_LIFETIME_JITTER").unwrap_or(0) } pub fn connection_recovery() -> ConnectionRecovery { @@ -1387,7 +1496,7 @@ impl General { } fn stats_period() -> u64 { - Self::env_or_default("PGDOG_STATS_PERIOD", 15_000) + Self::env_millis("PGDOG_STATS_PERIOD").unwrap_or(15_000) } fn default_passthrough_auth() -> PassthroughAuth { @@ -1456,6 +1565,27 @@ mod tests { use super::*; use crate::test_utils::*; + #[test] + fn test_env_human_durations() { + let _guard = set_env_var("PGDOG_BAN_TIMEOUT", "5m"); + assert_eq!(General::ban_timeout(), 300_000); + + let _guard = set_env_var("PGDOG_BAN_TIMEOUT", "300000"); + assert_eq!(General::ban_timeout(), 300_000); + + let _guard = set_env_var("PGDOG_BAN_TIMEOUT", "not a duration"); + assert_eq!(General::ban_timeout(), 300_000); + + let _guard = remove_env_var("PGDOG_BAN_TIMEOUT"); + assert_eq!(General::ban_timeout(), 300_000); + + let _guard = set_env_var("PGDOG_TWO_PHASE_COMMIT_WAL_CHECKPOINT_INTERVAL", "2m"); + assert_eq!(General::two_phase_commit_wal_checkpoint_interval(), 120); + + let _guard = set_env_var("PGDOG_DNS_TTL", "1h"); + assert_eq!(General::default_dns_ttl(), Some(3_600_000)); + } + #[test] fn test_frontend_query_size_limit_block() { let mut g = General::default(); diff --git a/pgdog-config/src/lib.rs b/pgdog-config/src/lib.rs index 142713fec..0d6cd4d6c 100644 --- a/pgdog-config/src/lib.rs +++ b/pgdog-config/src/lib.rs @@ -3,6 +3,7 @@ pub mod auth; pub mod core; pub mod data_types; pub mod database; +pub mod duration; pub mod error; pub mod general; pub mod memory; diff --git a/pgdog-config/src/networking.rs b/pgdog-config/src/networking.rs index da0a2aaca..87d2ed42b 100644 --- a/pgdog-config/src/networking.rs +++ b/pgdog-config/src/networking.rs @@ -62,10 +62,14 @@ pub struct Tcp { /// _Default:_ system default (2 hours) /// /// + #[serde(default, deserialize_with = "crate::duration::millis_optional")] + #[schemars(with = "Option")] time: Option, /// Time between successive keep-alive probes. Milliseconds. /// /// + #[serde(default, deserialize_with = "crate::duration::millis_optional")] + #[schemars(with = "Option")] interval: Option, /// How many consecutive failed keep-alive probes before the connection is terminated. /// @@ -76,6 +80,8 @@ pub struct Tcp { /// **Note:** Linux only. /// /// + #[serde(default, deserialize_with = "crate::duration::millis_optional")] + #[schemars(with = "Option")] user_timeout: Option, /// TCP congestion control algorithm (e.g. `"reno"`, `"cubic"`). /// diff --git a/pgdog-config/src/otel.rs b/pgdog-config/src/otel.rs index 6ba7ed51b..bd2d15afb 100644 --- a/pgdog-config/src/otel.rs +++ b/pgdog-config/src/otel.rs @@ -59,7 +59,11 @@ pub struct Otel { /// _Default:_ `10000` /// /// Env: `OTEL_METRIC_EXPORT_INTERVAL` - #[serde(default = "Otel::push_interval")] + #[serde( + default = "Otel::push_interval", + deserialize_with = "crate::duration::millis" + )] + #[schemars(with = "crate::duration::TimeValue")] pub push_interval: u64, } diff --git a/pgdog-config/src/replication.rs b/pgdog-config/src/replication.rs index 67e90de81..ad413fd73 100644 --- a/pgdog-config/src/replication.rs +++ b/pgdog-config/src/replication.rs @@ -6,9 +6,9 @@ use std::time::Duration; #[derive(Deserialize)] struct RawReplicaLag { - #[serde(default)] + #[serde(default, deserialize_with = "crate::duration::millis_optional")] check_interval: Option, - #[serde(default)] + #[serde(default, deserialize_with = "crate::duration::millis_optional")] max_age: Option, } @@ -18,11 +18,13 @@ pub struct ReplicaLag { /// How often to check replica lag, in milliseconds. /// /// _Default:_ `1000` + #[schemars(with = "crate::duration::TimeValue")] pub check_interval: Duration, /// Maximum allowed replication lag before a replica is banned, in milliseconds. /// /// _Default:_ `25` + #[schemars(with = "crate::duration::TimeValue")] pub max_age: Duration, } diff --git a/pgdog-config/src/users.rs b/pgdog-config/src/users.rs index 3b6fe0529..bf86d0cac 100644 --- a/pgdog-config/src/users.rs +++ b/pgdog-config/src/users.rs @@ -338,6 +338,8 @@ pub struct User { /// **Note:** Nothing is preventing the user from manually changing this setting at runtime, e.g., by running `SET statement_timeout TO 0`; /// /// + #[serde(default, deserialize_with = "crate::duration::millis_optional")] + #[schemars(with = "Option")] pub statement_timeout: Option, /// Lock timeout. /// @@ -351,6 +353,8 @@ pub struct User { /// e.g., by running `SET lock_timeout TO 0`; /// /// + #[serde(default, deserialize_with = "crate::duration::millis_optional")] + #[schemars(with = "Option")] pub lock_timeout: Option, /// Sets the `replication=database` parameter on user connections to Postgres. Allows this user to use replication commands. /// @@ -364,6 +368,8 @@ pub struct User { /// Overrides [`idle_timeout`](https://docs.pgdog.dev/configuration/pgdog.toml/general/#idle_timeout) for this user. Server connections that have been idle for this long, without affecting [`min_pool_size`](https://docs.pgdog.dev/configuration/pgdog.toml/general/#min_pool_size), will be closed. /// /// + #[serde(default, deserialize_with = "crate::duration::millis_optional")] + #[schemars(with = "Option")] pub idle_timeout: Option, /// Sets `default_transaction_read_only` to `on` for all connections. pub read_only: Option, @@ -381,9 +387,13 @@ pub struct User { /// pub two_phase_commit_auto: Option, /// Server connections older than this (in milliseconds) will be closed when returned to the pool. + #[serde(default, deserialize_with = "crate::duration::millis_optional")] + #[schemars(with = "Option")] pub server_lifetime: Option, /// Maximum random adjustment applied to `server_lifetime` per backend connection (milliseconds). /// Overrides the database-level and general-level `server_lifetime_jitter` setting for this user. + #[serde(default, deserialize_with = "crate::duration::millis_optional")] + #[schemars(with = "Option")] pub server_lifetime_jitter: Option, } diff --git a/pgdog-config/src/vault.rs b/pgdog-config/src/vault.rs index 5ffc6b195..499aa822c 100644 --- a/pgdog-config/src/vault.rs +++ b/pgdog-config/src/vault.rs @@ -60,6 +60,8 @@ pub struct Vault { /// Seconds before the Vault client token expires to trigger a re-login. /// /// _Default:_ `60` + #[serde(default, deserialize_with = "crate::duration::seconds_optional")] + #[schemars(with = "Option")] pub client_token_ttl: Option, }