diff --git a/lib/postgres.dart b/lib/postgres.dart index fb4e429..a023746 100644 --- a/lib/postgres.dart +++ b/lib/postgres.dart @@ -534,6 +534,10 @@ class ConnectionSettings extends SessionSettings { super.queryTimeout, super.queryMode, super.ignoreSuperfluousParameters, + super.keepAlive, + super.keepAliveIdle, + super.keepAliveInterval, + super.keepAliveCount, }); } @@ -557,11 +561,39 @@ class SessionSettings { /// parameters are found. final bool? ignoreSuperfluousParameters; + /// Whether to enable TCP keep-alive on the socket connection. + /// + /// When enabled, sets `SO_KEEPALIVE` on the underlying TCP socket so that + /// the operating system will periodically send probes on idle connections + /// to detect broken peers. + /// + /// Defaults to `false`. Has no effect on Unix-domain socket connections. + final bool? keepAlive; + + /// Time a connection must be idle before the first keep-alive probe is sent. + /// Sets `TCP_KEEPIDLE` (Linux) / `TCP_KEEPALIVE` (macOS) per-socket. + /// Requires [keepAlive] to be `true`. Falls back to OS default if null. + final Duration? keepAliveIdle; + + /// Interval between successive keep-alive probes when no acknowledgement + /// is received. Sets `TCP_KEEPINTVL` per-socket. + /// Requires [keepAlive] to be `true`. Falls back to OS default if null. + final Duration? keepAliveInterval; + + /// Number of unacknowledged probes before the connection is considered dead. + /// Sets `TCP_KEEPCNT` per-socket. + /// Requires [keepAlive] to be `true`. Falls back to OS default if null. + final int? keepAliveCount; + const SessionSettings({ this.connectTimeout, this.queryTimeout, this.queryMode, this.ignoreSuperfluousParameters, + this.keepAlive, + this.keepAliveIdle, + this.keepAliveInterval, + this.keepAliveCount, }); } diff --git a/lib/src/pool/pool_api.dart b/lib/src/pool/pool_api.dart index b4af7ba..1e90b4b 100644 --- a/lib/src/pool/pool_api.dart +++ b/lib/src/pool/pool_api.dart @@ -42,6 +42,10 @@ class PoolSettings extends ConnectionSettings { super.ignoreSuperfluousParameters, super.onOpen, super.typeRegistry, + super.keepAlive, + super.keepAliveIdle, + super.keepAliveInterval, + super.keepAliveCount, }); } diff --git a/lib/src/v3/connection.dart b/lib/src/v3/connection.dart index 4d8e27c..746b85b 100644 --- a/lib/src/v3/connection.dart +++ b/lib/src/v3/connection.dart @@ -303,6 +303,13 @@ class PgConnectionImplementation extends _PgSessionBase implements Connection { timeout: settings.connectTimeout, ); + // Enable TCP keep-alive if requested and not using a Unix-domain socket. + // Must be set before any SSL upgrade because SecureSocket does not expose + // setRawOption. + if (settings.keepAlive && !endpoint.isUnixSocket) { + _enableKeepAlive(socket, settings); + } + final sslCompleter = Completer(); // ignore: cancel_subscriptions final subscription = socket.listen( @@ -404,6 +411,46 @@ class PgConnectionImplementation extends _PgSessionBase implements Connection { ); } + static void _enableKeepAlive( + Socket socket, + ResolvedConnectionSettings settings, + ) { + final isLinux = Platform.isLinux || Platform.isAndroid; + + final soKeepAlive = isLinux ? 0x0009 : 0x0008; + socket.setRawOption( + RawSocketOption.fromBool(RawSocketOption.levelSocket, soKeepAlive, true), + ); + + final idle = settings.keepAliveIdle?.inSeconds; + final interval = settings.keepAliveInterval?.inSeconds; + final count = settings.keepAliveCount; + + if (idle != null) { + // TCP_KEEPIDLE (Linux=4) / TCP_KEEPALIVE (macOS=0x10, Windows=4) + final opt = Platform.isMacOS || Platform.isIOS ? 0x10 : 4; + socket.setRawOption( + RawSocketOption.fromInt(RawSocketOption.levelTcp, opt, idle), + ); + } + + if (interval != null) { + // TCP_KEEPINTVL: Linux=5, macOS=0x101, Windows=5 + final opt = Platform.isMacOS || Platform.isIOS ? 0x101 : 5; + socket.setRawOption( + RawSocketOption.fromInt(RawSocketOption.levelTcp, opt, interval), + ); + } + + if (count != null) { + // TCP_KEEPCNT: Linux=6, macOS=0x102, Windows=6 + final opt = Platform.isMacOS || Platform.isIOS ? 0x102 : 6; + socket.setRawOption( + RawSocketOption.fromInt(RawSocketOption.levelTcp, opt, count), + ); + } + } + final Endpoint _endpoint; @override final ResolvedConnectionSettings _settings; diff --git a/lib/src/v3/resolved_settings.dart b/lib/src/v3/resolved_settings.dart index 3b9ce5f..4c8bb94 100644 --- a/lib/src/v3/resolved_settings.dart +++ b/lib/src/v3/resolved_settings.dart @@ -15,6 +15,14 @@ class ResolvedSessionSettings implements SessionSettings { final QueryMode queryMode; @override final bool ignoreSuperfluousParameters; + @override + final bool keepAlive; + @override + final Duration? keepAliveIdle; + @override + final Duration? keepAliveInterval; + @override + final int? keepAliveCount; ResolvedSessionSettings(SessionSettings? settings, SessionSettings? fallback) : connectTimeout = @@ -30,7 +38,12 @@ class ResolvedSessionSettings implements SessionSettings { ignoreSuperfluousParameters = settings?.ignoreSuperfluousParameters ?? fallback?.ignoreSuperfluousParameters ?? - false; + false, + keepAlive = settings?.keepAlive ?? fallback?.keepAlive ?? false, + keepAliveIdle = settings?.keepAliveIdle ?? fallback?.keepAliveIdle, + keepAliveInterval = + settings?.keepAliveInterval ?? fallback?.keepAliveInterval, + keepAliveCount = settings?.keepAliveCount ?? fallback?.keepAliveCount; bool isMatchingSession(ResolvedSessionSettings other) { return connectTimeout == other.connectTimeout &&