Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions lib/postgres.dart
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,10 @@ class ConnectionSettings extends SessionSettings {
super.queryTimeout,
super.queryMode,
super.ignoreSuperfluousParameters,
super.keepAlive,
super.keepAliveIdle,
super.keepAliveInterval,
super.keepAliveCount,
});
}

Expand All @@ -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,
});
}

Expand Down
4 changes: 4 additions & 0 deletions lib/src/pool/pool_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ class PoolSettings extends ConnectionSettings {
super.ignoreSuperfluousParameters,
super.onOpen,
super.typeRegistry,
super.keepAlive,
super.keepAliveIdle,
super.keepAliveInterval,
super.keepAliveCount,
});
}

Expand Down
47 changes: 47 additions & 0 deletions lib/src/v3/connection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>();
// ignore: cancel_subscriptions
final subscription = socket.listen(
Expand Down Expand Up @@ -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;
Expand Down
15 changes: 14 additions & 1 deletion lib/src/v3/resolved_settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -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 &&
Expand Down