diff --git a/docs/docs/start/config.md b/docs/docs/start/config.md index 25b7fd6639..ffff05e39d 100644 --- a/docs/docs/start/config.md +++ b/docs/docs/start/config.md @@ -256,6 +256,9 @@ The following database options can be configured: {{ configsetting("INVENTREE_DB_PASSWORD") }} Database password (if required) | {{ configsetting("INVENTREE_DB_HOST") }} Database host address (if required) | {{ configsetting("INVENTREE_DB_PORT") }} Database host port (if required) | +{{ configsetting("INVENTREE_DB_CONN_MAX_AGE") }} Database connection max age (s) | +{{ configsetting("INVENTREE_DB_CONN_HEALTH_CHECKS") }} Enable database connection health checks | + {{ configsetting("INVENTREE_DB_OPTIONS") }} Additional database options (as a JSON object) | !!! tip "Database Password" @@ -266,11 +269,12 @@ The following database options can be configured: If running with a PostgreSQL database backend, the following additional options are available: {{ configtable() }} -{{ configsetting("INVENTREE_DB_TIMEOUT", default="2") }} Database connection timeout (s) | +{{ configsetting("INVENTREE_DB_TIMEOUT", default="10") }} Database connection timeout (s) | | `INVENTREE_DB_TCP_KEEPALIVES` | database.tcp_keepalives | 1 | TCP keepalive | -| `INVENTREE_DB_TCP_KEEPALIVES_IDLE` | database.tcp_keepalives_idle | 1 | Idle TCP keepalive | -| `INVENTREE_DB_TCP_KEEPALIVES_INTERVAL` | database.tcp_keepalives_interval | 1| TCP keepalive interval | +| `INVENTREE_DB_TCP_KEEPALIVES_IDLE` | database.tcp_keepalives_idle | 5 | Idle TCP keepalive | +| `INVENTREE_DB_TCP_KEEPALIVES_INTERVAL` | database.tcp_keepalives_interval | 5 | TCP keepalive interval | | `INVENTREE_DB_TCP_KEEPALIVES_COUNT` | database.tcp_keepalives_count | 5 | TCP keepalive count | +| `INVENTREE_DB_TCP_USER_TIMEOUT` | database.tcp_user_timeout | 2000 | TCP user timeout (ms) | | `INVENTREE_DB_ISOLATION_SERIALIZABLE` | database.serializable | False | Database isolation level configured to "serializable" | ### MySQL Settings @@ -278,6 +282,7 @@ If running with a PostgreSQL database backend, the following additional options If running with a MySQL database backend, the following additional options are available: {{ configtable() }} +{{ configsetting("INVENTREE_DB_TIMEOUT", default="10") }} Database connection timeout (s) | | `INVENTREE_DB_ISOLATION_SERIALIZABLE` | database.serializable | False | Database isolation level configured to "serializable" | ### SQLite Settings diff --git a/src/backend/InvenTree/InvenTree/setting/db_backend.py b/src/backend/InvenTree/InvenTree/setting/db_backend.py index 3cedea07d4..6defd52374 100644 --- a/src/backend/InvenTree/InvenTree/setting/db_backend.py +++ b/src/backend/InvenTree/InvenTree/setting/db_backend.py @@ -5,10 +5,23 @@ from pathlib import Path import structlog from InvenTree.config import get_boolean_setting, get_config_value, get_setting +from InvenTree.ready import isInWorkerThread logger = structlog.get_logger('inventree') +def _get_conn_max_age() -> int | None: + """Return the configured CONN_MAX_AGE value. + + Accepts an integer number of seconds, or 'None' for unlimited persistence. + Defaults to 0 (close the connection after each request). + """ + value = get_setting('INVENTREE_DB_CONN_MAX_AGE', 'database.conn_max_age', 0) + if value is None or str(value).strip().lower() == 'none': + return None + return int(value) + + def get_db_backend(): """Return the database backend configuration.""" # For the core database configuration values, we test for UPPERCASE configuration values as a backup, @@ -43,6 +56,13 @@ def get_db_backend(): ) or get_config_value('database.OPTIONS') or {}, + # Seconds to keep idle connections open across requests (0 = close after each request). + # Set to None for unlimited. Enable CONN_HEALTH_CHECKS alongside any non-zero value + # so stale connections are detected before use rather than causing request failures. + 'CONN_MAX_AGE': _get_conn_max_age(), + 'CONN_HEALTH_CHECKS': get_boolean_setting( + 'INVENTREE_DB_CONN_HEALTH_CHECKS', 'database.conn_health_checks', False + ), } # Check for required keys @@ -113,9 +133,9 @@ def set_postgres_options(db_options: dict): if 'connect_timeout' not in db_options: # The DB server is in the same data center, it should not take very # long to connect to the database server - # # seconds, 2 is minimum allowed by libpq - db_options['connect_timeout'] = int( - get_setting('INVENTREE_DB_TIMEOUT', 'database.timeout', 2) + # Note: 2 seconds is minimum allowed by libpq + db_options['connect_timeout'] = max( + 2, int(get_setting('INVENTREE_DB_TIMEOUT', 'database.timeout', 10)) ) # Setup TCP keepalive @@ -133,7 +153,7 @@ def set_postgres_options(db_options: dict): if 'keepalives_idle' not in db_options: db_options['keepalives_idle'] = int( get_setting( - 'INVENTREE_DB_TCP_KEEPALIVES_IDLE', 'database.tcp_keepalives_idle', 1 + 'INVENTREE_DB_TCP_KEEPALIVES_IDLE', 'database.tcp_keepalives_idle', 5 ) ) @@ -143,7 +163,7 @@ def set_postgres_options(db_options: dict): get_setting( 'INVENTREE_DB_TCP_KEEPALIVES_INTERVAL', 'database.tcp_keepalives_interval', - '1', + '5', ) ) @@ -157,10 +177,13 @@ def set_postgres_options(db_options: dict): ) ) - # # Milliseconds for how long pending data should remain unacked - # by the remote server - # TODO: Supported starting in PSQL 11 - # "tcp_user_timeout": int(os.getenv("PGTCP_USER_TIMEOUT", "1000"), + # # Milliseconds for how long pending data should remain unacked by the remote server + if 'tcp_user_timeout' not in db_options: + db_options['tcp_user_timeout'] = int( + get_setting( + 'INVENTREE_DB_TCP_USER_TIMEOUT', 'database.tcp_user_timeout', '2000' + ) + ) # Postgres's default isolation level is Read Committed which is # normally fine, but most developers think the database server is @@ -178,10 +201,21 @@ def set_postgres_options(db_options: dict): else IsolationLevel.READ_COMMITTED ) + # Specify the application name for the database connection + # This can be useful for debugging and monitoring purposes + if 'application_name' not in db_options: + db_options['application_name'] = ( + 'inventree-worker' if isInWorkerThread() else 'inventree-server' + ) + def set_mysql_options(db_options: dict): """Set database options specific to mysql backend.""" - # TODO TCP time outs and keepalives + # Timeout values + if 'connect_timeout' not in db_options: + db_options['connect_timeout'] = int( + get_setting('INVENTREE_DB_TIMEOUT', 'database.timeout', 10) + ) # MariaDB's default isolation level is Repeatable Read which is # normally fine, but most developers think the database server is