Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,7 @@ private void initToolTips() {
receiveTimeoutField.setToolTipText("The amount of time, in milliseconds, to wait without receiving a message before closing a connection.");
bufferSizeField.setToolTipText("<html>Use larger values for larger messages, and smaller values <br>for smaller messages. Generally, the default value is fine.</html>");

toolTipText = "<html>Select No to close the listening socket after a received message has finished processing.<br/>Otherwise the socket will remain open until the sending system closes it. In that case,<br/>messages will only be processed if data is received and either the receive timeout is reached,<br/>the client closes the socket, or an end of message byte sequence has been detected.</html>";
toolTipText = "<html>Select No to close the listening socket after a received message has finished processing.<br/>Otherwise the socket will remain open until the sending system closes it. In that case,<br/>messages will only be processed if data is received and either the receive timeout is reached,<br/>the client closes the socket, or an end of message byte sequence has been detected.<br/><br/>With Yes, TCP keepalive probes start after the connection has been idle for 120 seconds,<br/>or for the server's OS keepalive time if that is lower. The server's oie.tcp.keepalive.idle<br/>system property changes the 120 seconds.</html>";
keepConnectionOpenYesRadio.setToolTipText(toolTipText);
keepConnectionOpenNoRadio.setToolTipText(toolTipText);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ public void actionPerformed(ActionEvent evt) {
keepConnectionOpenYesRadio.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
keepConnectionOpenGroup.add(keepConnectionOpenYesRadio);
keepConnectionOpenYesRadio.setText("Yes");
keepConnectionOpenYesRadio.setToolTipText("<html>Select Yes to keep the connection to the host open across multiple messages.<br>Select No to immediately the close the connection to the host after sending each message.</html>");
keepConnectionOpenYesRadio.setToolTipText("<html>Select Yes to keep the connection to the host open across multiple messages.<br>Select No to immediately the close the connection to the host after sending each message.<br><br>With Yes, TCP keepalive probes start after the connection has been idle for 120 seconds,<br>or for the server's OS keepalive time if that is lower. The server's oie.tcp.keepalive.idle<br>system property changes the 120 seconds.</html>");
keepConnectionOpenYesRadio.setMargin(new Insets(0, 0, 0, 0));
keepConnectionOpenYesRadio.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent evt) {
Expand All @@ -452,7 +452,7 @@ public void actionPerformed(ActionEvent evt) {
keepConnectionOpenNoRadio.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
keepConnectionOpenGroup.add(keepConnectionOpenNoRadio);
keepConnectionOpenNoRadio.setText("No");
keepConnectionOpenNoRadio.setToolTipText("<html>Select Yes to keep the connection to the host open across multiple messages.<br>Select No to immediately the close the connection to the host after sending each message.</html>");
keepConnectionOpenNoRadio.setToolTipText("<html>Select Yes to keep the connection to the host open across multiple messages.<br>Select No to immediately the close the connection to the host after sending each message.<br><br>With Yes, TCP keepalive probes start after the connection has been idle for 120 seconds,<br>or for the server's OS keepalive time if that is lower. The server's oie.tcp.keepalive.idle<br>system property changes the 120 seconds.</html>");
keepConnectionOpenNoRadio.setMargin(new Insets(0, 0, 0, 0));
keepConnectionOpenNoRadio.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent evt) {
Expand Down
8 changes: 8 additions & 0 deletions server/conf/custom.vmoptions
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@
#
# Any lines which do not match the special directives above will be passed through directly as
# arguments to the java binary.
#
# Engine options set with -D:
#
# -Doie.tcp.keepalive.idle=<seconds>
# : seconds a TCP connection with keepalive enabled can sit idle before the first
# : keepalive probe is sent. Defaults to 120, which keeps idle connections alive
# : through NAT devices and load balancers. Only lowers the OS default; a lower OS
# : setting is kept. 0 or less always uses the OS default.



Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,28 @@
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import com.mirth.connect.util.TcpUtil;

import jdk.net.ExtendedSocketOptions;

public class SocketUtil {

private static final Logger logger = LogManager.getLogger(SocketUtil.class);

static final String KEEPALIVE_IDLE_PROPERTY = "oie.tcp.keepalive.idle";
static final int DEFAULT_KEEPALIVE_IDLE = 120;
private static final Optional<Integer> KEEPALIVE_IDLE = initKeepAliveIdle();
private static final AtomicBoolean keepAliveIdleFailed = new AtomicBoolean();

public static Socket createSocket(TcpConfiguration configuration) throws UnknownHostException, IOException {
return createSocket(configuration, null);
}
Expand Down Expand Up @@ -67,6 +81,72 @@ public static void connectSocket(Socket socket, String host, int port, int timeo
socket.connect(new InetSocketAddress(InetAddress.getByName(TcpUtil.getFixedHost(host)), port), timeout);
}

/**
* Loads this class, which decides and logs the keepalive idle time. Called when the TCP plugin
* starts so the decision, and any error in the oie.tcp.keepalive.idle system property, is
* logged at server startup instead of at the first TCP connection.
*/
public static void init() {}

/**
* Enables or disables TCP keepalive on a socket. When enabling, also lowers the idle time before
* the first keepalive probe to the oie.tcp.keepalive.idle system property (in seconds) if the
* OS default was higher when this class was loaded and the socket supports it. A value of 0 or
* less leaves the OS default.
*/
public static void setKeepAlive(Socket socket, boolean keepAlive) throws SocketException {
socket.setKeepAlive(keepAlive);

if (keepAlive && KEEPALIVE_IDLE.isPresent() && socket.supportedOptions().contains(ExtendedSocketOptions.TCP_KEEPIDLE)) {
Integer idle = KEEPALIVE_IDLE.get();
try {
socket.setOption(ExtendedSocketOptions.TCP_KEEPIDLE, idle);
} catch (IOException | IllegalArgumentException e) {
if (keepAliveIdleFailed.compareAndSet(false, true)) {
logger.error("Failed to set TCP_KEEPIDLE to {} seconds; using the OS default. Further failures will be logged at DEBUG.", idle, e);
} else {
logger.debug("Failed to set TCP_KEEPIDLE to {} seconds.", idle, e);
}
}
}
}

private static Optional<Integer> initKeepAliveIdle() {
int idle = DEFAULT_KEEPALIVE_IDLE;
String value = System.getProperty(KEEPALIVE_IDLE_PROPERTY);
if (value != null) {
try {
idle = Integer.parseInt(value.trim());
} catch (NumberFormatException e) {
logger.error("Invalid value '{}' for {}; using {} seconds.", value, KEEPALIVE_IDLE_PROPERTY, DEFAULT_KEEPALIVE_IDLE);
}
}

if (idle <= 0) {
logger.info("TCP connectors use the OS keepalive idle time; {} is {}.", KEEPALIVE_IDLE_PROPERTY, idle);
return Optional.empty();
}

try (Socket socket = new Socket()) {
if (!socket.supportedOptions().contains(ExtendedSocketOptions.TCP_KEEPIDLE)) {
logger.info("TCP connectors use the OS keepalive idle time; TCP_KEEPIDLE is not supported.");
return Optional.empty();
}

int osIdle = socket.getOption(ExtendedSocketOptions.TCP_KEEPIDLE);
if (osIdle <= idle) {
logger.info("TCP connectors use the OS keepalive idle time; its current value of {} seconds is not higher than {} ({} seconds). Later changes to the OS value still apply, but {} is not compared again until the server restarts.", osIdle, KEEPALIVE_IDLE_PROPERTY, idle, KEEPALIVE_IDLE_PROPERTY);
return Optional.empty();
}

logger.info("TCP connectors use a keepalive idle time of {} seconds instead of the current OS value of {} seconds. Later changes to the OS value are ignored until the server restarts.", idle, osIdle);
} catch (IOException e) {
logger.info("TCP connectors use a keepalive idle time of {} seconds; the current OS value could not be read. Later changes to the OS value are ignored until the server restarts.", idle);
}

return Optional.of(idle);
}

public static void closeSocket(Socket socket) throws IOException {
if (socket != null) {
/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ public Response send(ConnectorProperties connectorProperties, ConnectorMessage m
socket.setReceiveBufferSize(bufferSize);
socket.setSendBufferSize(bufferSize);
socket.setSoTimeout(responseTimeout);
socket.setKeepAlive(tcpDispatcherProperties.isKeepConnectionOpen());
SocketUtil.setKeepAlive(socket, tcpDispatcherProperties.isKeepConnectionOpen());

eventController.dispatchEvent(new ConnectorCountEvent(getChannelId(), getMetaDataId(), getDestinationName(), ConnectionStatusEventType.CONNECTED, SocketUtil.getLocalAddress(socket) + " -> " + SocketUtil.getInetAddress(socket), true));
}
Expand Down Expand Up @@ -810,7 +810,7 @@ private void initSocket(Socket socket) throws SocketException {
socket.setReceiveBufferSize(bufferSize);
socket.setSendBufferSize(bufferSize);
socket.setSoTimeout(responseTimeout);
socket.setKeepAlive(true);
SocketUtil.setKeepAlive(socket, true);
socket.setReuseAddress(true);
socket.setTcpNoDelay(true);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1067,7 +1067,7 @@ private void initSocket(Socket socket) throws SocketException {
socket.setReceiveBufferSize(bufferSize);
socket.setSendBufferSize(bufferSize);
socket.setSoTimeout(timeout);
socket.setKeepAlive(connectorProperties.isKeepConnectionOpen());
SocketUtil.setKeepAlive(socket, connectorProperties.isKeepConnectionOpen());
socket.setReuseAddress(true);
socket.setTcpNoDelay(true);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ public String getPluginPointName() {
}

@Override
public void start() {}
public void start() {
SocketUtil.init();
}

@Override
public void stop() {}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 Tony Germano <[email protected]>
package com.mirth.connect.connectors.tcp;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import java.net.Socket;

import org.junit.Assume;
import org.junit.Test;

import jdk.net.ExtendedSocketOptions;

public class SocketUtilTest {

@Test
public void setKeepAliveEnablesKeepAliveAndCapsIdle() throws Exception {
assertNull("Test expects " + SocketUtil.KEEPALIVE_IDLE_PROPERTY + " to be unset", System.getProperty(SocketUtil.KEEPALIVE_IDLE_PROPERTY));

try (Socket socket = new Socket()) {
Assume.assumeTrue(socket.supportedOptions().contains(ExtendedSocketOptions.TCP_KEEPIDLE));
int osIdle = socket.getOption(ExtendedSocketOptions.TCP_KEEPIDLE);

SocketUtil.setKeepAlive(socket, true);

assertTrue(socket.getKeepAlive());
assertEquals(Math.min(osIdle, SocketUtil.DEFAULT_KEEPALIVE_IDLE), (int) socket.getOption(ExtendedSocketOptions.TCP_KEEPIDLE));
}
}

@Test
public void setKeepAliveDisablesKeepAliveAndLeavesIdle() throws Exception {
try (Socket socket = new Socket()) {
socket.setKeepAlive(true);
Integer idle = socket.supportedOptions().contains(ExtendedSocketOptions.TCP_KEEPIDLE) ? socket.getOption(ExtendedSocketOptions.TCP_KEEPIDLE) : null;

SocketUtil.setKeepAlive(socket, false);

assertFalse(socket.getKeepAlive());
if (idle != null) {
assertEquals(idle, socket.getOption(ExtendedSocketOptions.TCP_KEEPIDLE));
}
}
}
}
Loading