Skip to content

Commit 9bc283e

Browse files
Introducing granular command timeouts global setting (#9659)
* Introducing granular command timeouts global setting * fix marvin tests * Fixed log messages * some more log message fix * Fix empty value setting * Converted the global setting to non-dynamic * set wait on command only when granular wait is defined. This is to keep the backward compatibility * Improve error logging
1 parent a4224e5 commit 9bc283e

File tree

6 files changed

+370
-74
lines changed

6 files changed

+370
-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 (in seconds) for specific commands. " +
55+
"For example: DhcpEntryCommand=600, SavePasswordCommand=300, VmDataCommand=300", false);
56+
5357
public enum TapAgentsAction {
5458
Add, Del, Contains,
5559
}

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

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import org.apache.cloudstack.managed.context.ManagedContextRunnable;
5454
import org.apache.cloudstack.outofbandmanagement.dao.OutOfBandManagementDao;
5555
import org.apache.cloudstack.utils.identity.ManagementServerNode;
56+
import org.apache.commons.collections.MapUtils;
5657
import org.apache.commons.lang3.BooleanUtils;
5758

5859
import com.cloud.agent.AgentManager;
@@ -139,6 +140,7 @@ public class AgentManagerImpl extends ManagerBase implements AgentManager, Handl
139140
protected List<Pair<Integer, Listener>> _cmdMonitors = new ArrayList<Pair<Integer, Listener>>(17);
140141
protected List<Pair<Integer, StartupCommandProcessor>> _creationMonitors = new ArrayList<Pair<Integer, StartupCommandProcessor>>(17);
141142
protected List<Long> _loadingAgents = new ArrayList<Long>();
143+
protected Map<String, Integer> _commandTimeouts = new HashMap<>();
142144
private int _monitorId = 0;
143145
private final Lock _agentStatusLock = new ReentrantLock();
144146

@@ -241,6 +243,8 @@ public boolean configure(final String name, final Map<String, Object> params) th
241243

242244
_monitorExecutor = new ScheduledThreadPoolExecutor(1, new NamedThreadFactory("AgentMonitor"));
243245

246+
initializeCommandTimeouts();
247+
244248
return true;
245249
}
246250

@@ -424,15 +428,77 @@ private void setEmptyAnswers(final Commands commands, final Command[] cmds) {
424428
}
425429
}
426430

431+
protected int getTimeout(final Commands commands, int timeout) {
432+
int result;
433+
if (timeout > 0) {
434+
result = timeout;
435+
} else {
436+
result = Wait.value();
437+
}
438+
439+
int granularTimeout = getTimeoutFromGranularWaitTime(commands);
440+
return (granularTimeout > 0) ? granularTimeout : result;
441+
}
442+
443+
protected int getTimeoutFromGranularWaitTime(final Commands commands) {
444+
int maxWait = 0;
445+
if (MapUtils.isNotEmpty(_commandTimeouts)) {
446+
for (final Command cmd : commands) {
447+
String simpleCommandName = cmd.getClass().getSimpleName();
448+
Integer commandTimeout = _commandTimeouts.get(simpleCommandName);
449+
if (commandTimeout != null && commandTimeout > maxWait) {
450+
maxWait = commandTimeout;
451+
}
452+
}
453+
}
454+
455+
return maxWait;
456+
}
457+
458+
private void initializeCommandTimeouts() {
459+
String commandWaits = GranularWaitTimeForCommands.value().trim();
460+
if (StringUtils.isNotEmpty(commandWaits)) {
461+
_commandTimeouts = getCommandTimeoutsMap(commandWaits);
462+
logger.info(String.format("Timeouts for management server internal commands successfully initialized from global setting commands.timeout: %s", _commandTimeouts));
463+
}
464+
}
465+
466+
private Map<String, Integer> getCommandTimeoutsMap(String commandWaits) {
467+
String[] commandPairs = commandWaits.split(",");
468+
Map<String, Integer> commandTimeouts = new HashMap<>();
469+
470+
for (String commandPair : commandPairs) {
471+
String[] parts = commandPair.trim().split("=");
472+
if (parts.length == 2) {
473+
try {
474+
String commandName = parts[0].trim();
475+
int commandTimeout = Integer.parseInt(parts[1].trim());
476+
commandTimeouts.put(commandName, commandTimeout);
477+
} catch (NumberFormatException e) {
478+
logger.error(String.format("Initialising the timeouts using commands.timeout: %s for management server internal commands failed with error %s", commandPair, e.getMessage()));
479+
}
480+
} else {
481+
logger.error(String.format("Error initialising the timeouts for management server internal commands. Invalid format in commands.timeout: %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+
logger.debug(String.format("Wait time setting on %s is %d seconds", commands, wait));
496+
for (Command cmd : commands) {
497+
String simpleCommandName = cmd.getClass().getSimpleName();
498+
Integer commandTimeout = _commandTimeouts.get(simpleCommandName);
499+
if (commandTimeout != null) {
500+
cmd.setWait(wait);
501+
}
436502
}
437503

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

455521
final Request req = new Request(hostId, agent.getName(), _nodeId, cmds, commands.stopOnError(), true);
456522
req.setSequence(agent.getNextSequence());
457-
final Answer[] answers = agent.send(req, timeout);
523+
final Answer[] answers = agent.send(req, wait);
458524
notifyAnswersToMonitors(hostId, req.getSequence(), answers);
459525
commands.setAnswers(answers);
460526
return answers;
@@ -997,6 +1063,11 @@ public Answer easySend(final Long hostId, final Command cmd) {
9971063
@Override
9981064
public Answer[] send(final Long hostId, final Commands cmds) throws AgentUnavailableException, OperationTimedoutException {
9991065
int wait = 0;
1066+
if (cmds.size() > 1) {
1067+
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," +
1068+
"then max wait time of those will be used", cmds));
1069+
}
1070+
10001071
for (final Command cmd : cmds) {
10011072
if (cmd.getWait() > wait) {
10021073
wait = cmd.getWait();
@@ -1821,7 +1892,7 @@ public String getConfigComponentName() {
18211892
@Override
18221893
public ConfigKey<?>[] getConfigKeys() {
18231894
return new ConfigKey<?>[] { CheckTxnBeforeSending, Workers, Port, Wait, AlertWait, DirectAgentLoadSize,
1824-
DirectAgentPoolSize, DirectAgentThreadCap, EnableKVMAutoEnableDisable, ReadyCommandWait };
1895+
DirectAgentPoolSize, DirectAgentThreadCap, EnableKVMAutoEnableDisable, ReadyCommandWait, GranularWaitTimeForCommands };
18251896
}
18261897

18271898
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: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1245,6 +1245,8 @@ protected String validateConfigurationValue(String name, String value, String sc
12451245
type = configuration.getType();
12461246
}
12471247

1248+
validateSpecificConfigurationValues(name, value, type);
1249+
12481250
boolean isTypeValid = validateValueType(value, type);
12491251
if (!isTypeValid) {
12501252
return String.format("Value [%s] is not a valid [%s].", value, type);
@@ -1373,6 +1375,78 @@ protected String validateValueRange(String name, String value, Class<?> type, Co
13731375
return validateIfStringValueIsInRange(name, value, range);
13741376
}
13751377

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

0 commit comments

Comments
 (0)