Skip to content

Commit 78fdfac

Browse files
committed
fix: clean up deployment artifacts
1 parent 8461ad2 commit 78fdfac

File tree

6 files changed

+426
-90
lines changed

6 files changed

+426
-90
lines changed

src/main/java/com/aws/greengrass/componentmanager/ComponentManager.java

Lines changed: 166 additions & 69 deletions
Large diffs are not rendered by default.

src/main/java/com/aws/greengrass/deployment/DefaultDeploymentTask.java

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ public DeploymentResult call() throws InterruptedException {
112112
Future<List<ComponentIdentifier>> resolveDependenciesFuture = null;
113113
Future<Void> preparePackagesFuture = null;
114114
Future<DeploymentResult> deploymentMergeFuture = null;
115+
DeploymentResult deploymentResult = null;
116+
List<ComponentIdentifier> desiredPackages = null;
115117
DeploymentDocument deploymentDocument = deployment.getDeploymentDocumentObj();
116118
try {
117119
logger.atInfo().setEventType(DEPLOYMENT_TASK_EVENT_TYPE)
@@ -131,7 +133,7 @@ public DeploymentResult call() throws InterruptedException {
131133
resolveDependenciesFuture = executorService.submit(() ->
132134
dependencyResolver.resolveDependencies(deploymentDocument, nonTargetGroupsToRootPackagesMap));
133135

134-
List<ComponentIdentifier> desiredPackages = resolveDependenciesFuture.get();
136+
desiredPackages = resolveDependenciesFuture.get();
135137

136138
// download configuration if large
137139
List<String> requiredCapabilities = deploymentDocument.getRequiredCapabilities();
@@ -163,23 +165,18 @@ public DeploymentResult call() throws InterruptedException {
163165
// on the device before the deployment started, and finding one of type nucleus.
164166
Optional<DeploymentPackageConfiguration> incomingNucleusComponentConfiguration =
165167
deploymentDocument.getDeploymentPackageConfigurationList() == null ? Optional.empty() :
166-
deploymentDocument.getDeploymentPackageConfigurationList().stream()
167-
.filter(c -> c.getPackageName().equals(deviceConfiguration.getNucleusComponentName()))
168-
.findAny();
168+
deploymentDocument.getDeploymentPackageConfigurationList().stream()
169+
.filter(c -> c.getPackageName()
170+
.equals(deviceConfiguration.getNucleusComponentName()))
171+
.findAny();
169172

170173
if (incomingNucleusComponentConfiguration.isPresent()
171-
&& incomingNucleusComponentConfiguration
172-
.get()
173-
.getConfigurationUpdateOperation() != null
174-
&& incomingNucleusComponentConfiguration
175-
.get()
176-
.getConfigurationUpdateOperation()
177-
.getValueToMerge() != null
178-
&& incomingNucleusComponentConfiguration
179-
.get()
180-
.getConfigurationUpdateOperation()
181-
.getValueToMerge()
182-
.containsKey(DEVICE_PARAM_DEPLOYMENT_CONFIGURATION_TIME_SOURCE)) {
174+
&& incomingNucleusComponentConfiguration.get().getConfigurationUpdateOperation() != null
175+
&& incomingNucleusComponentConfiguration.get().getConfigurationUpdateOperation()
176+
.getValueToMerge() != null
177+
&& incomingNucleusComponentConfiguration.get().getConfigurationUpdateOperation()
178+
.getValueToMerge()
179+
.containsKey(DEVICE_PARAM_DEPLOYMENT_CONFIGURATION_TIME_SOURCE)) {
183180
logger.atDebug(DEPLOYMENT_TASK_EVENT_TYPE).log(
184181
"Incoming nucleus component configuration contains deployment configuration time source");
185182
String incomingDeploymentConfigurationTimeSource = Coerce.toString(
@@ -223,28 +220,41 @@ public DeploymentResult call() throws InterruptedException {
223220
// Block this without timeout because it can take a long time for the device to update the config
224221
// (if it's not in a safe window).
225222
DeploymentResult result = deploymentMergeFuture.get();
223+
deploymentResult = result;
226224

227225
logger.atInfo(DEPLOYMENT_TASK_EVENT_TYPE).setEventType(DEPLOYMENT_TASK_EVENT_TYPE)
228226
.log("Finished deployment task");
229227

230-
componentManager.cleanupStaleVersions();
231228
return result;
232229
} catch (PackageLoadingException | DeploymentTaskFailureException | IOException e) {
233230
logger.atError().setCause(e).log("Error occurred while processing deployment");
234-
return new DeploymentResult(DeploymentResult.DeploymentStatus.FAILED_NO_STATE_CHANGE, e);
231+
deploymentResult = new DeploymentResult(DeploymentResult.DeploymentStatus.FAILED_NO_STATE_CHANGE, e);
232+
return deploymentResult;
235233
} catch (ExecutionException e) {
236234
logger.atError().setCause(e).log("Error occurred while processing deployment");
237235
Throwable t = e.getCause();
238236
if (t instanceof InterruptedException) {
239237
throw (InterruptedException) t;
240238
} else {
241-
return new DeploymentResult(DeploymentResult.DeploymentStatus.FAILED_NO_STATE_CHANGE, t);
239+
deploymentResult = new DeploymentResult(DeploymentResult.DeploymentStatus.FAILED_NO_STATE_CHANGE, t);
240+
return deploymentResult;
242241
}
243242
} catch (InterruptedException e) {
244243
// DeploymentTask got interrupted while performing a blocking step.
245244
cancelDeploymentTask(resolveDependenciesFuture, preparePackagesFuture, deploymentMergeFuture);
246245
// Populate the exception up to the stack
247246
throw e;
247+
} finally {
248+
// Clean up stale artifacts from previous deployments after downloading deployment document
249+
// but before preparing packages, so we don't interfere with current deployment
250+
Map<String, String> failedDeploymentVersions = null;
251+
if (desiredPackages != null) {
252+
failedDeploymentVersions = desiredPackages.stream()
253+
.collect(Collectors.toMap(
254+
ComponentIdentifier::getName,
255+
ci -> ci.getVersion().getValue()));
256+
}
257+
componentManager.cleanupStaleVersions(deploymentResult, failedDeploymentVersions);
248258
}
249259
}
250260

src/main/java/com/aws/greengrass/deployment/KernelUpdateDeploymentTask.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ public DeploymentResult call() {
7979
result = deploymentResultCompletableFuture.get();
8080
} catch (InterruptedException | ExecutionException | CancellationException e) {
8181
// nothing to report when deployment is cancelled
82+
componentManager.cleanupStaleVersions();
8283
return null;
8384
}
8485
componentManager.cleanupStaleVersions();

src/main/java/com/aws/greengrass/lifecyclemanager/UpdateSystemPolicyService.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,12 +209,14 @@ private long getTimeToReCheck(long timeout, String deploymentId,
209209
try {
210210
DeferComponentUpdateRequest deferRequest = fut.get();
211211
if (deploymentId.equals(deferRequest.getDeploymentId())) {
212-
long timeToRecheck = currentTimeMillis + deferRequest.getRecheckAfterMs();
212+
Long recheckAfterMs = deferRequest.getRecheckAfterMs();
213+
long recheckValue = recheckAfterMs == null ? 0 : recheckAfterMs;
214+
long timeToRecheck = currentTimeMillis + recheckValue;
213215
if (timeToRecheck > maxTimeToReCheck) {
214216
maxTimeToReCheck = timeToRecheck;
215217
logger.atInfo().setEventType("service-update-deferred")
216218
.log("deferred for {} millis with message {}",
217-
deferRequest.getRecheckAfterMs(),
219+
recheckValue,
218220
deferRequest.getMessage());
219221
}
220222
} else {
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package com.aws.greengrass.componentmanager;
7+
8+
import com.aws.greengrass.config.Topic;
9+
import com.aws.greengrass.config.Topics;
10+
import com.aws.greengrass.deployment.model.DeploymentResult;
11+
import com.aws.greengrass.lifecyclemanager.GreengrassService;
12+
import com.aws.greengrass.lifecyclemanager.Kernel;
13+
import org.junit.jupiter.api.BeforeEach;
14+
import org.junit.jupiter.api.Test;
15+
import org.junit.jupiter.api.extension.ExtendWith;
16+
import org.mockito.Mock;
17+
import org.mockito.junit.jupiter.MockitoExtension;
18+
19+
import java.util.Arrays;
20+
import java.util.Collections;
21+
import java.util.HashMap;
22+
import java.util.HashSet;
23+
import java.util.Map;
24+
import java.util.Set;
25+
26+
import static com.aws.greengrass.componentmanager.KernelConfigResolver.PREV_VERSION_CONFIG_KEY;
27+
import static com.aws.greengrass.componentmanager.KernelConfigResolver.VERSION_CONFIG_KEY;
28+
import static org.junit.jupiter.api.Assertions.assertEquals;
29+
import static org.mockito.Mockito.mock;
30+
import static org.mockito.Mockito.when;
31+
32+
@ExtendWith(MockitoExtension.class)
33+
class ComponentManagerCleanupTest {
34+
35+
@Mock
36+
private Kernel kernel;
37+
@Mock
38+
private ComponentStore componentStore;
39+
40+
private ComponentManager componentManager;
41+
42+
@BeforeEach
43+
void setup() {
44+
componentManager = new ComponentManager(null, null, null, componentStore, kernel, null, null, null);
45+
}
46+
47+
@Test
48+
void GIVEN_successful_deployment_WHEN_getVersionsToKeep_THEN_keeps_current_and_previous() {
49+
GreengrassService service = createServiceWithBothVersions("TestComponent", "2.0.0", "1.0.0");
50+
when(kernel.orderedDependencies()).thenReturn(Collections.singletonList(service));
51+
52+
DeploymentResult result = new DeploymentResult(DeploymentResult.DeploymentStatus.SUCCESSFUL, null);
53+
Map<String, Set<String>> versions = componentManager.getVersionsToKeep(result, null);
54+
55+
assertEquals(new HashSet<>(Arrays.asList("2.0.0", "1.0.0")), versions.get("TestComponent"));
56+
}
57+
58+
@Test
59+
void GIVEN_failed_deployment_WHEN_getVersionsToKeep_THEN_keeps_three_versions() {
60+
GreengrassService service = createServiceWithBothVersions("TestComponent", "3.0.0", "2.0.0");
61+
when(kernel.orderedDependencies()).thenReturn(Collections.singletonList(service));
62+
63+
// Mock available versions in component store
64+
Map<String, Set<String>> availableVersions = new HashMap<>();
65+
availableVersions.put("TestComponent", new HashSet<>(Arrays.asList("1.0.0", "2.0.0", "3.0.0")));
66+
when(componentStore.listAvailableComponentVersions()).thenReturn(availableVersions);
67+
68+
DeploymentResult result = new DeploymentResult(DeploymentResult.DeploymentStatus.FAILED_NO_STATE_CHANGE,
69+
new RuntimeException());
70+
Map<String, Set<String>> versions = componentManager.getVersionsToKeep(result, null);
71+
72+
// Should keep current (3.0.0 failed) + previous (2.0.0) + one more (1.0.0) = 3 total
73+
assertEquals(new HashSet<>(Arrays.asList("3.0.0", "2.0.0", "1.0.0")), versions.get("TestComponent"));
74+
}
75+
76+
@Test
77+
void GIVEN_null_deployment_WHEN_getVersionsToKeep_THEN_keeps_current_and_previous() {
78+
GreengrassService service = createServiceWithBothVersions("TestComponent", "2.0.0", "1.0.0");
79+
when(kernel.orderedDependencies()).thenReturn(Collections.singletonList(service));
80+
81+
Map<String, Set<String>> versions = componentManager.getVersionsToKeep(null, null);
82+
83+
assertEquals(new HashSet<>(Arrays.asList("2.0.0", "1.0.0")), versions.get("TestComponent"));
84+
}
85+
86+
@Test
87+
void GIVEN_failed_deployment_with_version_info_WHEN_getVersionsToKeep_THEN_includes_failed_version() {
88+
GreengrassService service = createServiceWithBothVersions("TestComponent", "1.0.0", "0.9.0");
89+
when(kernel.orderedDependencies()).thenReturn(Arrays.asList(service));
90+
91+
DeploymentResult failedResult = new DeploymentResult(DeploymentResult.DeploymentStatus.FAILED_NO_STATE_CHANGE,
92+
new RuntimeException("Test failure"));
93+
Map<String, String> failedVersions = Collections.singletonMap("TestComponent", "2.0.0");
94+
95+
Map<String, Set<String>> versions = componentManager.getVersionsToKeep(failedResult, failedVersions);
96+
97+
assertEquals(new HashSet<>(Arrays.asList("1.0.0", "0.9.0", "2.0.0")), versions.get("TestComponent"));
98+
}
99+
100+
@Test
101+
void GIVEN_service_with_null_version_topics_WHEN_getVersionsToKeep_THEN_handles_gracefully() {
102+
GreengrassService service = createServiceWithNullVersions("TestComponent");
103+
when(kernel.orderedDependencies()).thenReturn(Collections.singletonList(service));
104+
105+
Map<String, Set<String>> versions = componentManager.getVersionsToKeep(null, null);
106+
107+
assertEquals(new HashSet<>(), versions.get("TestComponent"));
108+
}
109+
110+
@Test
111+
void GIVEN_failed_deployment_no_available_versions_WHEN_getVersionsToKeep_THEN_keeps_current_and_previous() {
112+
GreengrassService service = createServiceWithBothVersions("TestComponent", "2.0.0", "1.0.0");
113+
when(kernel.orderedDependencies()).thenReturn(Collections.singletonList(service));
114+
115+
// Mock empty available versions in component store
116+
when(componentStore.listAvailableComponentVersions()).thenReturn(new HashMap<>());
117+
118+
DeploymentResult result = new DeploymentResult(DeploymentResult.DeploymentStatus.FAILED_NO_STATE_CHANGE,
119+
new RuntimeException());
120+
Map<String, Set<String>> versions = componentManager.getVersionsToKeep(result, null);
121+
122+
// Should only keep current + previous when no additional versions available
123+
assertEquals(new HashSet<>(Arrays.asList("2.0.0", "1.0.0")), versions.get("TestComponent"));
124+
}
125+
126+
private GreengrassService createServiceWithBothVersions(String name, String currentVersion, String previousVersion) {
127+
GreengrassService service = mock(GreengrassService.class);
128+
Topics config = mock(Topics.class);
129+
Topic versionTopic = mock(Topic.class);
130+
Topic prevVersionTopic = mock(Topic.class);
131+
132+
when(service.getName()).thenReturn(name);
133+
when(service.getServiceConfig()).thenReturn(config);
134+
when(versionTopic.getOnce()).thenReturn(currentVersion);
135+
when(prevVersionTopic.getOnce()).thenReturn(previousVersion);
136+
when(config.find(VERSION_CONFIG_KEY)).thenReturn(versionTopic);
137+
when(config.find(PREV_VERSION_CONFIG_KEY)).thenReturn(prevVersionTopic);
138+
139+
return service;
140+
}
141+
142+
private GreengrassService createServiceWithNullVersions(String name) {
143+
GreengrassService service = mock(GreengrassService.class);
144+
Topics config = mock(Topics.class);
145+
146+
when(service.getName()).thenReturn(name);
147+
when(service.getServiceConfig()).thenReturn(config);
148+
when(config.find(VERSION_CONFIG_KEY)).thenReturn(null);
149+
when(config.find(PREV_VERSION_CONFIG_KEY)).thenReturn(null);
150+
151+
return service;
152+
}
153+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package com.aws.greengrass.deployment;
7+
8+
import com.aws.greengrass.componentmanager.ComponentManager;
9+
import com.aws.greengrass.componentmanager.models.ComponentIdentifier;
10+
import com.aws.greengrass.deployment.model.DeploymentResult;
11+
import com.vdurmont.semver4j.Semver;
12+
import org.junit.jupiter.api.Test;
13+
import org.junit.jupiter.api.extension.ExtendWith;
14+
import org.mockito.Mock;
15+
import org.mockito.junit.jupiter.MockitoExtension;
16+
17+
import java.util.Arrays;
18+
import java.util.Collections;
19+
import java.util.List;
20+
import java.util.Map;
21+
import java.util.stream.Collectors;
22+
23+
import static org.junit.jupiter.api.Assertions.assertEquals;
24+
import static org.mockito.ArgumentMatchers.any;
25+
import static org.mockito.ArgumentMatchers.eq;
26+
import static org.mockito.Mockito.verify;
27+
28+
@ExtendWith(MockitoExtension.class)
29+
class DefaultDeploymentTaskCleanupTest {
30+
31+
@Mock
32+
private ComponentManager componentManager;
33+
34+
@Test
35+
void GIVEN_deployment_completes_WHEN_cleanup_called_THEN_passes_deployment_result() {
36+
DeploymentResult result = new DeploymentResult(DeploymentResult.DeploymentStatus.SUCCESSFUL, null);
37+
38+
// Simulate the cleanup call that happens in DefaultDeploymentTask
39+
componentManager.cleanupStaleVersions(result, null);
40+
41+
verify(componentManager).cleanupStaleVersions(eq(result), eq(null));
42+
}
43+
44+
@Test
45+
void GIVEN_failed_deployment_WHEN_cleanup_called_THEN_passes_failed_result_with_versions() {
46+
DeploymentResult result = new DeploymentResult(DeploymentResult.DeploymentStatus.FAILED_NO_STATE_CHANGE,
47+
new RuntimeException("Test failure"));
48+
49+
// Simulate the cleanup call that happens in DefaultDeploymentTask for failed deployments
50+
Map<String, String> failedVersions = Collections.singletonMap("TestComponent", "1.0.0");
51+
componentManager.cleanupStaleVersions(result, failedVersions);
52+
53+
verify(componentManager).cleanupStaleVersions(eq(result), any(Map.class));
54+
}
55+
56+
@Test
57+
void GIVEN_desiredPackages_WHEN_extracting_versions_THEN_creates_correct_map() {
58+
// Simulate the version extraction logic from DefaultDeploymentTask
59+
List<ComponentIdentifier> desiredPackages = Arrays.asList(
60+
new ComponentIdentifier("ComponentA", new Semver("1.0.0")),
61+
new ComponentIdentifier("ComponentB", new Semver("2.1.0"))
62+
);
63+
64+
Map<String, String> failedDeploymentVersions = desiredPackages.stream()
65+
.collect(Collectors.toMap(
66+
ComponentIdentifier::getName,
67+
ci -> ci.getVersion().getValue()));
68+
69+
assertEquals("1.0.0", failedDeploymentVersions.get("ComponentA"));
70+
assertEquals("2.1.0", failedDeploymentVersions.get("ComponentB"));
71+
assertEquals(2, failedDeploymentVersions.size());
72+
}
73+
}

0 commit comments

Comments
 (0)