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..597749aabe 100644 --- a/src/integrationtests/java/com/aws/greengrass/integrationtests/ipc/IPCHibernateTest.java +++ b/src/integrationtests/java/com/aws/greengrass/integrationtests/ipc/IPCHibernateTest.java @@ -10,6 +10,7 @@ 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.CgroupSubSystem; import com.aws.greengrass.util.platforms.unix.linux.LinuxSystemResourceController; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -115,7 +116,7 @@ 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(new Cgroup(CgroupSubSystem.Freezer).getCgroupFreezerStateFilePath(serviceName)), StandardCharsets.UTF_8).trim()); } } 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 f6b37ace13..497437e801 100644 --- a/src/integrationtests/java/com/aws/greengrass/integrationtests/lifecyclemanager/GenericExternalServiceIntegTest.java +++ b/src/integrationtests/java/com/aws/greengrass/integrationtests/lifecyclemanager/GenericExternalServiceIntegTest.java @@ -21,7 +21,11 @@ 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.CgroupSubSystem; +import com.aws.greengrass.util.platforms.unix.linux.CgroupSubSystemV2; +import com.aws.greengrass.util.platforms.unix.linux.LinuxPlatform; import com.aws.greengrass.util.platforms.unix.linux.LinuxSystemResourceController; +import com.aws.greengrass.util.platforms.unix.linux.LinuxSystemResourceControllerV2; import org.apache.commons.lang3.SystemUtils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -32,14 +36,18 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; 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.HashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; @@ -75,6 +83,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue; import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -84,6 +93,9 @@ class GenericExternalServiceIntegTest extends BaseITCase { private Kernel kernel; + @Mock + LinuxPlatform platform; + static Stream posixTestUserConfig() { return Stream.of( arguments("config_run_with_user.yaml", "nobody", "nobody"), @@ -582,6 +594,87 @@ 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 { + String componentName = "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(componentName) && newState.equals(State.RUNNING)) { + service.countDown(); + } + }); + + String componentPathString = "/sys/fs/cgroup/greengrass/" + componentName; + String cgroupv2RootPathString = "/sys/fs/cgroup"; + String cgroupv2GGPathString = "/sys/fs/cgroup/greengrass"; + Path cgroupv2GGPath = Paths.get(cgroupv2GGPathString); + Path path = Paths.get(componentPathString); + Path cgroupv2RootPath = Paths.get(cgroupv2RootPathString); + + Files.createDirectories(path); + + if (!Files.exists(path.resolve("memory.max"))) { + Files.createFile(path.resolve("memory.max")); + } + if (!Files.exists(path.resolve("cpu.max"))) { + Files.createFile(path.resolve("cpu.max")); + } + Files.write(path.resolve("cpu.max"), "max 100000".getBytes(StandardCharsets.UTF_8)); + + if (!Files.exists(cgroupv2RootPath.resolve("cgroup.subtree_control"))) { + Files.createFile(cgroupv2RootPath.resolve("cgroup.subtree_control")); + } + if (!Files.exists(cgroupv2GGPath.resolve("cgroup.subtree_control"))) { + Files.createFile(cgroupv2GGPath.resolve("cgroup.subtree_control")); + } + + kernel.launch(); + assertTrue(service.await(20, TimeUnit.SECONDS), "service running"); + + LinuxSystemResourceControllerV2 linuxSystemResourceControllerV2 = new LinuxSystemResourceControllerV2(platform); + LinuxSystemResourceControllerV2 spyV2 = spy(linuxSystemResourceControllerV2); + Set rootPath = new HashSet<>(); + rootPath.add(cgroupv2RootPathString); + + lenient().doReturn(spyV2).when(platform).getSystemResourceController(); + lenient().doReturn(rootPath).when(spyV2).getMountedPaths(); + // Run with nucleus default resource limit + assertResourceLimits_V2(componentName, 10240l * 1024, 1.5); + + // Run with component resource limit + kernel.getConfig().lookup(SERVICES_NAMESPACE_TOPIC, componentName, RUN_WITH_NAMESPACE_TOPIC, + SYSTEM_RESOURCE_LIMITS_TOPICS, "memory").withValue(102400l); + + kernel.getConfig().lookup(SERVICES_NAMESPACE_TOPIC, componentName, RUN_WITH_NAMESPACE_TOPIC, + SYSTEM_RESOURCE_LIMITS_TOPICS, "cpus").withValue(0.5); + // Block until events are completed + kernel.getContext().waitForPublishQueueToClear(); + + assertResourceLimits_V2(componentName, 102400l * 1024, 0.5); + + // Run with updated component resource limit + kernel.getConfig().lookup(SERVICES_NAMESPACE_TOPIC, componentName, RUN_WITH_NAMESPACE_TOPIC, + SYSTEM_RESOURCE_LIMITS_TOPICS, "memory").withValue(51200l); + kernel.getConfig().lookup(SERVICES_NAMESPACE_TOPIC, componentName, RUN_WITH_NAMESPACE_TOPIC, + SYSTEM_RESOURCE_LIMITS_TOPICS, "cpus").withValue(0.35); + kernel.getConfig().lookup(SERVICES_NAMESPACE_TOPIC, componentName, VERSION_CONFIG_KEY).withValue("2.0.0"); + // Block until events are completed + kernel.getContext().waitForPublishQueueToClear(); + + assertResourceLimits_V2(componentName, 51200l * 1024, 0.35); + + //Remove component resource limit, should fall back to default + kernel.getConfig().lookupTopics(SERVICES_NAMESPACE_TOPIC, componentName, RUN_WITH_NAMESPACE_TOPIC, + SYSTEM_RESOURCE_LIMITS_TOPICS).remove(); + kernel.getContext().waitForPublishQueueToClear(); + + assertResourceLimits_V2(componentName, 10240l * 1024, 1.5); + } + @Test void GIVEN_service_starts_up_WHEN_service_breaks_THEN_status_details_persisted_for_errored_and_broken_states() throws Exception { @@ -713,11 +806,11 @@ void GIVEN_service_starts_up_WHEN_startup_times_out_THEN_timeout_error_code_pers } private void assertResourceLimits(String componentName, long memory, double cpus) throws Exception { - byte[] buf1 = Files.readAllBytes(Cgroup.Memory.getComponentMemoryLimitPath(componentName)); + byte[] buf1 = Files.readAllBytes(new Cgroup(CgroupSubSystem.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(new Cgroup(CgroupSubSystem.CPU).getComponentCpuQuotaPath(componentName)); + byte[] buf3 = Files.readAllBytes(new Cgroup(CgroupSubSystem.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()); @@ -725,6 +818,22 @@ private void assertResourceLimits(String componentName, long memory, double cpus assertThat(expectedQuota, equalTo(quota)); } + private void assertResourceLimits_V2(String componentName, long memory, double cpus) throws Exception { + byte[] buf1 = Files.readAllBytes(new Cgroup(CgroupSubSystemV2.Memory).getComponentMemoryLimitPath(componentName)); + assertThat(memory, equalTo(Long.parseLong(new String(buf1, StandardCharsets.UTF_8).trim()))); + + byte[] buf2 = Files.readAllBytes(new Cgroup(CgroupSubSystemV2.CPU).getComponentCpuMaxPath(componentName)); + + 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)); + } + void GIVEN_running_service_WHEN_pause_resume_requested_THEN_pause_resume_Service_and_freeze_thaw_cgroup( ExtensionContext context) throws Exception { ignoreExceptionOfType(context, FileSystemException.class); @@ -761,7 +870,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(new Cgroup(CgroupSubSystem.Freezer).getCgroupFreezerStateFilePath(serviceName)) , StandardCharsets.UTF_8).trim()); } } diff --git a/src/main/java/com/aws/greengrass/util/platforms/unix/linux/CGroupSubSystemPath.java b/src/main/java/com/aws/greengrass/util/platforms/unix/linux/CGroupSubSystemPath.java new file mode 100644 index 0000000000..500fdfd03e --- /dev/null +++ b/src/main/java/com/aws/greengrass/util/platforms/unix/linux/CGroupSubSystemPath.java @@ -0,0 +1,73 @@ +/* + * 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; + +@SuppressFBWarnings(value = "DMI_HARDCODED_ABSOLUTE_FILENAME", + justification = "CGroupSubSystemPath virtual filesystem path cannot be relative") +public interface CGroupSubSystemPath { + String CGROUP_ROOT = "/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"; + + default Path getRootPath() { + return Paths.get(CGROUP_ROOT); + } + + String rootMountCmd(); + + default String subsystemMountCmd() { + return null; + } + + Path getSubsystemRootPath(); + + Path getSubsystemGGPath(); + + Path getSubsystemComponentPath(String componentName); + + Path getComponentMemoryLimitPath(String componentName); + + default Path getComponentCpuPeriodPath(String componentName) { + return null; + } + + default Path getComponentCpuQuotaPath(String componentName) { + return null; + } + + Path getCgroupProcsPath(String componentName); + + Path getCgroupFreezerStateFilePath(String componentName); + + default Path getRootSubTreeControlPath() { + return null; + } + + default Path getGGSubTreeControlPath() { + return null; + } + + default Path getComponentCpuMaxPath(String componentName) { + return null; + } + + default Path getCgroupFreezePath(String componentName) { + return null; + } +} 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 index ed4b814ed7..43b877f506 100644 --- 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 @@ -5,81 +5,93 @@ 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. + * Represents Linux cgroup 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"; +public class Cgroup { + private final CGroupSubSystemPath subSystem; - private final String osString; - private final String mountSrc; - - Cgroup(String str) { - osString = str; - mountSrc = "cgroup"; + public Cgroup(CGroupSubSystemPath subSystem) { + this.subSystem = subSystem; } - Cgroup(String str, String mountSrc) { - this.osString = str; - this.mountSrc = mountSrc; + public Path getRootPath() { + return subSystem.getRootPath(); } - public static Path getRootPath() { - return Paths.get(CGROUP_ROOT); - } - - public static String rootMountCmd() { - return String.format("mount -t tmpfs cgroup %s", CGROUP_ROOT); + /** + * root mount cmd. + * + * @return mount command string + */ + public String rootMountCmd() { + return subSystem.rootMountCmd(); } public String subsystemMountCmd() { - return String.format("mount -t cgroup -o %s %s %s", osString, mountSrc, getSubsystemRootPath()); + return subSystem.subsystemMountCmd(); } public Path getSubsystemRootPath() { - return Paths.get(CGROUP_ROOT).resolve(osString); + return subSystem.getSubsystemRootPath(); } public Path getSubsystemGGPath() { - return getSubsystemRootPath().resolve(GG_NAMESPACE); + return subSystem.getSubsystemGGPath(); } public Path getSubsystemComponentPath(String componentName) { - return getSubsystemGGPath().resolve(componentName); + return subSystem.getSubsystemComponentPath(componentName); } + /** + * get component memory limit path. + * + * @param componentName componentName + * @return memory limit Path + */ public Path getComponentMemoryLimitPath(String componentName) { - return getSubsystemComponentPath(componentName).resolve(CGROUP_MEMORY_LIMITS); + return subSystem.getComponentMemoryLimitPath(componentName); } public Path getComponentCpuPeriodPath(String componentName) { - return getSubsystemComponentPath(componentName).resolve(CPU_CFS_PERIOD_US); + return subSystem.getComponentCpuPeriodPath(componentName); } public Path getComponentCpuQuotaPath(String componentName) { - return getSubsystemComponentPath(componentName).resolve(CPU_CFS_QUOTA_US); + return subSystem.getComponentCpuQuotaPath(componentName); } public Path getCgroupProcsPath(String componentName) { - return getSubsystemComponentPath(componentName).resolve(CGROUP_PROCS); + return subSystem.getCgroupProcsPath(componentName); } + /** + * get cgroup freezer path. + * + * @param componentName componentName + * @return cgroup freezer path + */ public Path getCgroupFreezerStateFilePath(String componentName) { - return getSubsystemComponentPath(componentName).resolve(FREEZER_STATE_FILE); + return subSystem.getCgroupFreezerStateFilePath(componentName); + } + + public Path getRootSubTreeControlPath() { + return subSystem.getRootSubTreeControlPath(); } + + public Path getGGSubTreeControlPath() { + return subSystem.getGGSubTreeControlPath(); + } + + public Path getComponentCpuMaxPath(String componentName) { + return subSystem.getComponentCpuMaxPath(componentName); + } + + public Path getCgroupFreezePath(String componentName) { + return subSystem.getCgroupFreezePath(componentName); + } + } diff --git a/src/main/java/com/aws/greengrass/util/platforms/unix/linux/CgroupSubSystem.java b/src/main/java/com/aws/greengrass/util/platforms/unix/linux/CgroupSubSystem.java new file mode 100644 index 0000000000..6b36dd7c3a --- /dev/null +++ b/src/main/java/com/aws/greengrass/util/platforms/unix/linux/CgroupSubSystem.java @@ -0,0 +1,93 @@ +/* + * 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; + +@SuppressFBWarnings(value = "DMI_HARDCODED_ABSOLUTE_FILENAME", + justification = "CgroupSubSystem virtual filesystem path cannot be relative") +public enum CgroupSubSystem implements CGroupSubSystemPath { + Memory("memory", ""), CPU("cpu,cpuacct", ""), Freezer("freezer", "freezer"); + + private String osString; + private String mountSrc; + + CgroupSubSystem(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); + } + + @Override + public String subsystemMountCmd() { + return String.format("mount -t cgroup -o %s %s %s", osString, mountSrc, getSubsystemRootPath()); + } + + @Override + public Path getSubsystemRootPath() { + return Paths.get(CGROUP_ROOT).resolve(osString); + } + + @Override + public Path getSubsystemGGPath() { + return getSubsystemRootPath().resolve(GG_NAMESPACE); + } + + @Override + public Path getSubsystemComponentPath(String componentName) { + return getSubsystemGGPath().resolve(componentName); + } + + @Override + public Path getComponentMemoryLimitPath(String componentName) { + return getSubsystemComponentPath(componentName).resolve(CGROUP_MEMORY_LIMITS); + } + + @Override + public Path getComponentCpuPeriodPath(String componentName) { + return getSubsystemComponentPath(componentName).resolve(CPU_CFS_PERIOD_US); + } + + @Override + public Path getComponentCpuQuotaPath(String componentName) { + return getSubsystemComponentPath(componentName).resolve(CPU_CFS_QUOTA_US); + } + + @Override + public Path getCgroupProcsPath(String componentName) { + return getSubsystemComponentPath(componentName).resolve(CGROUP_PROCS); + } + + @Override + 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/CgroupSubSystemV2.java b/src/main/java/com/aws/greengrass/util/platforms/unix/linux/CgroupSubSystemV2.java new file mode 100644 index 0000000000..7a3cf1b31e --- /dev/null +++ b/src/main/java/com/aws/greengrass/util/platforms/unix/linux/CgroupSubSystemV2.java @@ -0,0 +1,72 @@ +/* + * 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; + +@SuppressFBWarnings(value = "DMI_HARDCODED_ABSOLUTE_FILENAME", + justification = "CgroupSubSystemV2 virtual filesystem path cannot be relative") +public enum CgroupSubSystemV2 implements CGroupSubSystemPath { + Memory, CPU, Freezer, Unified; + + @Override + public String rootMountCmd() { + return String.format("mount -t cgroup2 none %s", CGROUP_ROOT); + } + + @Override + public Path getSubsystemRootPath() { + return Paths.get(CGROUP_ROOT); + } + + @Override + public Path getSubsystemGGPath() { + return getSubsystemRootPath().resolve(GG_NAMESPACE); + } + + @Override + public Path getSubsystemComponentPath(String componentName) { + return getSubsystemGGPath().resolve(componentName); + } + + @Override + public Path getComponentMemoryLimitPath(String componentName) { + return getSubsystemComponentPath(componentName).resolve(MEMORY_MAX); + } + + @Override + public Path getCgroupProcsPath(String componentName) { + return getSubsystemComponentPath(componentName).resolve(CGROUP_PROCS); + } + + @Override + public Path getCgroupFreezerStateFilePath(String componentName) { + return getSubsystemComponentPath(componentName).resolve(CGROUP_FREEZE); + } + + @Override + public Path getRootSubTreeControlPath() { + return getSubsystemRootPath().resolve(CGROUP_SUBTREE_CONTROL); + } + + @Override + public Path getGGSubTreeControlPath() { + return getSubsystemGGPath().resolve(CGROUP_SUBTREE_CONTROL); + } + + @Override + public Path getComponentCpuMaxPath(String componentName) { + return getSubsystemComponentPath(componentName).resolve(CPU_MAX); + } + + @Override + public Path getCgroupFreezePath(String componentName) { + return getSubsystemComponentPath(componentName).resolve(CGROUP_FREEZE); + } +} 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/LinuxPlatform.java b/src/main/java/com/aws/greengrass/util/platforms/unix/linux/LinuxPlatform.java index 83dcd8c9af..6bb3944688 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,33 @@ 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 String CGROUP_ROOT = "/sys/fs/cgroup"; + private static final String CGROUP_CONTROLLERS = "cgroup.controllers"; + + SystemResourceController systemResourceController; @Override public SystemResourceController getSystemResourceController() { + //if the path exists, identify it as cgroupv1, otherwise identify it as cgroupv2 + if (Files.exists(getControllersRootPath())) { + systemResourceController = new LinuxSystemResourceControllerV2(this); + } else { + systemResourceController = new LinuxSystemResourceController(this); + } + return systemResourceController; } + + private Path getControllersRootPath() { + return Paths.get(CGROUP_ROOT).resolve(CGROUP_CONTROLLERS); + } } 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..f926b80d84 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 @@ -34,20 +34,34 @@ 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"; + + protected static final String COMPONENT_NAME = "componentName"; + protected static final String MEMORY_KEY = "memory"; + protected 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); + protected Cgroup memoryCgroup; + protected Cgroup cpuCgroup; + protected Cgroup freezerCgroup; + protected Cgroup unifiedCgroup; + protected List resourceLimitCgroups; + protected CopyOnWriteArrayList usedCgroups = new CopyOnWriteArrayList<>(); + + protected LinuxPlatform platform; - private final CopyOnWriteArrayList usedCgroups = new CopyOnWriteArrayList<>(); + public LinuxSystemResourceController() { - protected final LinuxPlatform platform; + } - public LinuxSystemResourceController(LinuxPlatform platform) { + LinuxSystemResourceController(LinuxPlatform platform) { this.platform = platform; + this.memoryCgroup = new Cgroup(CgroupSubSystem.Memory); + this.cpuCgroup = new Cgroup(CgroupSubSystem.CPU); + this.freezerCgroup = new Cgroup(CgroupSubSystem.Freezer); + resourceLimitCgroups = Arrays.asList( + memoryCgroup, cpuCgroup); } + @Override public void removeResourceController(GreengrassService component) { usedCgroups.forEach(cg -> { @@ -64,36 +78,24 @@ 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())); + cpuCgroup.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(Cgroup.CPU.getComponentCpuQuotaPath(component.getServiceName()), + Files.write(cpuCgroup.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 +107,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 (Cgroup cg : resourceLimitCgroups) { try { if (Files.exists(cg.getSubsystemComponentPath(component.getServiceName()))) { Files.delete(cg.getSubsystemComponentPath(component.getServiceName())); @@ -122,7 +142,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,19 +159,14 @@ 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); - } - + prePauseComponentProcesses(component, processes); if (CgroupFreezerState.FROZEN.equals(currentFreezerCgroupState(component.getServiceName()))) { return; } @@ -160,6 +175,7 @@ public void pauseComponentProcesses(GreengrassService component, List p StandardOpenOption.TRUNCATE_EXISTING); } + @Override public void resumeComponentProcesses(GreengrassService component) throws IOException { if (CgroupFreezerState.THAWED.equals(currentFreezerCgroupState(component.getServiceName()))) { @@ -170,7 +186,7 @@ public void resumeComponentProcesses(GreengrassService component) throws IOExcep StandardOpenOption.TRUNCATE_EXISTING); } - private void addComponentProcessToCgroup(String component, Process process, Cgroup cg) + protected void addComponentProcessToCgroup(String component, Process process, Cgroup cg) throws IOException { if (!Files.exists(cg.getSubsystemComponentPath(component))) { @@ -219,7 +235,12 @@ private void handleErrorAddingPidToCgroup(IOException e, String component) { } } - private Set getMountedPaths() throws IOException { + /** + * Get mounted path for Cgroup. + * @return mounted path set + * @throws IOException IOException + */ + public Set getMountedPaths() throws IOException { Set mountedPaths = new HashSet<>(); Path procMountsPath = Paths.get("/proc/self/mounts"); @@ -244,15 +265,18 @@ private Set getMountedPaths() throws IOException { return mountedPaths; } - private void initializeCgroup(GreengrassService component, Cgroup cgroup) throws IOException { + protected 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"); + + 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"); + platform.runCmd(cgroup.subsystemMountCmd(), o -> { + }, "Failed to mount cgroup subsystem"); } if (!Files.exists(cgroup.getSubsystemGGPath())) { Files.createDirectory(cgroup.getSubsystemGGPath()); @@ -260,6 +284,7 @@ private void initializeCgroup(GreengrassService component, Cgroup cgroup) throws if (!Files.exists(cgroup.getSubsystemComponentPath(component.getServiceName()))) { Files.createDirectory(cgroup.getSubsystemComponentPath(component.getServiceName())); } + usedCgroups.add(cgroup); } @@ -268,8 +293,8 @@ private Set pidsInComponentCgroup(Cgroup cgroup, String component) thro .stream().map(Integer::parseInt).collect(Collectors.toSet()); } - private Path freezerCgroupStateFile(String component) { - return Cgroup.Freezer.getCgroupFreezerStateFilePath(component); + protected Path freezerCgroupStateFile(String component) { + return freezerCgroup.getCgroupFreezerStateFilePath(component); } private CgroupFreezerState currentFreezerCgroupState(String component) throws IOException { @@ -281,6 +306,14 @@ private CgroupFreezerState currentFreezerCgroupState(String component) throws IO return CgroupFreezerState.valueOf(stateFileContent.get(0).trim()); } + protected void prePauseComponentProcesses(GreengrassService component, List processes) throws IOException { + initializeCgroup(component, freezerCgroup); + + for (Process process : processes) { + addComponentProcessToCgroup(component.getServiceName(), process, freezerCgroup); + } + } + public enum CgroupFreezerState { THAWED, FREEZING, FROZEN } diff --git a/src/main/java/com/aws/greengrass/util/platforms/unix/linux/LinuxSystemResourceControllerV2.java b/src/main/java/com/aws/greengrass/util/platforms/unix/linux/LinuxSystemResourceControllerV2.java new file mode 100644 index 0000000000..57a7f52c96 --- /dev/null +++ b/src/main/java/com/aws/greengrass/util/platforms/unix/linux/LinuxSystemResourceControllerV2.java @@ -0,0 +1,133 @@ +/* + * 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.logging.api.Logger; +import com.aws.greengrass.logging.impl.LogManager; +import com.aws.greengrass.util.Coerce; +import org.apache.commons.lang3.StringUtils; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * https://www.kernel.org/doc/Documentation/cgroup-v2.txt + */ +public class LinuxSystemResourceControllerV2 extends LinuxSystemResourceController { + private static final Logger logger = LogManager.getLogger(LinuxSystemResourceControllerV2.class); + private static final String CGROUP_SUBTREE_CONTROL_CONTENT = "+cpuset +cpu +io +memory +pids"; + + /** + * Linux system resource controller V2 constrcutors. + * + * @param platform linux platform + */ + public LinuxSystemResourceControllerV2(LinuxPlatform platform) { + super(); + this.platform = platform; + this.unifiedCgroup = new Cgroup(CgroupSubSystemV2.Unified); + this.memoryCgroup = new Cgroup(CgroupSubSystemV2.Memory); + this.cpuCgroup = new Cgroup(CgroupSubSystemV2.CPU); + this.freezerCgroup = new Cgroup(CgroupSubSystemV2.Freezer); + resourceLimitCgroups = Arrays.asList(unifiedCgroup); + } + + + @Override + public void updateResourceLimits(GreengrassService component, Map resourceLimit) { + try { + super.updateMemoryResourceLimits(component, resourceLimit); + + if (resourceLimit.containsKey(CPUS_KEY)) { + double cpu = Coerce.toDouble(resourceLimit.get(CPUS_KEY)); + if (cpu > 0) { + handleCpuLimits(component, cpu); + } else { + logger.atWarn().kv(COMPONENT_NAME, component.getServiceName()).kv(CPUS_KEY, cpu) + .log("The provided cpu limit is invalid"); + } + } + } catch (IOException e) { + logger.atError().setCause(e).kv(COMPONENT_NAME, component.getServiceName()) + .log("Failed to apply resource limits"); + } + } + + @Override + public void pauseComponentProcesses(GreengrassService component, List processes) throws IOException { + prePauseComponentProcesses(component, processes); + + Files.write(freezerCgroupStateFile(component.getServiceName()), + String.valueOf(CgroupV2FreezerState.FROZEN.getIndex()).getBytes(StandardCharsets.UTF_8), + StandardOpenOption.TRUNCATE_EXISTING); + } + + @Override + public void resumeComponentProcesses(GreengrassService component) throws IOException { + Files.write(freezerCgroupStateFile(component.getServiceName()), + String.valueOf(CgroupV2FreezerState.THAWED.getIndex()).getBytes(StandardCharsets.UTF_8), + StandardOpenOption.TRUNCATE_EXISTING); + } + + @Override + protected 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 (!Files.exists(cgroup.getSubsystemGGPath())) { + Files.createDirectory(cgroup.getSubsystemGGPath()); + } + if (!Files.exists(cgroup.getSubsystemComponentPath(component.getServiceName()))) { + Files.createDirectory(cgroup.getSubsystemComponentPath(component.getServiceName())); + } + + //Enable controllers for root group + Files.write(cgroup.getRootSubTreeControlPath(), + CGROUP_SUBTREE_CONTROL_CONTENT.getBytes(StandardCharsets.UTF_8)); + //Enable controllers for gg group + Files.write(cgroup.getGGSubTreeControlPath(), + CGROUP_SUBTREE_CONTROL_CONTENT.getBytes(StandardCharsets.UTF_8)); + + usedCgroups.add(cgroup); + } + + private void handleCpuLimits(GreengrassService component, double cpu) throws IOException { + byte[] content = Files.readAllBytes( + cpuCgroup.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(cpuCgroup.getComponentCpuMaxPath(component.getServiceName()), + latestCpuMaxContent.getBytes(StandardCharsets.UTF_8)); + } + +} 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..b6f43377a7 --- /dev/null +++ b/src/test/java/com/aws/greengrass/util/platforms/unix/linux/LinuxSystemResourceControllerV2Test.java @@ -0,0 +1,111 @@ +/* + * 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 com.aws.greengrass.util.Utils; +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.File; +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.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.when; + +@ExtendWith({MockitoExtension.class, GGExtension.class}) +@DisabledOnOs(OS.WINDOWS) +class LinuxSystemResourceControllerV2Test { + @Mock + GreengrassService component; + @Mock + Cgroup cpuCgroup; + @Mock + Cgroup memoryCgroup; + @Mock + LinuxPlatform platform; + @InjectMocks + @Spy + LinuxSystemResourceControllerV2 linuxSystemResourceControllerV2 = new LinuxSystemResourceControllerV2(platform); + private static final String FILE_PATH = "/cgroupv2test"; + private static final String CGROUP_MEMORY_LIMIT_FILE_NAME = "memory.txt"; + private static final String CGROUP_CPU_LIMIT_FILE_NAME = "cpu.txt"; + private static final long MEMORY_IN_KB = 2048000; + private static final double CPU_TIME = 0.5; + private static final String COMPONENT_NAME = "testComponentName"; + + @Test + void GIVEN_cgroupv2_WHEN_memory_limit_updated_THEN_memory_limit_file_updated() throws IOException { + Map resourceLimit = new HashMap<>(); + resourceLimit.put("memory", String.valueOf(MEMORY_IN_KB)); + doReturn("testComponentName").when(component).getServiceName(); + + Path path = Paths.get(FILE_PATH + "/" + CGROUP_MEMORY_LIMIT_FILE_NAME); + + Path componentNameFolderPath = Paths.get(FILE_PATH); + Utils.createPaths(componentNameFolderPath); + File file = new File(FILE_PATH + "/" + CGROUP_MEMORY_LIMIT_FILE_NAME); + if (!Files.exists(path)) { + file.createNewFile(); + } + + when(memoryCgroup.getComponentMemoryLimitPath(COMPONENT_NAME)).thenReturn(path); + lenient().when(memoryCgroup.getSubsystemComponentPath(COMPONENT_NAME)).thenReturn(componentNameFolderPath); + linuxSystemResourceControllerV2.updateResourceLimits(component, resourceLimit); + + List mounts = Files.readAllLines(path); + assertEquals(String.valueOf(MEMORY_IN_KB * 1024), mounts.get(0)); + + Files.deleteIfExists(path); + Files.deleteIfExists(componentNameFolderPath); + } + + @Test + void GIVEN_cgroupv2_WHEN_cpu_limit_updated_THEN_cpu_limit_file_updated() throws IOException { + Map resourceLimit = new HashMap<>(); + resourceLimit.put("cpus", String.valueOf(CPU_TIME)); + doReturn("testComponentName").when(component).getServiceName(); + + Path path = Paths.get(FILE_PATH + "/" + CGROUP_CPU_LIMIT_FILE_NAME); + + Path componentNameFolderPath = Paths.get(FILE_PATH); + Utils.createPaths(componentNameFolderPath); + File file = new File(FILE_PATH + "/" + CGROUP_CPU_LIMIT_FILE_NAME); + if (!Files.exists(path)) { + file.createNewFile(); + } + + Files.write(path, "max 100000".getBytes(StandardCharsets.UTF_8)); + + doNothing().when(linuxSystemResourceControllerV2).updateMemoryResourceLimits(component, resourceLimit); + when(cpuCgroup.getComponentCpuMaxPath(COMPONENT_NAME)).thenReturn(path); + lenient().when(cpuCgroup.getSubsystemComponentPath(COMPONENT_NAME)).thenReturn(componentNameFolderPath); + linuxSystemResourceControllerV2.updateResourceLimits(component, resourceLimit); + + List mounts = Files.readAllLines(path); + assertEquals((int) (CPU_TIME * 100000) + " 100000", mounts.get(0)); + + Files.deleteIfExists(path); + Files.deleteIfExists(componentNameFolderPath); + } +} \ No newline at end of file