From 2d6e1b01f86060123a512c4c8f99c5a7d73131ae Mon Sep 17 00:00:00 2001 From: J M Date: Tue, 28 Apr 2026 10:48:07 +0800 Subject: [PATCH] [kvm]: add kvmagent auto-restart window config Add KVMAGENT_AUTO_RESTART_WINDOW (kvm.kvmagent.autorestart.window) so the automatic kvmagent restart triggered by the physical-memory hard-limit alarm only fires within a configured daily time window. 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. Default is 02:00-04:00 so auto-restart only fires during low-traffic hours out of the box; operators who want 24/7 restart can clear the value. The gate is added in processKvmagentPhysicalMemUsageAbnormal() between the existing hard-limit check and the RestartKvmAgentMsg send. The 'no running task on host' check inside restartKvmAgentOnHost is unchanged, so the final guard is: in-window AND over-hardlimit AND no-host-tasks. A GlobalConfigValidatorExtensionPoint is registered in start() to reject malformed values inline, following the pattern used by RESERVED_MEMORY_CAPACITY (no try/catch wrappers). Includes unit tests for the window-membership function covering empty/null, normal window, half-open boundary, cross-midnight, and 00:00-23:59 edge cases. Resolves: ZSTAC-84618 Change-Id: I872bfe96fe30cb83dec21d40157bb315966978ba --- conf/globalConfig/kvm.xml | 8 +++ .../java/org/zstack/kvm/KVMGlobalConfig.java | 3 + .../java/org/zstack/kvm/KVMHostFactory.java | 47 +++++++++++++++ .../test/unittest/JUnitTestSuite.groovy | 4 +- .../utils/KVMAutoRestartWindowCase.java | 59 +++++++++++++++++++ test/src/test/resources/globalConfig/kvm.xml | 8 +++ 6 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 test/src/test/groovy/org/zstack/test/unittest/utils/KVMAutoRestartWindowCase.java 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 +