Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
Expand Down Expand Up @@ -89,9 +90,7 @@ private void waitForServicesToStart() {
Deployment.DeploymentStage stage = deployment.getDeploymentStage();
DeploymentResult result = null;
try {
List<GreengrassService> servicesToTrack =
kernel.orderedDependencies().stream().filter(GreengrassService::shouldAutoStart)
.filter(o -> !kernel.getMain().equals(o)).collect(Collectors.toList());
Set<GreengrassService> servicesToTrack = kernel.findAutoStartableServicesToTrack();
long mergeTimestamp = kernel.getConfig().lookup("system", "rootpath").getModtime();

logger.atInfo().kv("serviceToTrack", servicesToTrack).kv("mergeTime", mergeTimestamp)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,9 @@
import com.aws.greengrass.lifecyclemanager.GreengrassService;
import com.aws.greengrass.lifecyclemanager.Kernel;
import com.aws.greengrass.lifecyclemanager.exceptions.ServiceLoadException;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -82,6 +79,9 @@ public void activate(Map<String, Object> newConfig, Deployment deployment,

try {
Set<GreengrassService> servicesToTrack = servicesChangeManager.servicesToTrack();
// Exclude all non-autoStartable services and their dependers
// Find the services which both changed and auto startable
servicesToTrack.retainAll(kernel.findAutoStartableServicesToTrack());
logger.atDebug(MERGE_CONFIG_EVENT_KEY).kv("serviceToTrack", servicesToTrack).kv("mergeTime", mergeTime)
.log("Applied new service config. Waiting for services to complete update");
waitForServicesToStart(servicesToTrack, mergeTime, kernel, totallyCompleteFuture);
Expand Down Expand Up @@ -144,10 +144,13 @@ void rollback(DeploymentDocument deploymentDocument, CompletableFuture<Deploymen
// be expected to still be broken
Set<GreengrassService> servicesToTrackForRollback = rollbackManager.servicesToTrack();

// Also don't track services if they have (transitive) hard dependencies on already-broken services
Set<GreengrassService> brokenServiceAndDependers
= findServiceDependers(servicesToTrackForRollback, rollbackManager.getAlreadyBrokenServices());
// Convert broken services names from String to GreengrassService type
Set<GreengrassService> brokenServices = servicesToTrackForRollback.stream()
.filter(service -> rollbackManager.getAlreadyBrokenServices().contains(service.getName()))
.collect(Collectors.toSet());

// Also don't track services if they have (transitive) hard dependencies on already-broken services
Set<GreengrassService> brokenServiceAndDependers = kernel.findDependers(brokenServices);
servicesToTrackForRollback.removeAll(brokenServiceAndDependers);

logger.atInfo(MERGE_CONFIG_EVENT_KEY)
Expand All @@ -170,37 +173,6 @@ void rollback(DeploymentDocument deploymentDocument, CompletableFuture<Deploymen
}
}

/**
* Finds all services which are dependers of given broken services, directly or indirectly
* This method performs a breadth-first search, starting from the broken services and traversing through
* service dependencies.
* @param rollbackServices the set of rollback services to track
* @param brokenServiceNames the set of broken service names
* @return a set of all services depending on the broken services, including themselves
*/
@SuppressFBWarnings("RV_RETURN_VALUE_IGNORED_BAD_PRACTICE")
private Set<GreengrassService> findServiceDependers(final Set<GreengrassService> rollbackServices,
final Set<String> brokenServiceNames) {

Set<GreengrassService> dependerServices = rollbackServices.stream()
.filter(service -> brokenServiceNames.contains(service.getName()))
.collect(Collectors.toSet());
Queue<GreengrassService> dependers = new LinkedList<>(dependerServices);

// Breadth-first search to find all dependent services, staring from broken services
while (!dependers.isEmpty()) {
GreengrassService currentService = dependers.poll();
for (GreengrassService depender : currentService.getHardDependers()) {
// Ensure dependers haven't been processed
if (dependerServices.add(depender)) {
dependers.offer(depender);
}
}
}
return dependerServices;
}


private void handleFailureRollback(CompletableFuture totallyCompleteFuture, Throwable deploymentFailureCause,
Throwable rollbackFailureCause) {
// Rollback execution failed
Expand All @@ -209,5 +181,4 @@ private void handleFailureRollback(CompletableFuture totallyCompleteFuture, Thro
totallyCompleteFuture.complete(new DeploymentResult(DeploymentResult.DeploymentStatus.FAILED_UNABLE_TO_ROLLBACK,
deploymentFailureCause));
}

}
48 changes: 48 additions & 0 deletions src/main/java/com/aws/greengrass/lifecyclemanager/Kernel.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,13 @@
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
Expand Down Expand Up @@ -913,4 +916,49 @@
public List<String> getSupportedCapabilities() {
return SUPPORTED_CAPABILITIES;
}

/**
* Finds all auto startable services with auto startable dependencies.
* This method performs a breadth-first search, starting from the target services and traversing through
* all hard dependencies and exclude non auto startable services from.
*
* @return a set of all services that only contains auto startable services and their dependencies are all
* auto startable services
*/
public Set<GreengrassService> findAutoStartableServicesToTrack() {
// Find all non auto startable services
Set<GreengrassService> nonAutoStartableServices = orderedDependencies().stream()
.filter(service -> !service.shouldAutoStart()).collect(Collectors.toSet());

Set<GreengrassService> nonAutoStartableDependers = findDependers(nonAutoStartableServices);

// Return the set which excludes all non auto startable services and their dependers
return orderedDependencies().stream()
.filter(service -> !nonAutoStartableDependers.contains(service))
.collect(Collectors.toSet());
}

/**
* Finds all services which are dependers of initial services, directly or indirectly
* This method performs a breadth-first search, starting from the initial services and traversing through
* all hard dependencies.
* @param initialServices the set of services that we want to find dependers
* @return a set of all services that depend on the target services, including the initial services
*/
@SuppressFBWarnings("RV_RETURN_VALUE_IGNORED_BAD_PRACTICE")
public Set<GreengrassService> findDependers(Set<GreengrassService> initialServices) {
Queue<GreengrassService> dependers = new LinkedList<>(initialServices);

// Breadth-first search to find all dependent services, staring from non auto startable services
while (!dependers.isEmpty()) {
GreengrassService currentService = dependers.poll();
for (GreengrassService depender : currentService.getHardDependers()) {
// Ensure dependers haven't been processed
if (initialServices.add(depender)) {
dependers.offer(depender);

Check notice

Code scanning / CodeQL

Ignored error status of call Note

Method findDependers ignores exceptional return value of Queue.offer.
}
}
}
return initialServices;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -600,19 +600,19 @@ GreengrassService getMain() {
* @return true if all services in terminal states
*/
public boolean allServicesInTerminalState() {
List<GreengrassService> servicesToTrack = kernel.orderedDependencies().stream()
.filter(GreengrassService::shouldAutoStart).collect(Collectors.toList());
List<GreengrassService> servicesToTrack = kernel.findAutoStartableServicesToTrack()
.stream().collect(Collectors.toList());
return servicesToTrack.stream().allMatch(service -> {
State state = service.getState();
// service is broken
if (State.BROKEN.equals(state)) {
return true;
}
// or service has reached desired state, and it is either running or finished
if (service.reachedDesiredState()) {
return State.RUNNING.equals(state) || State.FINISHED.equals(state);
}
return false;
});
State state = service.getState();
// service is broken
if (State.BROKEN.equals(state)) {
return true;
}
// or service has reached desired state, and it is either running or finished
if (service.reachedDesiredState()) {
return State.RUNNING.equals(state) || State.FINISHED.equals(state);
}
return false;
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.stream.Collectors;

import static com.aws.greengrass.dependency.State.BROKEN;
import static com.aws.greengrass.dependency.State.FINISHED;
Expand Down Expand Up @@ -99,7 +100,7 @@ void beforeEach() throws Exception {
lenient().doReturn("A").when(greengrassService).getName();
lenient().doReturn(mainService).when(kernel).getMain();
lenient().doReturn(true).when(greengrassService).shouldAutoStart();
lenient().doReturn(Arrays.asList(greengrassService)).when(kernel).orderedDependencies();
lenient().doReturn(Arrays.asList(greengrassService).stream().collect(Collectors.toSet())).when(kernel).findAutoStartableServicesToTrack();
lenient().doNothing().when(componentManager).cleanupStaleVersions();
lenient().doReturn(nucleusPaths).when(kernel).getNucleusPaths();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,10 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static com.amazon.aws.iot.greengrass.component.common.SerializerFactory.getRecipeSerializer;
import static com.aws.greengrass.componentmanager.KernelConfigResolver.VERSION_CONFIG_KEY;
Expand All @@ -80,6 +82,7 @@
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
Expand Down Expand Up @@ -112,7 +115,14 @@ class KernelTest {
private Path mockFile;
private Path tempFile;
DeviceConfiguration deviceConfiguration;

@Mock
private GreengrassService service1;
@Mock
private GreengrassService service2;
@Mock
private GreengrassService service3;
@Mock
private GreengrassService service4;
@BeforeEach
void beforeEach() throws Exception{
System.setProperty("root", tempRootDir.toAbsolutePath().toString());
Expand Down Expand Up @@ -788,7 +798,94 @@ void GIVEN_unpack_dir_is_nucleus_root_WHEN_initializeComponentStore_THEN_copy_to
mockNucleusUnpackDir.assertDirectoryEquals(nucleusPaths.unarchiveArtifactPath(
componentIdentifier, DEFAULT_NUCLEUS_COMPONENT_NAME.toLowerCase()));
}
@Test
void GIVEN_all_services_autoStartable_THEN_all_services_in_services_to_track() {
kernel = spy(kernel);
// Arrange
when(service1.shouldAutoStart()).thenReturn(true);
when(service2.shouldAutoStart()).thenReturn(true);
Set<GreengrassService> services = new HashSet<>(Arrays.asList(service1, service2));

when(kernel.orderedDependencies()).thenReturn(Arrays.asList(service1, service2));

// Act
Set<GreengrassService> result = kernel.findAutoStartableServicesToTrack();

// Assert
assertEquals(services, result);
assertEquals(2, result.size());
}

@Test
void GIVEN_no_autoStartableServices_THEN_services_to_track_list_empty() {
kernel = spy(kernel);
// Arrange
when(service1.shouldAutoStart()).thenReturn(false);
when(service2.shouldAutoStart()).thenReturn(false);
Set<GreengrassService> services = new HashSet<>(Arrays.asList(service1, service2));

when(kernel.orderedDependencies()).thenReturn(services);
when(service1.getHardDependers()).thenReturn(Collections.emptyList());
when(service2.getHardDependers()).thenReturn(Collections.emptyList());

// Act
Set<GreengrassService> result = kernel.findAutoStartableServicesToTrack();

// Assert
assertTrue(result.isEmpty());
}

@Test
void GIVEN_mixed_autoStartableServices_THEN_services_to_track_only_contain_autoStartableServices() {
kernel = spy(kernel);
// Arrange
when(service1.shouldAutoStart()).thenReturn(true);
when(service2.shouldAutoStart()).thenReturn(false);
when(service3.shouldAutoStart()).thenReturn(true);
when(service4.shouldAutoStart()).thenReturn(true);

Set<GreengrassService> services = new HashSet<>(Arrays.asList(service1, service2, service3, service4));

// service3 depends on service2 (non-auto-startable)
when(service2.getHardDependers()).thenReturn(Arrays.asList(service3));
when(service3.getHardDependers()).thenReturn(Collections.emptyList());

when(kernel.orderedDependencies()).thenReturn(services);

// Act
Set<GreengrassService> result = kernel.findAutoStartableServicesToTrack();

// Assert
assertEquals(2, result.size());
assertTrue(result.contains(service1));
assertTrue(result.contains(service4));
assertFalse(result.contains(service2));
assertFalse(result.contains(service3));
}

@Test
void GIVEN_chained_services_THEN_services_to_track_only_contain_autoStartableServices() {
kernel = spy(kernel);
// Arrange
when(service1.shouldAutoStart()).thenReturn(false);
when(service2.shouldAutoStart()).thenReturn(true);
when(service3.shouldAutoStart()).thenReturn(true);

Set<GreengrassService> services = new HashSet<>(Arrays.asList(service1, service2, service3));

// service2 depends on service1, service3 depends on service2
when(service1.getHardDependers()).thenReturn(Arrays.asList(service2));
when(service2.getHardDependers()).thenReturn(Arrays.asList(service3));
when(service3.getHardDependers()).thenReturn(Collections.emptyList());

when(kernel.orderedDependencies()).thenReturn(services);

// Act
Set<GreengrassService> result = kernel.findAutoStartableServicesToTrack();

// Assert
assertTrue(result.isEmpty());
}
static class TestClass extends GreengrassService {
public TestClass(Topics topics) {
super(topics);
Expand Down
Loading