diff --git a/src/integrationtests/java/com/aws/greengrass/integrationtests/ipc/IPCHibernateTest.java b/src/integrationtests/java/com/aws/greengrass/integrationtests/ipc/IPCHibernateTest.java index 1a38192c9f..c47eaf1292 100644 --- a/src/integrationtests/java/com/aws/greengrass/integrationtests/ipc/IPCHibernateTest.java +++ b/src/integrationtests/java/com/aws/greengrass/integrationtests/ipc/IPCHibernateTest.java @@ -9,7 +9,7 @@ import com.aws.greengrass.lifecyclemanager.Kernel; import com.aws.greengrass.testcommons.testutilities.GGExtension; import com.aws.greengrass.testcommons.testutilities.TestUtils; -import com.aws.greengrass.util.platforms.unix.linux.Cgroup; +import com.aws.greengrass.util.platforms.unix.linux.CGroupV1; import com.aws.greengrass.util.platforms.unix.linux.LinuxSystemResourceController; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -19,6 +19,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.io.TempDir; +import org.mockito.junit.jupiter.MockitoExtension; import software.amazon.awssdk.aws.greengrass.GreengrassCoreIPCClient; import software.amazon.awssdk.aws.greengrass.model.PauseComponentRequest; import software.amazon.awssdk.aws.greengrass.model.ResumeComponentRequest; @@ -30,6 +31,7 @@ import java.nio.file.FileSystemException; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Optional; import java.util.concurrent.TimeUnit; @@ -42,8 +44,9 @@ import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; -@ExtendWith({GGExtension.class}) +@ExtendWith({GGExtension.class, MockitoExtension.class}) class IPCHibernateTest { private static final String TARGET_COMPONENT_NAME = "HibernateTarget"; private static final String CONTROLLER_COMPONENT_NAME = "HibernateController"; @@ -92,6 +95,7 @@ void beforeEach(ExtensionContext context) throws Exception { @Test void GIVEN_LifeCycleEventStreamClient_WHEN_pause_resume_component_THEN_target_service_paused_and_resumed() throws Exception { + assumeTrue(!ifCgroupV2(), "skip this test case if v2 is enabled."); GenericExternalService component = (GenericExternalService) kernel.locate(TARGET_COMPONENT_NAME); PauseComponentRequest pauseRequest = new PauseComponentRequest(); @@ -115,8 +119,12 @@ void GIVEN_LifeCycleEventStreamClient_WHEN_pause_resume_component_THEN_target_se private LinuxSystemResourceController.CgroupFreezerState getCgroupFreezerState(String serviceName) throws IOException { return LinuxSystemResourceController.CgroupFreezerState.valueOf( - new String(Files.readAllBytes(Cgroup.Freezer.getCgroupFreezerStateFilePath(serviceName)), + new String(Files.readAllBytes(CGroupV1.Freezer.getCgroupFreezerStateFilePath(serviceName)), StandardCharsets.UTF_8).trim()); } + + private boolean ifCgroupV2() { + return Files.exists(Paths.get("/sys/fs/cgroup/cgroup.controllers")); + } } diff --git a/src/integrationtests/java/com/aws/greengrass/integrationtests/lifecyclemanager/GenericExternalServiceIntegTest.java b/src/integrationtests/java/com/aws/greengrass/integrationtests/lifecyclemanager/GenericExternalServiceIntegTest.java index 73641feed4..ac9d1f9ab7 100644 --- a/src/integrationtests/java/com/aws/greengrass/integrationtests/lifecyclemanager/GenericExternalServiceIntegTest.java +++ b/src/integrationtests/java/com/aws/greengrass/integrationtests/lifecyclemanager/GenericExternalServiceIntegTest.java @@ -18,9 +18,11 @@ import com.aws.greengrass.logging.impl.GreengrassLogMessage; import com.aws.greengrass.logging.impl.Slf4jLogAdapter; import com.aws.greengrass.status.model.ComponentStatusDetails; +import com.aws.greengrass.testcommons.testutilities.GGExtension; import com.aws.greengrass.testcommons.testutilities.NoOpPathOwnershipHandler; import com.aws.greengrass.util.Pair; -import com.aws.greengrass.util.platforms.unix.linux.Cgroup; +import com.aws.greengrass.util.platforms.unix.linux.CGroupV1; +import com.aws.greengrass.util.platforms.unix.linux.CGroupV2; import com.aws.greengrass.util.platforms.unix.linux.LinuxSystemResourceController; import org.apache.commons.lang3.SystemUtils; import org.junit.jupiter.api.AfterEach; @@ -28,16 +30,19 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledOnOs; import org.junit.jupiter.api.condition.OS; +import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.junit.jupiter.MockitoExtension; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.FileSystemException; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -81,11 +86,13 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; - +@ExtendWith({GGExtension.class, MockitoExtension.class}) class GenericExternalServiceIntegTest extends BaseITCase { - private Kernel kernel; + private final static String ROOT_PATH_STRING = "/sys/fs/cgroup"; + private final static String GG_PATH_STRING = "greengrass"; + static Stream posixTestUserConfig() { return Stream.of( arguments("config_run_with_user.yaml", "nobody", "nobody"), @@ -536,7 +543,7 @@ void GIVEN_posix_default_user_WHEN_runs_THEN_runs_with_default_user(String file, String messageOnStdout = m.getMessage(); if (STDOUT.equals(m.getEventType()) && messageOnStdout != null && (messageOnStdout.contains("run as") - || messageOnStdout.contains("install as") )) { + || messageOnStdout.contains("install as") )) { stdouts.add(messageOnStdout); countDownLatch.countDown(); } @@ -578,6 +585,7 @@ void GIVEN_posix_default_user_WHEN_runs_THEN_runs_with_default_user(String file, @EnabledOnOs({OS.LINUX}) @Test void GIVEN_linux_resource_limits_WHEN_it_changes_THEN_component_runs_with_new_resource_limits() throws Exception { + assumeTrue(!ifCgroupV2(), "skip this test case if v2 is enabled."); String componentName = "echo_service"; // Run with no resource limit ConfigPlatformResolver.initKernelWithMultiPlatformConfig(kernel, @@ -624,6 +632,48 @@ void GIVEN_linux_resource_limits_WHEN_it_changes_THEN_component_runs_with_new_re assertResourceLimits(componentName, 10240l * 1024, 1.5); } + @EnabledOnOs({OS.LINUX}) + @Test + void GIVEN_linux_resource_limits_WHEN_it_changes_THEN_component_runs_with_new_resource_limits_V2() throws Exception { + assumeTrue(ifCgroupV2(), "skip this test case if v1 is enabled."); + + String echoComponentName = "echo_service"; + // Run with no resource limit + ConfigPlatformResolver.initKernelWithMultiPlatformConfig(kernel, + getClass().getResource("config_run_with_user.yaml")); + CountDownLatch service = new CountDownLatch(1); + kernel.getContext().addGlobalStateChangeListener((s, oldState, newState) -> { + if (s.getName().equals(echoComponentName) && newState.equals(State.RUNNING)) { + service.countDown(); + } + }); + + kernel.launch(); + assertResourceLimitsCgroupV2(10240l * 1024, 1.5); + + // Run with updated component resource limit + kernel.getConfig().lookup(SERVICES_NAMESPACE_TOPIC, echoComponentName, RUN_WITH_NAMESPACE_TOPIC, + SYSTEM_RESOURCE_LIMITS_TOPICS, "memory").withValue(51200l); + kernel.getConfig().lookup(SERVICES_NAMESPACE_TOPIC, echoComponentName, RUN_WITH_NAMESPACE_TOPIC, + SYSTEM_RESOURCE_LIMITS_TOPICS, "cpus").withValue(0.35); + kernel.getConfig().lookup(SERVICES_NAMESPACE_TOPIC, echoComponentName, VERSION_CONFIG_KEY).withValue("2.0.0"); + // Block until events are completed + kernel.getContext().waitForPublishQueueToClear(); + + assertResourceLimitsCgroupV2(51200l * 1024, 0.35); + + //Remove component resource limit, should fall back to default + kernel.getConfig().lookupTopics(SERVICES_NAMESPACE_TOPIC, echoComponentName, RUN_WITH_NAMESPACE_TOPIC, + SYSTEM_RESOURCE_LIMITS_TOPICS).remove(); + kernel.getContext().waitForPublishQueueToClear(); + + assertResourceLimitsCgroupV2(10240l * 1024, 1.5); + } + + private boolean ifCgroupV2() { + return Files.exists(Paths.get("/sys/fs/cgroup/cgroup.controllers")); + } + @Test void GIVEN_service_starts_up_WHEN_service_breaks_THEN_status_details_persisted_for_errored_and_broken_states() throws Exception { @@ -754,12 +804,52 @@ void GIVEN_service_starts_up_WHEN_startup_times_out_THEN_timeout_error_code_pers assertThat(statusB.get().getStatusReason(), containsString(ComponentStatusCode.RUN_TIMEOUT.getDescription())); } + @Test + @EnabledOnOs({OS.LINUX}) + void GIVEN_running_service_WHEN_pause_resume_requested_THEN_pause_resume_Service_and_freeze_thaw_cgroup_V2( + ExtensionContext context) throws Exception { + assumeTrue(ifCgroupV2(), "skip this test case if v1 is enabled."); + ignoreExceptionOfType(context, FileSystemException.class); + ConfigPlatformResolver.initKernelWithMultiPlatformConfig(kernel, + getClass().getResource("long_running_services.yaml")); + kernel.launch(); + + CountDownLatch mainRunningLatch = new CountDownLatch(1); + kernel.getContext().addGlobalStateChangeListener((service, oldState, newState) -> { + if (kernel.getMain().equals(service) && newState.isRunning()) { + mainRunningLatch.countDown(); + } + }); + + // wait for main to run + assertTrue(mainRunningLatch.await(60, TimeUnit.SECONDS), "main running"); + + GenericExternalService component = (GenericExternalService) kernel.locate("sleeperA"); + assertThat(component.getState(), is(State.RUNNING)); + + component.pause(); + assertTrue(component.isPaused()); + assertEquals(getCgroupFreezerStateV2(component.getServiceName()), + "1"); + + component.resume(); + assertFalse(component.isPaused()); + assertEquals(getCgroupFreezerStateV2(component.getServiceName()), + "0"); + } + + private String getCgroupFreezerStateV2(String serviceName) + throws IOException { + return new String(Files.readAllBytes(CGroupV2.Freezer.getCgroupFreezerStateFilePath(serviceName)) + , StandardCharsets.UTF_8).trim(); + } + private void assertResourceLimits(String componentName, long memory, double cpus) throws Exception { - byte[] buf1 = Files.readAllBytes(Cgroup.Memory.getComponentMemoryLimitPath(componentName)); + byte[] buf1 = Files.readAllBytes(CGroupV1.Memory.getComponentMemoryLimitPath(componentName)); assertThat(memory, equalTo(Long.parseLong(new String(buf1, StandardCharsets.UTF_8).trim()))); - byte[] buf2 = Files.readAllBytes(Cgroup.CPU.getComponentCpuQuotaPath(componentName)); - byte[] buf3 = Files.readAllBytes(Cgroup.CPU.getComponentCpuPeriodPath(componentName)); + byte[] buf2 = Files.readAllBytes(CGroupV1.CPU.getComponentCpuQuotaPath(componentName)); + byte[] buf3 = Files.readAllBytes(CGroupV1.CPU.getComponentCpuPeriodPath(componentName)); int quota = Integer.parseInt(new String(buf2, StandardCharsets.UTF_8).trim()); int period = Integer.parseInt(new String(buf3, StandardCharsets.UTF_8).trim()); @@ -767,8 +857,27 @@ private void assertResourceLimits(String componentName, long memory, double cpus assertThat(expectedQuota, equalTo(quota)); } + private void assertResourceLimitsCgroupV2(long memory, double cpus) throws Exception { + byte[] buf1 = Files.readAllBytes(Paths.get(String.format("%s/%s/echo_service/memory.max", ROOT_PATH_STRING, GG_PATH_STRING))); + assertThat(memory, equalTo(Long.parseLong(new String(buf1, StandardCharsets.UTF_8).trim()))); + + byte[] buf2 = Files.readAllBytes(Paths.get(String.format("%s/%s/echo_service/cpu.max", ROOT_PATH_STRING, GG_PATH_STRING))); + + String cpuMaxContent = new String(buf2, StandardCharsets.UTF_8).trim(); + String[] cpuMaxContentArr = cpuMaxContent.split(" "); + + String cpuMaxStr = cpuMaxContentArr[0]; + String cpuPeriodStr = cpuMaxContentArr[1]; + int quota = Integer.parseInt(cpuMaxStr); + int expectedQuota = (int) (cpus * Integer.parseInt(cpuPeriodStr)); + assertThat(expectedQuota, equalTo(quota)); + } + + @Test + @EnabledOnOs({OS.LINUX}) void GIVEN_running_service_WHEN_pause_resume_requested_THEN_pause_resume_Service_and_freeze_thaw_cgroup( ExtensionContext context) throws Exception { + assumeTrue(!ifCgroupV2(), "skip this test case if v2 is enabled."); ignoreExceptionOfType(context, FileSystemException.class); ConfigPlatformResolver.initKernelWithMultiPlatformConfig(kernel, getClass().getResource("long_running_services.yaml")); @@ -803,7 +912,7 @@ void GIVEN_running_service_WHEN_pause_resume_requested_THEN_pause_resume_Service private LinuxSystemResourceController.CgroupFreezerState getCgroupFreezerState(String serviceName) throws IOException { return LinuxSystemResourceController.CgroupFreezerState - .valueOf(new String(Files.readAllBytes(Cgroup.Freezer.getCgroupFreezerStateFilePath(serviceName)) + .valueOf(new String(Files.readAllBytes(CGroupV1.Freezer.getCgroupFreezerStateFilePath(serviceName)) , StandardCharsets.UTF_8).trim()); } } diff --git a/src/main/java/com/aws/greengrass/util/platforms/unix/linux/CGroupSubSystemPaths.java b/src/main/java/com/aws/greengrass/util/platforms/unix/linux/CGroupSubSystemPaths.java new file mode 100644 index 0000000000..32bec8b12e --- /dev/null +++ b/src/main/java/com/aws/greengrass/util/platforms/unix/linux/CGroupSubSystemPaths.java @@ -0,0 +1,129 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.aws.greengrass.util.platforms.unix.linux; + +import com.aws.greengrass.lifecyclemanager.GreengrassService; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@SuppressFBWarnings(value = "DMI_HARDCODED_ABSOLUTE_FILENAME", + justification = "CGroupSubSystemPath virtual filesystem path cannot be relative") +public interface CGroupSubSystemPaths { + Path CGROUP_ROOT = Paths.get("/sys/fs/cgroup"); + String GG_NAMESPACE = "greengrass"; + String CGROUP_MEMORY_LIMITS = "memory.limit_in_bytes"; + String CPU_CFS_PERIOD_US = "cpu.cfs_period_us"; + String CPU_CFS_QUOTA_US = "cpu.cfs_quota_us"; + String CGROUP_PROCS = "cgroup.procs"; + String FREEZER_STATE_FILE = "freezer.state"; + String CPU_MAX = "cpu.max"; + String MEMORY_MAX = "memory.max"; + String CGROUP_SUBTREE_CONTROL = "cgroup.subtree_control"; + String CGROUP_FREEZE = "cgroup.freeze"; + String MOUNT_PATH = "/proc/self/mounts"; + String UNICODE_SPACE = "\\040"; + + default Path getRootPath() { + return CGROUP_ROOT; + } + + String rootMountCmd(); + + Path getSubsystemRootPath(); + + default Path getSubsystemGGPath() { + return getSubsystemRootPath().resolve(GG_NAMESPACE); + } + + default Path getSubsystemComponentPath(String componentName) { + return getSubsystemGGPath().resolve(componentName); + } + + Path getComponentMemoryLimitPath(String componentName); + + default Path getCgroupProcsPath(String componentName) { + return getSubsystemComponentPath(componentName).resolve(CGROUP_PROCS); + } + + Path getCgroupFreezerStateFilePath(String componentName); + + void initializeCgroup(GreengrassService component, LinuxPlatform platform) + throws IOException; + + /** + * Initialize cgroup core method. + * + * @param component component + * @param platform platform + * @param mountSubSystem mount subsystem method + * @throws IOException IOException + */ + default void initializeCgroupCore(GreengrassService component, LinuxPlatform platform, + InitializeCgroup mountSubSystem) throws IOException { + Set mounts = getMountedPaths(); + + if (!mounts.contains(getRootPath().toString())) { + platform.runCmd(rootMountCmd(), o -> { + }, "Failed to mount cgroup root"); + Files.createDirectory(getSubsystemRootPath()); + } + + if (!mounts.contains(getSubsystemRootPath().toString())) { + mountSubSystem.add(); + } + + if (!Files.exists(getSubsystemGGPath())) { + Files.createDirectory(getSubsystemGGPath()); + } + if (!Files.exists(getSubsystemComponentPath(component.getServiceName()))) { + Files.createDirectory(getSubsystemComponentPath(component.getServiceName())); + } + } + + void handleCpuLimits(GreengrassService component, double cpu) throws IOException; + + void pauseComponentProcessesCore(GreengrassService component) throws IOException; + + void resumeComponentProcesses(GreengrassService component) throws IOException; + + /** + * Get mounted paths. + * + * @return A set of String + * @throws IOException IOException + */ + default Set getMountedPaths() throws IOException { + Set mountedPaths = new HashSet<>(); + + Path procMountsPath = Paths.get(MOUNT_PATH); + List mounts = Files.readAllLines(procMountsPath); + for (String mount : mounts) { + String[] split = mount.split(" "); + // As reported in fstab(5) manpage, struct is: + // 1st field is volume name + // 2nd field is path with spaces escaped as \040 + // 3rd field is fs type + // 4th field is mount options + // 5th field is used by dump(8) (ignored) + // 6th field is fsck order (ignored) + if (split.length < 6) { + continue; + } + + // We only need the path of the mounts to verify whether cgroup is mounted + String path = split[1].replace(UNICODE_SPACE, " "); + mountedPaths.add(path); + } + return mountedPaths; + } +} diff --git a/src/main/java/com/aws/greengrass/util/platforms/unix/linux/CGroupV1.java b/src/main/java/com/aws/greengrass/util/platforms/unix/linux/CGroupV1.java new file mode 100644 index 0000000000..a51285ea12 --- /dev/null +++ b/src/main/java/com/aws/greengrass/util/platforms/unix/linux/CGroupV1.java @@ -0,0 +1,138 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.aws.greengrass.util.platforms.unix.linux; + +import com.aws.greengrass.lifecyclemanager.GreengrassService; +import com.aws.greengrass.util.Utils; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.List; + +@SuppressFBWarnings(value = "DMI_HARDCODED_ABSOLUTE_FILENAME", + justification = "CGroupV1 virtual filesystem path cannot be relative") +public enum CGroupV1 implements CGroupSubSystemPaths { + Memory("memory", ""), CPU("cpu,cpuacct", ""), Freezer("freezer", "freezer"); + + private String osString; + private String mountSrc; + + CGroupV1(String osString, String mountSrc) { + this.osString = osString; + this.mountSrc = mountSrc; + } + + /** + * Get the osString associated with this CgroupSubController. + * + * @return the osString associated with this CgroupSubController. + */ + public String getOsString() { + return osString; + } + + /** + * Get the mountSrc associated with this CgroupSubController. + * + * @return the mountSrc associated with this CgroupSubController. + */ + public String getMountSrc() { + return mountSrc; + } + + @Override + public String rootMountCmd() { + return String.format("mount -t tmpfs cgroup %s", CGROUP_ROOT); + } + + public String subsystemMountCmd() { + return String.format("mount -t cgroup -o %s %s %s", osString, mountSrc, getSubsystemRootPath()); + } + + @Override + public Path getSubsystemRootPath() { + return CGROUP_ROOT.resolve(osString); + } + + @Override + public Path getComponentMemoryLimitPath(String componentName) { + return getSubsystemComponentPath(componentName).resolve(CGROUP_MEMORY_LIMITS); + } + + public Path getComponentCpuPeriodPath(String componentName) { + return getSubsystemComponentPath(componentName).resolve(CPU_CFS_PERIOD_US); + } + + public Path getComponentCpuQuotaPath(String componentName) { + return getSubsystemComponentPath(componentName).resolve(CPU_CFS_QUOTA_US); + } + + @Override + public Path getCgroupFreezerStateFilePath(String componentName) { + return getSubsystemComponentPath(componentName).resolve(FREEZER_STATE_FILE); + } + + @Override + public void initializeCgroup(GreengrassService component, LinuxPlatform platform) + throws IOException { + initializeCgroupCore(component, platform, () -> { + platform.runCmd(subsystemMountCmd(), o -> { + }, "Failed to mount cgroup subsystem"); + } + ); + } + + @Override + public void handleCpuLimits(GreengrassService component, double cpu) throws IOException { + byte[] content = Files.readAllBytes( + getComponentCpuPeriodPath(component.getServiceName())); + int cpuPeriodUs = Integer.parseInt(new String(content, StandardCharsets.UTF_8).trim()); + + int cpuQuotaUs = (int) (cpuPeriodUs * cpu); + String cpuQuotaUsStr = Integer.toString(cpuQuotaUs); + + Files.write(getComponentCpuQuotaPath(component.getServiceName()), + cpuQuotaUsStr.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public void pauseComponentProcessesCore(GreengrassService component) + throws IOException { + if (LinuxSystemResourceController.CgroupFreezerState.FROZEN.equals( + currentFreezerCgroupState(component.getServiceName()))) { + return; + } + Files.write(getCgroupFreezerStateFilePath(component.getServiceName()), + LinuxSystemResourceController.CgroupFreezerState.FROZEN.toString().getBytes(StandardCharsets.UTF_8), + StandardOpenOption.TRUNCATE_EXISTING); + } + + @Override + public void resumeComponentProcesses(GreengrassService component) throws IOException { + if (LinuxSystemResourceController.CgroupFreezerState.THAWED.equals( + currentFreezerCgroupState(component.getServiceName()))) { + return; + } + + Files.write(getCgroupFreezerStateFilePath(component.getServiceName()), + LinuxSystemResourceController.CgroupFreezerState.THAWED.toString().getBytes(StandardCharsets.UTF_8), + StandardOpenOption.TRUNCATE_EXISTING); + } + + private LinuxSystemResourceController.CgroupFreezerState currentFreezerCgroupState(String component) + throws IOException { + List stateFileContent = + Files.readAllLines(getCgroupFreezerStateFilePath(component)); + if (Utils.isEmpty(stateFileContent) || stateFileContent.size() != 1) { + throw new IOException("Unexpected error reading freezer cgroup state"); + } + return LinuxSystemResourceController.CgroupFreezerState.valueOf(stateFileContent.get(0).trim()); + } +} diff --git a/src/main/java/com/aws/greengrass/util/platforms/unix/linux/CGroupV2.java b/src/main/java/com/aws/greengrass/util/platforms/unix/linux/CGroupV2.java new file mode 100644 index 0000000000..766d0123b9 --- /dev/null +++ b/src/main/java/com/aws/greengrass/util/platforms/unix/linux/CGroupV2.java @@ -0,0 +1,107 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.aws.greengrass.util.platforms.unix.linux; + +import com.aws.greengrass.lifecyclemanager.GreengrassService; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import org.apache.commons.lang3.StringUtils; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; + +@SuppressFBWarnings(value = "DMI_HARDCODED_ABSOLUTE_FILENAME", + justification = "CGroupV2 virtual filesystem path cannot be relative") +public enum CGroupV2 implements CGroupSubSystemPaths { + Memory, CPU, Freezer, Unified; + private static final String CGROUP_SUBTREE_CONTROL_CONTENT = "+cpuset +cpu +io +memory +pids"; + + @Override + public String rootMountCmd() { + return String.format("mount -t cgroup2 none %s", CGROUP_ROOT); + } + + @Override + public Path getSubsystemRootPath() { + return CGROUP_ROOT; + } + + @Override + public Path getComponentMemoryLimitPath(String componentName) { + return getSubsystemComponentPath(componentName).resolve(MEMORY_MAX); + } + + @Override + public Path getCgroupFreezerStateFilePath(String componentName) { + return getSubsystemComponentPath(componentName).resolve(CGROUP_FREEZE); + } + + public Path getRootSubTreeControlPath() { + return getSubsystemRootPath().resolve(CGROUP_SUBTREE_CONTROL); + } + + public Path getGGSubTreeControlPath() { + return getSubsystemGGPath().resolve(CGROUP_SUBTREE_CONTROL); + } + + public Path getComponentCpuMaxPath(String componentName) { + return getSubsystemComponentPath(componentName).resolve(CPU_MAX); + } + + @Override + public void initializeCgroup(GreengrassService component, LinuxPlatform platform) throws IOException { + initializeCgroupCore(component, platform, () -> { + }); + //Enable controllers for root group + Files.write(getRootSubTreeControlPath(), + CGROUP_SUBTREE_CONTROL_CONTENT.getBytes(StandardCharsets.UTF_8)); + //Enable controllers for gg group + Files.write(getGGSubTreeControlPath(), + CGROUP_SUBTREE_CONTROL_CONTENT.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public void handleCpuLimits(GreengrassService component, double cpu) throws IOException { + byte[] content = Files.readAllBytes( + getComponentCpuMaxPath(component.getServiceName())); + String cpuMaxContent = new String(content, StandardCharsets.UTF_8).trim(); + String[] cpuMaxContentArr = cpuMaxContent.split(" "); + String cpuMaxStr = "max"; + String cpuPeriodStr = "100000"; + + if (cpuMaxContentArr.length >= 2) { + cpuMaxStr = cpuMaxContentArr[0]; + cpuPeriodStr = cpuMaxContentArr[1]; + + if (!StringUtils.isEmpty(cpuPeriodStr)) { + int period = Integer.parseInt(cpuPeriodStr.trim()); + int max = (int) (period * cpu); + cpuMaxStr = Integer.toString(max); + } + } + + String latestCpuMaxContent = String.format("%s %s", cpuMaxStr, cpuPeriodStr); + Files.write(getComponentCpuMaxPath(component.getServiceName()), + latestCpuMaxContent.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public void pauseComponentProcessesCore(GreengrassService component) + throws IOException { + Files.write(getCgroupFreezerStateFilePath(component.getServiceName()), + String.valueOf(CgroupV2FreezerState.FROZEN.getIndex()).getBytes(StandardCharsets.UTF_8), + StandardOpenOption.TRUNCATE_EXISTING); + } + + @Override + public void resumeComponentProcesses(GreengrassService component) throws IOException { + Files.write(getCgroupFreezerStateFilePath(component.getServiceName()), + String.valueOf(CgroupV2FreezerState.THAWED.getIndex()).getBytes(StandardCharsets.UTF_8), + StandardOpenOption.TRUNCATE_EXISTING); + } +} diff --git a/src/main/java/com/aws/greengrass/util/platforms/unix/linux/Cgroup.java b/src/main/java/com/aws/greengrass/util/platforms/unix/linux/Cgroup.java deleted file mode 100644 index ed4b814ed7..0000000000 --- a/src/main/java/com/aws/greengrass/util/platforms/unix/linux/Cgroup.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.aws.greengrass.util.platforms.unix.linux; - -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - -import java.nio.file.Path; -import java.nio.file.Paths; - -/** - * Represents Linux cgroup v1 subsystems. - */ -@SuppressFBWarnings(value = "DMI_HARDCODED_ABSOLUTE_FILENAME", justification = "Cgroup virtual filesystem path " - + "cannot be relative") -public enum Cgroup { - Memory("memory"), CPU("cpu,cpuacct"), Freezer("freezer", "freezer"); - - private static final String CGROUP_ROOT = "/sys/fs/cgroup"; - private static final String GG_NAMESPACE = "greengrass"; - private static final String CGROUP_MEMORY_LIMITS = "memory.limit_in_bytes"; - private static final String CPU_CFS_PERIOD_US = "cpu.cfs_period_us"; - private static final String CPU_CFS_QUOTA_US = "cpu.cfs_quota_us"; - private static final String CGROUP_PROCS = "cgroup.procs"; - private static final String FREEZER_STATE_FILE = "freezer.state"; - - private final String osString; - private final String mountSrc; - - Cgroup(String str) { - osString = str; - mountSrc = "cgroup"; - } - - Cgroup(String str, String mountSrc) { - this.osString = str; - this.mountSrc = mountSrc; - } - - public static Path getRootPath() { - return Paths.get(CGROUP_ROOT); - } - - public static String rootMountCmd() { - return String.format("mount -t tmpfs cgroup %s", CGROUP_ROOT); - } - - public String subsystemMountCmd() { - return String.format("mount -t cgroup -o %s %s %s", osString, mountSrc, getSubsystemRootPath()); - } - - public Path getSubsystemRootPath() { - return Paths.get(CGROUP_ROOT).resolve(osString); - } - - public Path getSubsystemGGPath() { - return getSubsystemRootPath().resolve(GG_NAMESPACE); - } - - public Path getSubsystemComponentPath(String componentName) { - return getSubsystemGGPath().resolve(componentName); - } - - public Path getComponentMemoryLimitPath(String componentName) { - return getSubsystemComponentPath(componentName).resolve(CGROUP_MEMORY_LIMITS); - } - - public Path getComponentCpuPeriodPath(String componentName) { - return getSubsystemComponentPath(componentName).resolve(CPU_CFS_PERIOD_US); - } - - public Path getComponentCpuQuotaPath(String componentName) { - return getSubsystemComponentPath(componentName).resolve(CPU_CFS_QUOTA_US); - } - - public Path getCgroupProcsPath(String componentName) { - return getSubsystemComponentPath(componentName).resolve(CGROUP_PROCS); - } - - public Path getCgroupFreezerStateFilePath(String componentName) { - return getSubsystemComponentPath(componentName).resolve(FREEZER_STATE_FILE); - } -} diff --git a/src/main/java/com/aws/greengrass/util/platforms/unix/linux/CgroupV2FreezerState.java b/src/main/java/com/aws/greengrass/util/platforms/unix/linux/CgroupV2FreezerState.java new file mode 100644 index 0000000000..09ebdb79b1 --- /dev/null +++ b/src/main/java/com/aws/greengrass/util/platforms/unix/linux/CgroupV2FreezerState.java @@ -0,0 +1,26 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.aws.greengrass.util.platforms.unix.linux; + +public enum CgroupV2FreezerState { + THAWED(0), + FROZEN(1); + + private int index; + + CgroupV2FreezerState(int index) { + this.index = index; + } + + /** + * Get the index value associated with this CgroupV2FreezerState. + * + * @return the integer index value associated with this CgroupV2FreezerState. + */ + public int getIndex() { + return index; + } +} diff --git a/src/main/java/com/aws/greengrass/util/platforms/unix/linux/InitializeCgroup.java b/src/main/java/com/aws/greengrass/util/platforms/unix/linux/InitializeCgroup.java new file mode 100644 index 0000000000..34a2ffcc9b --- /dev/null +++ b/src/main/java/com/aws/greengrass/util/platforms/unix/linux/InitializeCgroup.java @@ -0,0 +1,13 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.aws.greengrass.util.platforms.unix.linux; + +import java.io.IOException; + +@FunctionalInterface +public interface InitializeCgroup { + void add() throws IOException; +} diff --git a/src/main/java/com/aws/greengrass/util/platforms/unix/linux/LinuxPlatform.java b/src/main/java/com/aws/greengrass/util/platforms/unix/linux/LinuxPlatform.java index 83dcd8c9af..5084789edf 100644 --- a/src/main/java/com/aws/greengrass/util/platforms/unix/linux/LinuxPlatform.java +++ b/src/main/java/com/aws/greengrass/util/platforms/unix/linux/LinuxPlatform.java @@ -7,12 +7,25 @@ import com.aws.greengrass.util.platforms.SystemResourceController; import com.aws.greengrass.util.platforms.unix.UnixPlatform; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +@SuppressFBWarnings(value = "DMI_HARDCODED_ABSOLUTE_FILENAME", + justification = "Cgroup Controller virtual filesystem path cannot be relative") public class LinuxPlatform extends UnixPlatform { - SystemResourceController systemResourceController = new LinuxSystemResourceController(this); + private static final Path CGROUP_CONTROLLERS = Paths.get("/sys/fs/cgroup/cgroup.controllers"); @Override public SystemResourceController getSystemResourceController() { - return systemResourceController; + //if the path exists, identify it as cgroupv2, otherwise identify it as cgroupv1 + if (Files.exists(CGROUP_CONTROLLERS)) { + return new LinuxSystemResourceController(this, false); + } else { + return new LinuxSystemResourceController(this, true); + } } + } diff --git a/src/main/java/com/aws/greengrass/util/platforms/unix/linux/LinuxSystemResourceController.java b/src/main/java/com/aws/greengrass/util/platforms/unix/linux/LinuxSystemResourceController.java index a2bd992321..3252a58827 100644 --- a/src/main/java/com/aws/greengrass/util/platforms/unix/linux/LinuxSystemResourceController.java +++ b/src/main/java/com/aws/greengrass/util/platforms/unix/linux/LinuxSystemResourceController.java @@ -11,16 +11,13 @@ import com.aws.greengrass.util.Coerce; import com.aws.greengrass.util.Utils; import com.aws.greengrass.util.platforms.SystemResourceController; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.zeroturnaround.process.PidUtil; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardOpenOption; import java.util.Arrays; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -32,21 +29,48 @@ import static org.apache.commons.io.FileUtils.ONE_KB; +@SuppressFBWarnings(value = "DMI_HARDCODED_ABSOLUTE_FILENAME", + justification = "Cgroup Controller virtual filesystem path cannot be relative") public class LinuxSystemResourceController implements SystemResourceController { private static final Logger logger = LogManager.getLogger(LinuxSystemResourceController.class); private static final String COMPONENT_NAME = "componentName"; private static final String MEMORY_KEY = "memory"; private static final String CPUS_KEY = "cpus"; - private static final String UNICODE_SPACE = "\\040"; - private static final List RESOURCE_LIMIT_CGROUPS = Arrays.asList(Cgroup.Memory, Cgroup.CPU); + private CGroupSubSystemPaths memoryCgroup; + private CGroupSubSystemPaths cpuCgroup; + private CGroupSubSystemPaths freezerCgroup; + private CGroupSubSystemPaths unifiedCgroup; + private List resourceLimitCgroups; + protected CopyOnWriteArrayList usedCgroups = new CopyOnWriteArrayList<>(); + + protected LinuxPlatform platform; + + /** + * LinuxSystemResourceController Constructor. + * + * @param platform platform + * @param isV1Used if you use v1 + */ + public LinuxSystemResourceController(LinuxPlatform platform, boolean isV1Used) { + this.platform = platform; + + if (isV1Used) { + this.memoryCgroup = CGroupV1.Memory; + this.cpuCgroup = CGroupV1.CPU; + this.freezerCgroup = CGroupV1.Freezer; + resourceLimitCgroups = Arrays.asList( + memoryCgroup, cpuCgroup); + } else { + this.unifiedCgroup = CGroupV2.Unified; + this.memoryCgroup = CGroupV2.Memory; + this.cpuCgroup = CGroupV2.CPU; + this.freezerCgroup = CGroupV2.Freezer; + resourceLimitCgroups = Arrays.asList(unifiedCgroup); + } + } - private final CopyOnWriteArrayList usedCgroups = new CopyOnWriteArrayList<>(); - protected final LinuxPlatform platform; - public LinuxSystemResourceController(LinuxPlatform platform) { - this.platform = platform; - } @Override public void removeResourceController(GreengrassService component) { @@ -64,36 +88,16 @@ public void removeResourceController(GreengrassService component) { @Override public void updateResourceLimits(GreengrassService component, Map resourceLimit) { try { - if (!Files.exists(Cgroup.Memory.getSubsystemComponentPath(component.getServiceName()))) { - initializeCgroup(component, Cgroup.Memory); - } - if (resourceLimit.containsKey(MEMORY_KEY)) { - long memoryLimitInKB = Coerce.toLong(resourceLimit.get(MEMORY_KEY)); - if (memoryLimitInKB > 0) { - String memoryLimit = Long.toString(memoryLimitInKB * ONE_KB); - Files.write(Cgroup.Memory.getComponentMemoryLimitPath(component.getServiceName()), - memoryLimit.getBytes(StandardCharsets.UTF_8)); - } else { - logger.atWarn().kv(COMPONENT_NAME, component.getServiceName()).kv(MEMORY_KEY, memoryLimitInKB) - .log("The provided memory limit is invalid"); - } - } + updateMemoryResourceLimits(component, resourceLimit); - if (!Files.exists(Cgroup.CPU.getSubsystemComponentPath(component.getServiceName()))) { - initializeCgroup(component, Cgroup.CPU); + if (!Files.exists(cpuCgroup.getSubsystemComponentPath(component.getServiceName()))) { + initializeCgroup(component, cpuCgroup); } if (resourceLimit.containsKey(CPUS_KEY)) { double cpu = Coerce.toDouble(resourceLimit.get(CPUS_KEY)); if (cpu > 0) { - byte[] content = Files.readAllBytes( - Cgroup.CPU.getComponentCpuPeriodPath(component.getServiceName())); - int cpuPeriodUs = Integer.parseInt(new String(content, StandardCharsets.UTF_8).trim()); - - int cpuQuotaUs = (int) (cpuPeriodUs * cpu); - String cpuQuotaUsStr = Integer.toString(cpuQuotaUs); + cpuCgroup.handleCpuLimits(component, cpu); - Files.write(Cgroup.CPU.getComponentCpuQuotaPath(component.getServiceName()), - cpuQuotaUsStr.getBytes(StandardCharsets.UTF_8)); } else { logger.atWarn().kv(COMPONENT_NAME, component.getServiceName()).kv(CPUS_KEY, cpu) .log("The provided cpu limit is invalid"); @@ -105,9 +109,27 @@ public void updateResourceLimits(GreengrassService component, Map resourceLimit) throws IOException { + if (!Files.exists(memoryCgroup.getSubsystemComponentPath(component.getServiceName()))) { + initializeCgroup(component, memoryCgroup); + } + if (resourceLimit.containsKey(MEMORY_KEY)) { + long memoryLimitInKB = Coerce.toLong(resourceLimit.get(MEMORY_KEY)); + if (memoryLimitInKB > 0) { + String memoryLimit = Long.toString(memoryLimitInKB * ONE_KB); + Files.write(memoryCgroup.getComponentMemoryLimitPath(component.getServiceName()), + memoryLimit.getBytes(StandardCharsets.UTF_8)); + } else { + logger.atWarn().kv(COMPONENT_NAME, component.getServiceName()).kv(MEMORY_KEY, memoryLimitInKB) + .log("The provided memory limit is invalid"); + } + } + } + @Override public void resetResourceLimits(GreengrassService component) { - for (Cgroup cg : RESOURCE_LIMIT_CGROUPS) { + for (CGroupSubSystemPaths cg : resourceLimitCgroups) { try { if (Files.exists(cg.getSubsystemComponentPath(component.getServiceName()))) { Files.delete(cg.getSubsystemComponentPath(component.getServiceName())); @@ -122,7 +144,7 @@ public void resetResourceLimits(GreengrassService component) { @Override public void addComponentProcess(GreengrassService component, Process process) { - RESOURCE_LIMIT_CGROUPS.forEach(cg -> { + resourceLimitCgroups.forEach(cg -> { try { addComponentProcessToCgroup(component.getServiceName(), process, cg); @@ -139,38 +161,24 @@ public void addComponentProcess(GreengrassService component, Process process) { }, 1, TimeUnit.SECONDS); } catch (IOException e) { - handleErrorAddingPidToCgroup(e, component.getServiceName()); + handleErrorAddingPidToCgroup(e, component.getServiceName()); } }); } @Override public void pauseComponentProcesses(GreengrassService component, List processes) throws IOException { - initializeCgroup(component, Cgroup.Freezer); - - for (Process process: processes) { - addComponentProcessToCgroup(component.getServiceName(), process, Cgroup.Freezer); - } - - if (CgroupFreezerState.FROZEN.equals(currentFreezerCgroupState(component.getServiceName()))) { - return; - } - Files.write(freezerCgroupStateFile(component.getServiceName()), - CgroupFreezerState.FROZEN.toString().getBytes(StandardCharsets.UTF_8), - StandardOpenOption.TRUNCATE_EXISTING); + prePauseComponentProcesses(component, processes); + freezerCgroup.pauseComponentProcessesCore(component); } + @Override public void resumeComponentProcesses(GreengrassService component) throws IOException { - if (CgroupFreezerState.THAWED.equals(currentFreezerCgroupState(component.getServiceName()))) { - return; - } - Files.write(freezerCgroupStateFile(component.getServiceName()), - CgroupFreezerState.THAWED.toString().getBytes(StandardCharsets.UTF_8), - StandardOpenOption.TRUNCATE_EXISTING); + freezerCgroup.resumeComponentProcesses(component); } - private void addComponentProcessToCgroup(String component, Process process, Cgroup cg) + protected void addComponentProcessToCgroup(String component, Process process, CGroupSubSystemPaths cg) throws IOException { if (!Files.exists(cg.getSubsystemComponentPath(component))) { @@ -219,66 +227,23 @@ private void handleErrorAddingPidToCgroup(IOException e, String component) { } } - private Set getMountedPaths() throws IOException { - Set mountedPaths = new HashSet<>(); - - Path procMountsPath = Paths.get("/proc/self/mounts"); - List mounts = Files.readAllLines(procMountsPath); - for (String mount : mounts) { - String[] split = mount.split(" "); - // As reported in fstab(5) manpage, struct is: - // 1st field is volume name - // 2nd field is path with spaces escaped as \040 - // 3rd field is fs type - // 4th field is mount options - // 5th field is used by dump(8) (ignored) - // 6th field is fsck order (ignored) - if (split.length < 6) { - continue; - } - - // We only need the path of the mounts to verify whether cgroup is mounted - String path = split[1].replace(UNICODE_SPACE, " "); - mountedPaths.add(path); - } - return mountedPaths; - } - - private void initializeCgroup(GreengrassService component, Cgroup cgroup) throws IOException { - Set mounts = getMountedPaths(); - if (!mounts.contains(Cgroup.getRootPath().toString())) { - platform.runCmd(Cgroup.rootMountCmd(), o -> {}, "Failed to mount cgroup root"); - Files.createDirectory(cgroup.getSubsystemRootPath()); - } - - if (!mounts.contains(cgroup.getSubsystemRootPath().toString())) { - platform.runCmd(cgroup.subsystemMountCmd(), o -> {}, "Failed to mount cgroup subsystem"); - } - if (!Files.exists(cgroup.getSubsystemGGPath())) { - Files.createDirectory(cgroup.getSubsystemGGPath()); - } - if (!Files.exists(cgroup.getSubsystemComponentPath(component.getServiceName()))) { - Files.createDirectory(cgroup.getSubsystemComponentPath(component.getServiceName())); - } + protected void initializeCgroup(GreengrassService component, CGroupSubSystemPaths cgroup) throws IOException { + cgroup.initializeCgroup(component, platform); usedCgroups.add(cgroup); } - private Set pidsInComponentCgroup(Cgroup cgroup, String component) throws IOException { + private Set pidsInComponentCgroup(CGroupSubSystemPaths cgroup, String component) throws IOException { return Files.readAllLines(cgroup.getCgroupProcsPath(component)) .stream().map(Integer::parseInt).collect(Collectors.toSet()); } - private Path freezerCgroupStateFile(String component) { - return Cgroup.Freezer.getCgroupFreezerStateFilePath(component); - } - private CgroupFreezerState currentFreezerCgroupState(String component) throws IOException { - List stateFileContent = - Files.readAllLines(freezerCgroupStateFile(component)); - if (Utils.isEmpty(stateFileContent) || stateFileContent.size() != 1) { - throw new IOException("Unexpected error reading freezer cgroup state"); + protected void prePauseComponentProcesses(GreengrassService component, List processes) throws IOException { + initializeCgroup(component, freezerCgroup); + + for (Process process : processes) { + addComponentProcessToCgroup(component.getServiceName(), process, freezerCgroup); } - return CgroupFreezerState.valueOf(stateFileContent.get(0).trim()); } public enum CgroupFreezerState { diff --git a/src/test/java/com/aws/greengrass/util/platforms/unix/linux/LinuxSystemResourceControllerV2Test.java b/src/test/java/com/aws/greengrass/util/platforms/unix/linux/LinuxSystemResourceControllerV2Test.java new file mode 100644 index 0000000000..13546841ec --- /dev/null +++ b/src/test/java/com/aws/greengrass/util/platforms/unix/linux/LinuxSystemResourceControllerV2Test.java @@ -0,0 +1,150 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.aws.greengrass.util.platforms.unix.linux; + +import com.aws.greengrass.lifecyclemanager.GreengrassService; +import com.aws.greengrass.testcommons.testutilities.GGExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.anyOf; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.mockito.Mockito.doReturn; + + +@ExtendWith({MockitoExtension.class, GGExtension.class}) +@DisabledOnOs(OS.WINDOWS) +class LinuxSystemResourceControllerV2Test { + @Mock + GreengrassService component; + @Mock + LinuxPlatform platform; + @InjectMocks + @Spy + LinuxSystemResourceController linuxSystemResourceControllerV2 = new LinuxSystemResourceController( + platform, false); + private static final long MEMORY_IN_KB = 2048000; + private static final double CPU_TIME = 0.2; + + @Test + void GIVEN_cgroupv2_WHEN_memory_limit_updated_THEN_memory_limit_file_updated_V2() throws IOException { + assumeTrue(ifCgroupV2(), "skip this test case if v1 is enabled."); + Map resourceLimit = new HashMap<>(); + resourceLimit.put("memory", String.valueOf(MEMORY_IN_KB)); + doReturn("testComponentName").when(component).getServiceName(); + + linuxSystemResourceControllerV2.updateResourceLimits(component, resourceLimit); + String memoryValue = new String(Files.readAllBytes( + CGroupV2.Memory.getComponentMemoryLimitPath("testComponentName")) + , StandardCharsets.UTF_8).trim(); + assertEquals(String.valueOf(MEMORY_IN_KB * 1024), memoryValue); + } + + @Test + void GIVEN_cgroupv2_WHEN_cpu_limit_updated_THEN_cpu_limit_file_updated_V2() throws IOException { + assumeTrue(ifCgroupV2(), "skip this test case if v1 is enabled."); + Map resourceLimit = new HashMap<>(); + resourceLimit.put("cpus", String.valueOf(CPU_TIME)); + doReturn("testComponentName").when(component).getServiceName(); + + linuxSystemResourceControllerV2.updateResourceLimits(component, resourceLimit); + String rawCpuValue = new String(Files.readAllBytes( + CGroupV2.CPU.getComponentCpuMaxPath("testComponentName")) + , StandardCharsets.UTF_8).trim(); + assertEquals(String.valueOf((int) (CPU_TIME * 100000)), rawCpuValue.split(" ")[0]); + } + + @Test + void GIVEN_cgroupv2_WHEN_pause_resume_THEN_freeze_state_1_0_V2() throws IOException { + assumeTrue(ifCgroupV2(), "skip this test case if v1 is enabled."); + LinuxSystemResourceController controller = new LinuxSystemResourceController(platform, false); + doReturn("testComponentName").when(component).getServiceName(); + List processes = new ArrayList<>(); + controller.pauseComponentProcesses(component, processes); + String freezerStateValue = new String(Files.readAllBytes( + CGroupV2.Freezer.getCgroupFreezerStateFilePath("testComponentName")) + , StandardCharsets.UTF_8).trim(); + assertEquals(freezerStateValue, "1"); + + controller.resumeComponentProcesses(component); + freezerStateValue = new String(Files.readAllBytes( + CGroupV2.Freezer.getCgroupFreezerStateFilePath("testComponentName")) + , StandardCharsets.UTF_8).trim(); + assertEquals(freezerStateValue, "0"); + } + + @Test + void GIVEN_cgroupv1_WHEN_memory_limit_updated_THEN_memory_limit_file_updated_V1() throws IOException { + assumeTrue(!ifCgroupV2(), "skip this test case if v2 is enabled."); + Map resourceLimit = new HashMap<>(); + resourceLimit.put("memory", String.valueOf(MEMORY_IN_KB)); + doReturn("testComponentName").when(component).getServiceName(); + + new LinuxSystemResourceController(platform, true).updateResourceLimits(component, resourceLimit); + String memoryValue = new String(Files.readAllBytes( + CGroupV1.Memory.getComponentMemoryLimitPath("testComponentName")) + , StandardCharsets.UTF_8).trim(); + assertEquals(String.valueOf(MEMORY_IN_KB * 1024), memoryValue); + } + + @Test + void GIVEN_cgroupv1_WHEN_cpu_limit_updated_THEN_cpu_limit_file_updated_V1() throws IOException { + assumeTrue(!ifCgroupV2(), "skip this test case if v2 is enabled."); + Map resourceLimit = new HashMap<>(); + resourceLimit.put("cpus", String.valueOf(CPU_TIME)); + doReturn("testComponentName").when(component).getServiceName(); + + new LinuxSystemResourceController(platform, true).updateResourceLimits(component, resourceLimit); + String rawCpuValue = new String(Files.readAllBytes( + CGroupV1.CPU.getComponentCpuQuotaPath("testComponentName")) + , StandardCharsets.UTF_8).trim(); + assertEquals(String.valueOf((int) (CPU_TIME * 100000)), rawCpuValue); + } + + @Test + void GIVEN_cgroupv1_WHEN_pause_resume_THEN_freeze_state_1_0_V1() throws IOException { + assumeTrue(!ifCgroupV2(), "skip this test case if v2 is enabled."); + LinuxSystemResourceController controller = new LinuxSystemResourceController(platform, true); + doReturn("testComponentName").when(component).getServiceName(); + List processes = new ArrayList<>(); + controller.pauseComponentProcesses(component, processes); + String freezerStateValue = new String(Files.readAllBytes( + CGroupV1.Freezer.getCgroupFreezerStateFilePath("testComponentName")) + , StandardCharsets.UTF_8).trim(); + + assertThat(freezerStateValue, + anyOf(is(LinuxSystemResourceController.CgroupFreezerState.FROZEN.toString()), + is(LinuxSystemResourceController.CgroupFreezerState.FREEZING.toString()))); + + controller.resumeComponentProcesses(component); + freezerStateValue = new String(Files.readAllBytes( + CGroupV1.Freezer.getCgroupFreezerStateFilePath("testComponentName")) + , StandardCharsets.UTF_8).trim(); + assertEquals(freezerStateValue, LinuxSystemResourceController.CgroupFreezerState.THAWED.toString()); + } + + private boolean ifCgroupV2() { + return Files.exists(Paths.get("/sys/fs/cgroup/cgroup.controllers")); + } +} \ No newline at end of file