HAProxy Configuration: network.loadbalancer.haproxy.idle.timeout (#12586)

* initial attempt at network.loadbalancer.haproxy.idle.timeout implementation

* implement test cases

* move idleTimeout configuration test to its own test case
This commit is contained in:
Brad House 2026-04-16 05:19:54 -04:00 committed by GitHub
parent e0fe953791
commit 6e810989b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 105 additions and 15 deletions

View File

@ -36,6 +36,7 @@ public class LoadBalancerConfigCommand extends NetworkElementCommand {
public String lbStatsAuth = "admin1:AdMiN123";
public String lbStatsUri = "/admin?stats";
public String maxconn = "";
public Long idleTimeout = 50000L; /* 0=infinite, >0 = timeout in milliseconds */
public String lbProtocol;
public boolean keepAliveEnabled = false;
NicTO nic;
@ -50,7 +51,7 @@ public class LoadBalancerConfigCommand extends NetworkElementCommand {
}
public LoadBalancerConfigCommand(LoadBalancerTO[] loadBalancers, String publicIp, String guestIp, String privateIp, NicTO nic, Long vpcId, String maxconn,
boolean keepAliveEnabled) {
boolean keepAliveEnabled, Long idleTimeout) {
this.loadBalancers = loadBalancers;
this.lbStatsPublicIP = publicIp;
this.lbStatsPrivateIP = privateIp;
@ -59,6 +60,7 @@ public class LoadBalancerConfigCommand extends NetworkElementCommand {
this.vpcId = vpcId;
this.maxconn = maxconn;
this.keepAliveEnabled = keepAliveEnabled;
this.idleTimeout = idleTimeout;
}
public NicTO getNic() {

View File

@ -638,6 +638,19 @@ public class HAProxyConfigurator implements LoadBalancerConfigurator {
if (lbCmd.keepAliveEnabled) {
dSection.set(7, "\tno option httpclose");
}
if (lbCmd.idleTimeout > 0) {
dSection.set(9, "\ttimeout client " + Long.toString(lbCmd.idleTimeout));
dSection.set(10, "\ttimeout server " + Long.toString(lbCmd.idleTimeout));
} else if (lbCmd.idleTimeout == 0) {
// .remove() is not allowed, only .set() operations are allowed as the list
// is a fixed size. So lets just mark the entry as blank.
dSection.set(9, "");
dSection.set(10, "");
} else {
// Negative idleTimeout values are considered invalid; retain the
// default HAProxy timeout values from defaultsSection for predictability.
logger.warn("Negative idleTimeout ({}) configured; retaining default HAProxy timeouts.", lbCmd.idleTimeout);
}
if (logger.isDebugEnabled()) {
for (final String s : dSection) {

View File

@ -235,7 +235,7 @@ public class ConfigHelperTest {
lbs.toArray(arrayLbs);
final NicTO nic = new NicTO();
final LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(arrayLbs, "64.10.2.10", "10.1.10.2", "192.168.1.2", nic, null, "1000", false);
final LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(arrayLbs, "64.10.2.10", "10.1.10.2", "192.168.1.2", nic, null, "1000", false, 0L);
cmd.setAccessDetail(NetworkElementCommand.ROUTER_IP, "10.1.10.2");
cmd.setAccessDetail(NetworkElementCommand.ROUTER_NAME, ROUTERNAME);

View File

@ -783,7 +783,7 @@ public class VirtualRoutingResourceTest implements VirtualRouterDeployer {
final LoadBalancerTO[] arrayLbs = new LoadBalancerTO[lbs.size()];
lbs.toArray(arrayLbs);
final NicTO nic = new NicTO();
final LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(arrayLbs, "64.10.2.10", "10.1.10.2", "192.168.1.2", nic, null, "1000", false);
final LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(arrayLbs, "64.10.2.10", "10.1.10.2", "192.168.1.2", nic, null, "1000", false, 50000L);
cmd.setAccessDetail(NetworkElementCommand.ROUTER_IP, "10.1.10.2");
cmd.setAccessDetail(NetworkElementCommand.ROUTER_NAME, ROUTERNAME);
return cmd;
@ -799,7 +799,7 @@ public class VirtualRoutingResourceTest implements VirtualRouterDeployer {
lbs.toArray(arrayLbs);
final NicTO nic = new NicTO();
nic.setIp("10.1.10.2");
final LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(arrayLbs, "64.10.2.10", "10.1.10.2", "192.168.1.2", nic, Long.valueOf(1), "1000", false);
final LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(arrayLbs, "64.10.2.10", "10.1.10.2", "192.168.1.2", nic, Long.valueOf(1), "1000", false, 50000L);
cmd.setAccessDetail(NetworkElementCommand.ROUTER_IP, "10.1.10.2");
cmd.setAccessDetail(NetworkElementCommand.ROUTER_NAME, ROUTERNAME);
return cmd;

View File

@ -79,13 +79,14 @@ public class HAProxyConfiguratorTest {
LoadBalancerTO[] lba = new LoadBalancerTO[1];
lba[0] = lb;
HAProxyConfigurator hpg = new HAProxyConfigurator();
LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(lba, "10.0.0.1", "10.1.0.1", "10.1.1.1", null, 1L, "12", false);
LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(lba, "10.0.0.1", "10.1.0.1", "10.1.1.1", null, 1L, "12", false, 0L);
String result = genConfig(hpg, cmd);
assertTrue("keepalive disabled should result in 'option httpclose' in the resulting haproxy config", result.contains("\toption httpclose"));
cmd = new LoadBalancerConfigCommand(lba, "10.0.0.1", "10.1.0.1", "10.1.1.1", null, 1L, "4", true);
cmd = new LoadBalancerConfigCommand(lba, "10.0.0.1", "10.1.0.1", "10.1.1.1", null, 1L, "4", true, 0L);
result = genConfig(hpg, cmd);
assertTrue("keepalive enabled should result in 'no option httpclose' in the resulting haproxy config", result.contains("\tno option httpclose"));
// TODO
// create lb command
// setup tests for
@ -93,6 +94,27 @@ public class HAProxyConfiguratorTest {
// httpmode
}
/**
* Test method for {@link com.cloud.network.HAProxyConfigurator#generateConfiguration(com.cloud.agent.api.routing.LoadBalancerConfigCommand)}.
*/
@Test
public void testGenerateConfigurationLoadBalancerIdleTimeoutConfigCommand() {
LoadBalancerTO lb = new LoadBalancerTO("1", "10.2.0.1", 80, "http", "bla", false, false, false, null);
LoadBalancerTO[] lba = new LoadBalancerTO[1];
lba[0] = lb;
HAProxyConfigurator hpg = new HAProxyConfigurator();
LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(lba, "10.0.0.1", "10.1.0.1", "10.1.1.1", null, 1L, "4", true, 0L);
String result = genConfig(hpg, cmd);
assertTrue("idleTimeout of 0 should not generate 'timeout server' in the resulting haproxy config", !result.contains("\ttimeout server"));
assertTrue("idleTimeout of 0 should not generate 'timeout client' in the resulting haproxy config", !result.contains("\ttimeout client"));
cmd = new LoadBalancerConfigCommand(lba, "10.0.0.1", "10.1.0.1", "10.1.1.1", null, 1L, "4", true, 1234L);
result = genConfig(hpg, cmd);
assertTrue("idleTimeout of 1234 should result in 'timeout server 1234' in the resulting haproxy config", result.contains("\ttimeout server 1234"));
assertTrue("idleTimeout of 1234 should result in 'timeout client 1234' in the resulting haproxy config", result.contains("\ttimeout client 1234"));
}
/**
* Test method for {@link com.cloud.network.HAProxyConfigurator#generateConfiguration(com.cloud.agent.api.routing.LoadBalancerConfigCommand)}.
*/
@ -106,7 +128,7 @@ public class HAProxyConfiguratorTest {
LoadBalancerTO[] lba = new LoadBalancerTO[1];
lba[0] = lb;
HAProxyConfigurator hpg = new HAProxyConfigurator();
LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(lba, "10.0.0.1", "10.1.0.1", "10.1.1.1", null, 1L, "12", false);
LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(lba, "10.0.0.1", "10.1.0.1", "10.1.1.1", null, 1L, "12", false, 0L);
String result = genConfig(hpg, cmd);
assertTrue("'send-proxy' should result if protocol is 'tcp-proxy'", result.contains("send-proxy"));
}
@ -118,7 +140,7 @@ public class HAProxyConfiguratorTest {
LoadBalancerTO[] lba = new LoadBalancerTO[1];
lba[0] = lb;
HAProxyConfigurator hpg = new HAProxyConfigurator();
LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(lba, "10.0.0.1", "10.1.0.1", "10.1.1.1", null, 1L, "12", false);
LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(lba, "10.0.0.1", "10.1.0.1", "10.1.1.1", null, 1L, "12", false, 0L);
String result = genConfig(hpg, cmd);
Assert.assertTrue(result.contains("acl network_allowed src 1.1.1.1 2.2.2.2/24 \n\ttcp-request connection reject if !network_allowed"));
}
@ -131,7 +153,7 @@ public class HAProxyConfiguratorTest {
LoadBalancerTO[] lba = new LoadBalancerTO[1];
lba[0] = lb;
HAProxyConfigurator hpg = new HAProxyConfigurator();
LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(lba, "10.0.0.1", "10.1.0.1", "10.1.1.1", null, 1L, "12", false);
LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(lba, "10.0.0.1", "10.1.0.1", "10.1.1.1", null, 1L, "12", false, 0L);
String result = genConfig(hpg, cmd);
Assert.assertTrue(result.contains("bind 10.2.0.1:443 ssl crt /etc/cloudstack/ssl/10_2_0_1-443.pem"));
}

View File

@ -122,6 +122,14 @@ public interface NetworkOrchestrationService {
"Load Balancer(haproxy) maximum number of concurrent connections(global max)",
true,
Scope.Global);
ConfigKey<Long> NETWORK_LB_HAPROXY_IDLE_TIMEOUT = new ConfigKey<>(
"Network",
Long.class,
"network.loadbalancer.haproxy.idle.timeout",
"50000",
"Load Balancer(haproxy) idle timeout in milliseconds. Use 0 for infinite.",
true,
Scope.Global);
List<? extends Network> setupNetwork(Account owner, NetworkOffering offering, DeploymentPlan plan, String name, String displayText, boolean isDefault)
throws ConcurrentOperationException;

View File

@ -4940,6 +4940,7 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra
return new ConfigKey<?>[]{NetworkGcWait, NetworkGcInterval, NetworkLockTimeout, DeniedRoutes,
GuestDomainSuffix, NetworkThrottlingRate, MinVRVersion,
PromiscuousMode, MacAddressChanges, ForgedTransmits, MacLearning, RollingRestartEnabled,
TUNGSTEN_ENABLED, NSX_ENABLED, NETRIS_ENABLED, NETWORK_LB_HAPROXY_MAX_CONN};
TUNGSTEN_ENABLED, NSX_ENABLED, NETRIS_ENABLED, NETWORK_LB_HAPROXY_MAX_CONN,
NETWORK_LB_HAPROXY_IDLE_TIMEOUT};
}
}

View File

@ -214,7 +214,8 @@ public class ElasticLoadBalancerManagerImpl extends ManagerBase implements Elast
maxconn = offering.getConcurrentConnections().toString();
}
LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(lbs, elbVm.getPublicIpAddress(), _nicDao.getIpAddress(guestNetworkId, elbVm.getId()),
elbVm.getPrivateIpAddress(), null, null, maxconn, offering.isKeepAliveEnabled());
elbVm.getPrivateIpAddress(), null, null, maxconn, offering.isKeepAliveEnabled(),
NetworkOrchestrationService.NETWORK_LB_HAPROXY_IDLE_TIMEOUT.value());
cmd.setAccessDetail(NetworkElementCommand.ROUTER_IP, elbVm.getPrivateIpAddress());
cmd.setAccessDetail(NetworkElementCommand.ROUTER_NAME, elbVm.getInstanceName());
//FIXME: why are we setting attributes directly? Ick!! There should be accessors and

View File

@ -513,7 +513,8 @@ public class InternalLoadBalancerVMManagerImpl extends ManagerBase implements In
}
final LoadBalancerConfigCommand cmd =
new LoadBalancerConfigCommand(lbs, guestNic.getIPv4Address(), guestNic.getIPv4Address(), internalLbVm.getPrivateIpAddress(), _itMgr.toNicTO(guestNicProfile,
internalLbVm.getHypervisorType()), internalLbVm.getVpcId(), maxconn, offering.isKeepAliveEnabled());
internalLbVm.getHypervisorType()), internalLbVm.getVpcId(), maxconn, offering.isKeepAliveEnabled(),
NetworkOrchestrationService.NETWORK_LB_HAPROXY_IDLE_TIMEOUT.value());
cmd.lbStatsVisibility = _configDao.getValue(Config.NetworkLBHaproxyStatsVisbility.key());
cmd.lbStatsUri = _configDao.getValue(Config.NetworkLBHaproxyStatsUri.key());

View File

@ -396,7 +396,8 @@ public class CommandSetupHelper {
}
final LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(lbs, routerPublicIp, _routerControlHelper.getRouterIpInNetwork(guestNetworkId, router.getId()),
router.getPrivateIpAddress(), _itMgr.toNicTO(nicProfile, router.getHypervisorType()), router.getVpcId(), maxconn, offering.isKeepAliveEnabled());
router.getPrivateIpAddress(), _itMgr.toNicTO(nicProfile, router.getHypervisorType()), router.getVpcId(), maxconn, offering.isKeepAliveEnabled(),
NetworkOrchestrationService.NETWORK_LB_HAPROXY_IDLE_TIMEOUT.value());
cmd.lbStatsVisibility = _configDao.getValue(Config.NetworkLBHaproxyStatsVisbility.key());
cmd.lbStatsUri = _configDao.getValue(Config.NetworkLBHaproxyStatsUri.key());

View File

@ -1688,7 +1688,7 @@ Configurable, StateListener<VirtualMachine.State, VirtualMachine.Event, VirtualM
} else {
loadBalancingData.append("maxconn=").append(offering.getConcurrentConnections());
}
loadBalancingData.append(",idletimeout=").append(NetworkOrchestrationService.NETWORK_LB_HAPROXY_IDLE_TIMEOUT.value());
loadBalancingData.append(",sourcePortStart=").append(firewallRuleVO.getSourcePortStart())
.append(",sourcePortEnd=").append(firewallRuleVO.getSourcePortEnd());
if (firewallRuleVO instanceof LoadBalancerVO) {

View File

@ -28,6 +28,46 @@ def checkMaxconn(haproxyData, haCfgSections):
return True
def checkIdletimeout(haproxyData, haCfgSections):
if "idletimeout" not in haproxyData:
return True
# Normalize idletimeout value to string for comparison
idle_value = str(haproxyData["idletimeout"]).strip()
# Safely get the defaults section and its timeout directives
defaults_section = haCfgSections.get("defaults", {})
timeout_lines = defaults_section.get("timeout", [])
# Extract client and server timeout values from the parsed "timeout" entries
timeout_values = {}
for tline in timeout_lines:
tline = tline.strip()
if not tline:
continue
parts = tline.split(None, 1)
if len(parts) < 2:
continue
kind, value = parts[0].strip(), parts[1].strip()
if kind in ("client", "server"):
timeout_values[kind] = value
# Special handling for idletimeout == 0: there should be no client/server timeouts configured
if idle_value == "0":
if "client" in timeout_values or "server" in timeout_values:
print("defaults timeout client or timeout server should be absent when idletimeout is 0")
return False
return True
# Non-zero idletimeout: both client and server timeouts must be present
if "client" not in timeout_values or "server" not in timeout_values:
print("defaults timeout client or timeout server missing")
return False
if idle_value != timeout_values["client"] or idle_value != timeout_values["server"]:
print("defaults timeout client or timeout server mismatch occurred")
return False
return True
def checkLoadBalance(haproxyData, haCfgSections):
correct = True
@ -120,9 +160,10 @@ def main():
currSectionDict[lineSec[0]].append(lineSec[1] if len(lineSec) > 1 else '')
checkMaxConn = checkMaxconn(haproxyData[0], haCfgSections)
checkIdleTimeout = checkIdletimeout(haproxyData[0], haCfgSections)
checkLbRules = checkLoadBalance(haproxyData, haCfgSections)
if checkMaxConn and checkLbRules:
if checkMaxConn and checkIdleTimeout and checkLbRules:
print("All checks pass")
exit(0)
else: