diff --git a/conf/globalConfig/kvm.xml b/conf/globalConfig/kvm.xml
index 76d53a5453a..863c4e6aacd 100755
--- a/conf/globalConfig/kvm.xml
+++ b/conf/globalConfig/kvm.xml
@@ -273,4 +273,12 @@
10737418240
java.lang.Long
+
+
+ kvm
+ kvmagent.autorestart.window
+ Daily time window during which the automatic restart of zstack-kvmagent (triggered by physical memory hard limit alarm) is allowed. Format: HH:MM-HH:MM in 24-hour server local time, e.g. 02:00-04:00. Cross-midnight windows are supported, e.g. 22:00-02:00. Empty value means always allowed (no time restriction).
+ 02:00-04:00
+ java.lang.String
+
diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMGlobalConfig.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMGlobalConfig.java
index ee765aad40f..9aae6c921f1 100755
--- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMGlobalConfig.java
+++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMGlobalConfig.java
@@ -149,6 +149,9 @@ public class KVMGlobalConfig {
@GlobalConfigValidation
public static GlobalConfig KVMAGENT_PHYSICAL_MEMORY_USAGE_HARD_LIMIT = new GlobalConfig(CATEGORY, "kvmagent.physicalmemory.usage.hardlimit");
+ @GlobalConfigValidation(notEmpty = false)
+ public static GlobalConfig KVMAGENT_AUTO_RESTART_WINDOW = new GlobalConfig(CATEGORY, "kvmagent.autorestart.window");
+
@GlobalConfigDef(defaultValue = "0G", description = "minimum free memory size to start vm, size in GB")
@BindResourceConfig({HostVO.class, ClusterVO.class})
public static GlobalConfig MINIMUM_MEMORY_SIZE_BEFORE_START_VM = new GlobalConfig(CATEGORY, "min.free.memory.size");
diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostFactory.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostFactory.java
index 0188c920a83..b1118a8a403 100755
--- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostFactory.java
+++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostFactory.java
@@ -99,6 +99,7 @@
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
+import java.time.LocalTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -459,6 +460,14 @@ private void processKvmagentPhysicalMemUsageAbnormal(HostProcessPhysicalMemoryUs
return;
}
+ String window = KVMGlobalConfig.KVMAGENT_AUTO_RESTART_WINDOW.value();
+ if (!isNowInAutoRestartWindow(window, LocalTime.now())) {
+ logger.info(String.format("zstack-kvmagent on host %s exceeded physical memory hard limit, " +
+ "but current time is outside auto-restart window [%s]; skip auto-restart, will retry on next alarm",
+ cmd.getHostUuid(), window));
+ return;
+ }
+
logger.debug("The zstack-kvmagent service has exceeded the hard limit for physical memory usage, " +
"and we will try restart it later");
RestartKvmAgentMsg restartKvmAgentMsg = new RestartKvmAgentMsg();
@@ -467,6 +476,19 @@ private void processKvmagentPhysicalMemUsageAbnormal(HostProcessPhysicalMemoryUs
bus.send(restartKvmAgentMsg);
}
+ public static boolean isNowInAutoRestartWindow(String configValue, LocalTime now) {
+ if (configValue == null || configValue.trim().isEmpty()) {
+ return true;
+ }
+ String[] parts = configValue.trim().split("-");
+ LocalTime start = LocalTime.parse(parts[0]);
+ LocalTime end = LocalTime.parse(parts[1]);
+ if (start.isBefore(end)) {
+ return !now.isBefore(start) && now.isBefore(end);
+ }
+ return !now.isBefore(start) || now.isBefore(end);
+ }
+
private void initLibvirtTlsCA() {
if (CoreGlobalProperty.UNIT_TEST_ON) {
return;
@@ -558,6 +580,31 @@ public void validateGlobalConfig(String category, String name, String oldValue,
}
}
});
+ KVMGlobalConfig.KVMAGENT_AUTO_RESTART_WINDOW.installValidateExtension(new GlobalConfigValidatorExtensionPoint() {
+ @Override
+ public void validateGlobalConfig(String category, String name, String oldValue, String value) throws GlobalConfigException {
+ if (value == null || value.trim().isEmpty()) {
+ return;
+ }
+ String[] parts = value.trim().split("-");
+ if (parts.length != 2 || !parts[0].matches("\\d{2}:\\d{2}") || !parts[1].matches("\\d{2}:\\d{2}")) {
+ throw new GlobalConfigException(String.format("%s must be in format HH:MM-HH:MM, but got %s",
+ KVMGlobalConfig.KVMAGENT_AUTO_RESTART_WINDOW.getCanonicalName(), value));
+ }
+ int sh = Integer.parseInt(parts[0].substring(0, 2));
+ int sm = Integer.parseInt(parts[0].substring(3, 5));
+ int eh = Integer.parseInt(parts[1].substring(0, 2));
+ int em = Integer.parseInt(parts[1].substring(3, 5));
+ if (sh > 23 || eh > 23 || sm > 59 || em > 59) {
+ throw new GlobalConfigException(String.format("%s has out-of-range hour/minute, but got %s",
+ KVMGlobalConfig.KVMAGENT_AUTO_RESTART_WINDOW.getCanonicalName(), value));
+ }
+ if (sh == eh && sm == em) {
+ throw new GlobalConfigException(String.format("%s start equals end, but got %s",
+ KVMGlobalConfig.KVMAGENT_AUTO_RESTART_WINDOW.getCanonicalName(), value));
+ }
+ }
+ });
ResourceConfig resourceConfig = rcf.getResourceConfig(KVMGlobalConfig.VM_CPU_HYPERVISOR_FEATURE.getIdentity());
resourceConfig.installValidatorExtension((resourceUuid, oldValue, newValue) -> {
if (Boolean.TRUE.toString().equals(newValue)) {
diff --git a/test/src/test/groovy/org/zstack/test/unittest/JUnitTestSuite.groovy b/test/src/test/groovy/org/zstack/test/unittest/JUnitTestSuite.groovy
index 0b0a0ea3bde..004984c58e6 100644
--- a/test/src/test/groovy/org/zstack/test/unittest/JUnitTestSuite.groovy
+++ b/test/src/test/groovy/org/zstack/test/unittest/JUnitTestSuite.groovy
@@ -6,6 +6,7 @@ import org.junit.runner.RunWith
import org.junit.runner.notification.Failure
import org.junit.runners.Suite
import org.zstack.configuration.OfferingUserConfigUtils
+import org.zstack.test.unittest.utils.KVMAutoRestartWindowCase
import org.zstack.test.unittest.utils.NetworkUtilsCase
import org.zstack.test.unittest.utils.OfferingUserConfigUtilsCase
import org.zstack.test.unittest.utils.SizeUnitUtilsCase
@@ -20,7 +21,8 @@ import java.util.stream.Collectors
@Suite.SuiteClasses([
NetworkUtilsCase.class,
OfferingUserConfigUtilsCase.class,
- SizeUnitUtilsCase.class
+ SizeUnitUtilsCase.class,
+ KVMAutoRestartWindowCase.class
])
class JUnitTestSuite {
diff --git a/test/src/test/groovy/org/zstack/test/unittest/utils/KVMAutoRestartWindowCase.java b/test/src/test/groovy/org/zstack/test/unittest/utils/KVMAutoRestartWindowCase.java
new file mode 100644
index 00000000000..5c655fa616f
--- /dev/null
+++ b/test/src/test/groovy/org/zstack/test/unittest/utils/KVMAutoRestartWindowCase.java
@@ -0,0 +1,59 @@
+package org.zstack.test.unittest.utils;
+
+import org.junit.Test;
+import org.zstack.kvm.KVMHostFactory;
+
+import java.time.LocalTime;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class KVMAutoRestartWindowCase {
+
+ private boolean inWindow(String configValue, String now) {
+ return KVMHostFactory.isNowInAutoRestartWindow(configValue, LocalTime.parse(now));
+ }
+
+ @Test
+ public void emptyOrNullValueAlwaysAllowed() {
+ assertTrue(inWindow("", "00:00"));
+ assertTrue(inWindow("", "12:00"));
+ assertTrue(inWindow("", "23:59"));
+ assertTrue(inWindow(null, "12:00"));
+ assertTrue(inWindow(" ", "12:00"));
+ }
+
+ @Test
+ public void normalWindowMembership() {
+ assertTrue(inWindow("02:00-04:00", "02:30"));
+ assertTrue(inWindow("02:00-04:00", "03:59"));
+ assertFalse(inWindow("02:00-04:00", "00:00"));
+ assertFalse(inWindow("02:00-04:00", "14:00"));
+ }
+
+ @Test
+ public void normalWindowBoundary() {
+ assertTrue(inWindow("02:00-04:00", "02:00"));
+ assertFalse(inWindow("02:00-04:00", "04:00"));
+ assertTrue(inWindow("02:00-04:00", "03:59"));
+ }
+
+ @Test
+ public void crossMidnightWindowMembership() {
+ assertTrue(inWindow("22:00-02:00", "23:30"));
+ assertTrue(inWindow("22:00-02:00", "22:00"));
+ assertTrue(inWindow("22:00-02:00", "00:30"));
+ assertTrue(inWindow("22:00-02:00", "01:59"));
+ assertFalse(inWindow("22:00-02:00", "14:00"));
+ assertFalse(inWindow("22:00-02:00", "02:00"));
+ assertFalse(inWindow("22:00-02:00", "21:59"));
+ }
+
+ @Test
+ public void wholeDayMinusOneMinute() {
+ assertTrue(inWindow("00:00-23:59", "00:00"));
+ assertTrue(inWindow("00:00-23:59", "12:00"));
+ assertTrue(inWindow("00:00-23:59", "23:58"));
+ assertFalse(inWindow("00:00-23:59", "23:59"));
+ }
+}
diff --git a/test/src/test/resources/globalConfig/kvm.xml b/test/src/test/resources/globalConfig/kvm.xml
index 6fb42631828..d5fc1dcf302 100755
--- a/test/src/test/resources/globalConfig/kvm.xml
+++ b/test/src/test/resources/globalConfig/kvm.xml
@@ -272,4 +272,12 @@
10737418240
java.lang.Long
+
+
+ kvm
+ kvmagent.autorestart.window
+ Daily time window during which the automatic restart of zstack-kvmagent (triggered by physical memory hard limit alarm) is allowed. Format: HH:MM-HH:MM in 24-hour server local time, e.g. 02:00-04:00. Cross-midnight windows are supported, e.g. 22:00-02:00. Empty value means always allowed (no time restriction).
+ 02:00-04:00
+ java.lang.String
+