Skip to content

Commit a666303

Browse files
Introducing granular command timeouts global setting
1 parent f8d8a9c commit a666303

File tree

6 files changed

+362
-74
lines changed

6 files changed

+362
-74
lines changed

engine/components-api/src/main/java/com/cloud/agent/AgentManager.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ public interface AgentManager {
5050
ConfigKey<Integer> ReadyCommandWait = new ConfigKey<Integer>("Advanced", Integer.class, "ready.command.wait",
5151
"60", "Time in seconds to wait for Ready command to return", true);
5252

53+
ConfigKey<String> GranularWaitTimeForCommands = new ConfigKey<>("Advanced", String.class, "commands.timeout", "",
54+
"This timeout overrides the wait global config. This holds a comma separated key value pairs containing timeout for specific commands. " +
55+
"For example: DhcpEntryCommand=600, SavePasswordCommand=300, VmDataCommand=300", true);
56+
5357
public enum TapAgentsAction {
5458
Add, Del, Contains,
5559
}

engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,7 @@ public Answer sendTo(final Long dcId, final HypervisorType type, final Command c
360360
public Answer send(final Long hostId, final Command cmd) throws AgentUnavailableException, OperationTimedoutException {
361361
final Commands cmds = new Commands(Command.OnError.Stop);
362362
cmds.addCommand(cmd);
363+
logger.debug(String.format("Wait time set on the command %s is %d seconds", cmd, cmd.getWait()));
363364
send(hostId, cmds, cmd.getWait());
364365
final Answer[] answers = cmds.getAnswers();
365366
if (answers != null && !(answers[0] instanceof UnsupportedAnswer)) {
@@ -424,15 +425,75 @@ private void setEmptyAnswers(final Commands commands, final Command[] cmds) {
424425
}
425426
}
426427

428+
protected int getTimeout(final Commands commands, int timeout) {
429+
int result;
430+
if (timeout > 0) {
431+
result = timeout;
432+
} else {
433+
logger.debug(String.format("Considering the Wait global setting %d, since wait time set on command is 0", Wait.value()));
434+
result = Wait.value();
435+
}
436+
437+
int granularTimeout = getTimeoutFromGranularWaitTime(commands);
438+
return (granularTimeout > 0) ? granularTimeout : result;
439+
}
440+
441+
protected int getTimeoutFromGranularWaitTime(final Commands commands) {
442+
logger.debug("Looking for the commands.timeout global setting for any command-specific timeout value");
443+
String commandWaits = GranularWaitTimeForCommands.value().trim();
444+
445+
int maxWait = 0;
446+
if (StringUtils.isNotEmpty(commandWaits)) {
447+
try {
448+
Map<String, Integer> commandTimeouts = getCommandTimeoutsMap(commandWaits);
449+
450+
for (final Command cmd : commands) {
451+
String simpleCommandName = cmd.getClass().getSimpleName();
452+
Integer commandTimeout = commandTimeouts.get(simpleCommandName);
453+
454+
if (commandTimeout != null) {
455+
logger.debug(String.format("Timeout %d found for command %s in commands.timeout global setting", commandTimeout, cmd.toString()));
456+
if (commandTimeout > maxWait) {
457+
maxWait = commandTimeout;
458+
}
459+
}
460+
}
461+
} catch (Exception e) {
462+
logger.error(String.format("Error while processing the commands.timeout global setting for the granular timeouts for the command, " +
463+
"falling back to the command timeout: %s", e.getMessage()));
464+
}
465+
}
466+
467+
return maxWait;
468+
}
469+
470+
private Map<String, Integer> getCommandTimeoutsMap(String commandWaits) {
471+
String[] commandPairs = commandWaits.split(",");
472+
Map<String, Integer> commandTimeouts = new HashMap<>();
473+
474+
for (String commandPair : commandPairs) {
475+
String[] parts = commandPair.trim().split("=");
476+
if (parts.length == 2) {
477+
String commandName = parts[0].trim();
478+
int commandTimeout = Integer.parseInt(parts[1].trim());
479+
commandTimeouts.put(commandName, commandTimeout);
480+
} else {
481+
logger.warn(String.format("Invalid format in commands.timeout global setting: %s", commandPair));
482+
}
483+
}
484+
return commandTimeouts;
485+
}
486+
427487
@Override
428488
public Answer[] send(final Long hostId, final Commands commands, int timeout) throws AgentUnavailableException, OperationTimedoutException {
429489
assert hostId != null : "Who's not checking the agent id before sending? ... (finger wagging)";
430490
if (hostId == null) {
431491
throw new AgentUnavailableException(-1);
432492
}
433493

434-
if (timeout <= 0) {
435-
timeout = Wait.value();
494+
int wait = getTimeout(commands, timeout);
495+
for (Command cmd : commands) {
496+
cmd.setWait(wait);
436497
}
437498

438499
if (CheckTxnBeforeSending.value()) {
@@ -454,7 +515,7 @@ public Answer[] send(final Long hostId, final Commands commands, int timeout) th
454515

455516
final Request req = new Request(hostId, agent.getName(), _nodeId, cmds, commands.stopOnError(), true);
456517
req.setSequence(agent.getNextSequence());
457-
final Answer[] answers = agent.send(req, timeout);
518+
final Answer[] answers = agent.send(req, wait);
458519
notifyAnswersToMonitors(hostId, req.getSequence(), answers);
459520
commands.setAnswers(answers);
460521
return answers;
@@ -988,7 +1049,13 @@ public Answer easySend(final Long hostId, final Command cmd) {
9881049
@Override
9891050
public Answer[] send(final Long hostId, final Commands cmds) throws AgentUnavailableException, OperationTimedoutException {
9901051
int wait = 0;
1052+
if (cmds.size() > 1) {
1053+
logger.debug(String.format("Checking the wait time in seconds to be used for the following commands : %s. If there are multiple commands sent at once," +
1054+
"then max wait time of those will be used", cmds));
1055+
}
1056+
9911057
for (final Command cmd : cmds) {
1058+
logger.debug(String.format("Wait time set on the command %s is %d", cmd, cmd.getWait()));
9921059
if (cmd.getWait() > wait) {
9931060
wait = cmd.getWait();
9941061
}
@@ -1802,7 +1869,7 @@ public String getConfigComponentName() {
18021869
@Override
18031870
public ConfigKey<?>[] getConfigKeys() {
18041871
return new ConfigKey<?>[] { CheckTxnBeforeSending, Workers, Port, Wait, AlertWait, DirectAgentLoadSize,
1805-
DirectAgentPoolSize, DirectAgentThreadCap, EnableKVMAutoEnableDisable, ReadyCommandWait };
1872+
DirectAgentPoolSize, DirectAgentThreadCap, EnableKVMAutoEnableDisable, ReadyCommandWait, GranularWaitTimeForCommands };
18061873
}
18071874

18081875
protected class SetHostParamsListener implements Listener {

engine/orchestration/src/test/java/com/cloud/agent/manager/AgentManagerImplTest.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,24 @@ public void testNotifyMonitorsOfConnectionWhenStoragePoolConnectionHostFailure()
8383
}
8484
Mockito.verify(mgr, Mockito.times(1)).handleDisconnectWithoutInvestigation(Mockito.any(attache.getClass()), Mockito.eq(Status.Event.AgentDisconnected), Mockito.eq(true), Mockito.eq(true));
8585
}
86+
87+
@Test
88+
public void testGetTimeoutWithPositiveTimeout() {
89+
Commands commands = Mockito.mock(Commands.class);
90+
int timeout = 30;
91+
int result = mgr.getTimeout(commands, timeout);
92+
93+
Assert.assertEquals(30, result);
94+
}
95+
96+
@Test
97+
public void testGetTimeoutWithGranularTimeout() {
98+
Commands commands = Mockito.mock(Commands.class);
99+
Mockito.doReturn(50).when(mgr).getTimeoutFromGranularWaitTime(commands);
100+
101+
int timeout = 0;
102+
int result = mgr.getTimeout(commands, timeout);
103+
104+
Assert.assertEquals(50, result);
105+
}
86106
}

server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1226,6 +1226,8 @@ protected String validateConfigurationValue(String name, String value, String sc
12261226
type = configuration.getType();
12271227
}
12281228

1229+
validateSpecificConfigurationValues(name, value, type);
1230+
12291231
boolean isTypeValid = validateValueType(value, type);
12301232
if (!isTypeValid) {
12311233
return String.format("Value [%s] is not a valid [%s].", value, type);
@@ -1354,6 +1356,76 @@ protected String validateValueRange(String name, String value, Class<?> type, Co
13541356
return validateIfStringValueIsInRange(name, value, range);
13551357
}
13561358

1359+
/**
1360+
* Validates configuration values for the given name, value, and type.
1361+
* <ul>
1362+
* <li>The value must be a comma-separated list of key-value pairs, where each value must be a positive integer.</li>
1363+
* <li>Each key-value pair must be in the format "command=value", with the value being a positive integer greater than 0,
1364+
* otherwise fails with an error message</li>
1365+
* <li>Throws an {@link InvalidParameterValueException} if validation fails.</li>
1366+
* </ul>
1367+
*
1368+
* @param name the configuration name
1369+
* @param value the configuration value as a comma-separated string of key-value pairs
1370+
* @param type the configuration type, expected to be String
1371+
* @throws InvalidParameterValueException if validation fails with a specific error message
1372+
*/
1373+
protected void validateSpecificConfigurationValues(String name, String value, Class<?> type) {
1374+
if (type.equals(String.class)) {
1375+
if (name.equals(AgentManager.GranularWaitTimeForCommands.toString())) {
1376+
Pair<Boolean, String> validationResult = validateCommaSeparatedKeyValueConfigWithPositiveIntegerValues(value);
1377+
if (!validationResult.first()) {
1378+
String errMsg = validationResult.second();
1379+
logger.error(validationResult.second());
1380+
throw new InvalidParameterValueException(errMsg);
1381+
}
1382+
}
1383+
}
1384+
}
1385+
1386+
protected Pair<Boolean, String> validateCommaSeparatedKeyValueConfigWithPositiveIntegerValues(String value) {
1387+
try {
1388+
String[] commands = value.split(",");
1389+
for (String command : commands) {
1390+
command = command.trim();
1391+
if (!command.contains("=")) {
1392+
String errorMessage = "Validation failed: Command '" + command + "' does not contain '='.";
1393+
return new Pair<>(false, errorMessage);
1394+
}
1395+
1396+
String[] parts = command.split("=");
1397+
if (parts.length != 2) {
1398+
String errorMessage = "Validation failed: Command '" + command + "' is not properly formatted.";
1399+
return new Pair<>(false, errorMessage);
1400+
}
1401+
1402+
String commandName = parts[0].trim();
1403+
String valueString = parts[1].trim();
1404+
1405+
if (commandName.isEmpty()) {
1406+
String errorMessage = "Validation failed: Command name is missing in '" + command + "'.";
1407+
return new Pair<>(false, errorMessage);
1408+
}
1409+
1410+
try {
1411+
int num = Integer.parseInt(valueString);
1412+
if (num <= 0) {
1413+
String errorMessage = "Validation failed: The value for command '" + commandName + "' is not greater than 0. Invalid value: " + num;
1414+
return new Pair<>(false, errorMessage);
1415+
}
1416+
} catch (NumberFormatException e) {
1417+
String errorMessage = "Validation failed: The value for command '" + commandName + "' is not a valid integer. Invalid value: " + valueString;
1418+
return new Pair<>(false, errorMessage);
1419+
}
1420+
}
1421+
1422+
return new Pair<>(true, "");
1423+
} catch (Exception e) {
1424+
String errorMessage = "Validation failed: An error occurred while parsing the command string. Error: " + e.getMessage();
1425+
return new Pair<>(false, errorMessage);
1426+
}
1427+
}
1428+
13571429
/**
13581430
* Returns a boolean indicating whether a Config's range should be validated. It should not be validated when:</br>
13591431
* <ul>

0 commit comments

Comments
 (0)