Skip to content

Commit 9120f92

Browse files
authored
Merge branch 'main' into 2024-12-06_main_add-time-sync
2 parents 3dbff9f + 51276bd commit 9120f92

File tree

10 files changed

+259
-26
lines changed

10 files changed

+259
-26
lines changed

pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@
238238
When updating the version here, ensure you match the correct aws-crt version below.
239239
Get the correct version from: https://github.com/aws/aws-iot-device-sdk-java-v2/blob/main/sdk/pom.xml#L45
240240
!-->
241-
<version>1.21.0</version>
241+
<version>1.23.0</version>
242242
<exclusions>
243243
<exclusion>
244244
<groupId>software.amazon.awssdk.crt</groupId>
@@ -249,7 +249,7 @@
249249
<dependency>
250250
<groupId>software.amazon.awssdk.crt</groupId>
251251
<artifactId>aws-crt</artifactId>
252-
<version>0.30.8</version>
252+
<version>0.33.5</version>
253253
<classifier>fips-where-available</classifier>
254254
</dependency>
255255
<dependency>

src/main/java/com/aws/greengrass/builtin/services/pubsub/SubscriptionTrie.java

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@
55

66
package com.aws.greengrass.builtin.services.pubsub;
77

8+
import com.aws.greengrass.util.Utils;
9+
810
import java.util.Collections;
911
import java.util.HashSet;
1012
import java.util.Map;
1113
import java.util.Set;
1214
import java.util.concurrent.ConcurrentHashMap;
15+
import java.util.concurrent.atomic.AtomicBoolean;
1316

1417
/**
1518
* Trie to manage subscriptions.
@@ -69,11 +72,43 @@ public boolean remove(String topic, K cb) {
6972
* @return if changed after removal
7073
*/
7174
public boolean remove(String topic, Set<K> cbs) {
72-
SubscriptionTrie<K> sub = lookup(topic);
73-
if (sub == null) {
75+
if (Utils.isEmpty(topic) || Utils.isEmpty(cbs)) {
7476
return false;
7577
}
76-
return sub.subscriptionCallbacks.removeAll(cbs);
78+
79+
AtomicBoolean isSubscriptionRemoved = new AtomicBoolean(false);
80+
removeRecursively(topic.split(TOPIC_LEVEL_SEPARATOR), this, cbs, 0, isSubscriptionRemoved);
81+
return isSubscriptionRemoved.get();
82+
}
83+
84+
private boolean canRemove(SubscriptionTrie<K> node) {
85+
// returns false if the topic level is prefix of another topic or has callbacks registered
86+
return node.children.isEmpty() && node.subscriptionCallbacks.isEmpty();
87+
}
88+
89+
/*
90+
This method removes the requested callback from a topic and prunes the subscription trie recursively by
91+
1. navigating to the last node of the requested topic
92+
1.a. at this node, the method persists the result of the requested callback removal for the penultimate result
93+
1.b. returns true if requested callback was removed and if current topic node can be pruned
94+
95+
2. the previous topic node receives the result from 1.b.
96+
2.a. if true, remove child topic node and return if current node can be pruned
97+
2.b. if false, simply do nothing and return false implying current node cannot be pruned
98+
*/
99+
private boolean removeRecursively(String[] topicNodes, SubscriptionTrie<K> topicNode, Set<K> cbs, int index,
100+
AtomicBoolean subscriptionRemoved) {
101+
if (index == topicNodes.length) {
102+
subscriptionRemoved.set(topicNode.subscriptionCallbacks.removeAll(cbs));
103+
return subscriptionRemoved.get() && canRemove(topicNode);
104+
}
105+
106+
if (removeRecursively(topicNodes, topicNode.children.get(topicNodes[index]), cbs, index + 1,
107+
subscriptionRemoved)) {
108+
topicNode.children.remove(topicNodes[index]);
109+
return canRemove(topicNode);
110+
}
111+
return false;
77112
}
78113

79114
/**
@@ -183,3 +218,4 @@ public static boolean isWildcard(String topic) {
183218
}
184219

185220
}
221+

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
import java.util.HashSet;
6262
import java.util.List;
6363
import java.util.Map;
64+
import java.util.Objects;
6465
import java.util.Optional;
6566
import java.util.Set;
6667
import java.util.concurrent.ExecutorService;
@@ -71,6 +72,7 @@
7172

7273
import static com.aws.greengrass.componentmanager.KernelConfigResolver.PREV_VERSION_CONFIG_KEY;
7374
import static com.aws.greengrass.componentmanager.KernelConfigResolver.VERSION_CONFIG_KEY;
75+
import static com.aws.greengrass.deployment.DeviceConfiguration.DEFAULT_NUCLEUS_COMPONENT_NAME;
7476
import static com.aws.greengrass.deployment.converter.DeploymentDocumentConverter.ANY_VERSION;
7577
import static org.apache.commons.io.FileUtils.ONE_MB;
7678

@@ -296,6 +298,37 @@ private void storeRecipeDigestInConfigStoreForPlugin(
296298
}
297299
}
298300

301+
/**
302+
* Un-archives the artifacts for the current Nucleus version package.
303+
*
304+
* @return list of un-archived paths
305+
* @throws PackageLoadingException when unable to load current Nucleus
306+
*/
307+
public List<Path> unArchiveCurrentNucleusVersionArtifacts() throws PackageLoadingException {
308+
String currentNucleusVersion = deviceConfiguration.getNucleusVersion();
309+
ComponentIdentifier nucleusComponentIdentifier =
310+
new ComponentIdentifier(DEFAULT_NUCLEUS_COMPONENT_NAME, new Semver(currentNucleusVersion));
311+
List<File> nucleusArtifactFileNames =
312+
componentStore.getArtifactFiles(nucleusComponentIdentifier, artifactDownloaderFactory);
313+
return nucleusArtifactFileNames.stream()
314+
.map(file -> {
315+
try {
316+
Path unarchivePath =
317+
nucleusPaths.unarchiveArtifactPath(nucleusComponentIdentifier, getFileName(file));
318+
/*
319+
Using a hard-coded ZIP un-archiver as today this code path is only used to un-archive a Nucleus
320+
.zip artifact.
321+
*/
322+
unarchiver.unarchive(Unarchive.ZIP, file, unarchivePath);
323+
return unarchivePath;
324+
} catch (IOException e) {
325+
logger.atDebug().setCause(e).kv("comp-id", nucleusComponentIdentifier)
326+
.log("Could not un-archive Nucleus artifact");
327+
return null;
328+
}
329+
}).filter(Objects::nonNull).collect(Collectors.toList());
330+
}
331+
299332
private Optional<ComponentIdentifier> findBestCandidateLocally(String componentName,
300333
Map<String, Requirement> versionRequirements)
301334
throws PackagingException {

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,10 @@
4747
import java.util.HashSet;
4848
import java.util.List;
4949
import java.util.Map;
50+
import java.util.Objects;
5051
import java.util.Optional;
5152
import java.util.Set;
53+
import java.util.stream.Collectors;
5254
import java.util.stream.LongStream;
5355
import javax.inject.Inject;
5456

@@ -412,6 +414,37 @@ public Path resolveArtifactDirectoryPath(@NonNull ComponentIdentifier componentI
412414
}
413415
}
414416

417+
/**
418+
* Returns the artifact file name.
419+
*
420+
* @param componentIdentifier packageIdentifier
421+
* @param artifactDownloaderFactory artifact downloader factory
422+
* @return the unarchive artifact directory path for target package.
423+
* @throws PackageLoadingException if creating the directory fails
424+
*/
425+
public List<File> getArtifactFiles(@NonNull ComponentIdentifier componentIdentifier,
426+
@NonNull ArtifactDownloaderFactory artifactDownloaderFactory)
427+
throws PackageLoadingException {
428+
Optional<String> componentRecipeContent = findComponentRecipeContent(componentIdentifier);
429+
if (!componentRecipeContent.isPresent()) {
430+
return Collections.emptyList();
431+
}
432+
433+
ComponentRecipe recipe = getPackageRecipe(componentIdentifier);
434+
Path packageArtifactDirectory = resolveArtifactDirectoryPath(componentIdentifier);
435+
return recipe.getArtifacts().stream().map(artifact -> {
436+
try {
437+
return artifactDownloaderFactory
438+
.getArtifactDownloader(componentIdentifier, artifact, packageArtifactDirectory)
439+
.getArtifactFile();
440+
} catch (PackageLoadingException | InvalidArtifactUriException e) {
441+
logger.atDebug().setCause(e).kv("comp-id", componentRecipeContent)
442+
.log("Could not get artifact file");
443+
return null;
444+
}
445+
}).filter(Objects::nonNull).collect(Collectors.toList());
446+
}
447+
415448
/**
416449
* Resolve the recipe file path for a target package id.
417450
*

src/main/java/com/aws/greengrass/deployment/errorcode/DeploymentErrorCode.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public enum DeploymentErrorCode {
6464
// JVM hashing issue
6565
HASHING_ALGORITHM_UNAVAILABLE(DeploymentErrorType.DEVICE_ERROR),
6666
// Could be a local file issue or a Nucleus issue; we will categorize as the latter for visibility
67-
LAUNCH_DIRECTORY_CORRUPTED(DeploymentErrorType.NUCLEUS_ERROR),
67+
LAUNCH_DIRECTORY_CORRUPTED(DeploymentErrorType.DEVICE_ERROR),
6868

6969
/* Component recipe errors */
7070
RECIPE_PARSE_ERROR(DeploymentErrorType.COMPONENT_RECIPE_ERROR),

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

Lines changed: 61 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
package com.aws.greengrass.lifecyclemanager;
77

8+
import com.aws.greengrass.componentmanager.ComponentManager;
9+
import com.aws.greengrass.componentmanager.exceptions.PackageLoadingException;
810
import com.aws.greengrass.config.Configuration;
911
import com.aws.greengrass.config.Topics;
1012
import com.aws.greengrass.dependency.Context;
@@ -35,6 +37,8 @@
3537
import java.nio.file.Path;
3638
import java.util.Collections;
3739
import java.util.HashSet;
40+
import java.util.List;
41+
import java.util.Optional;
3842
import java.util.Set;
3943
import javax.inject.Inject;
4044

@@ -70,15 +74,18 @@ public class KernelAlternatives {
7074
private static final String BOOTSTRAP_ON_ROLLBACK_CONFIG_KEY = "bootstrapOnRollback";
7175

7276
private final NucleusPaths nucleusPaths;
77+
private final ComponentManager componentManager;
7378

7479
/**
7580
* Constructor for KernelAlternatives, which manages the alternative launch directory of Kernel.
7681
*
7782
* @param nucleusPaths nucleus paths
83+
* @param componentManager component manager
7884
*/
7985
@Inject
80-
public KernelAlternatives(NucleusPaths nucleusPaths) {
86+
public KernelAlternatives(NucleusPaths nucleusPaths, ComponentManager componentManager) {
8187
this.nucleusPaths = nucleusPaths;
88+
this.componentManager = componentManager;
8289
try {
8390
setupInitLaunchDirIfAbsent();
8491
} catch (IOException e) {
@@ -162,31 +169,69 @@ public boolean isLaunchDirSetup() {
162169
return Files.isSymbolicLink(getCurrentDir()) && validateLaunchDirSetup(getCurrentDir());
163170
}
164171

172+
protected boolean canRecoverMissingLaunchDirSetup()
173+
throws IOException, URISyntaxException, PackageLoadingException {
174+
/*
175+
Try and relink launch dir with the following replacement criteria
176+
1. check if current Nucleus execution package is valid
177+
2. un-archive current Nucleus version from component store
178+
3. fail with DirectoryValidationException if above steps do not satisfy
179+
*/
180+
Path currentNucleusExecutablePath = locateCurrentKernelUnpackDir();
181+
if (Files.exists(currentNucleusExecutablePath.resolve(KERNEL_BIN_DIR)
182+
.resolve(Platform.getInstance().loaderFilename()))) {
183+
logger.atDebug().kv("path", currentNucleusExecutablePath)
184+
.log("Current Nucleus executable is valid, setting up launch dir");
185+
relinkInitLaunchDir(currentNucleusExecutablePath, true);
186+
return true;
187+
}
188+
189+
List<Path> localNucleusExecutablePaths = componentManager.unArchiveCurrentNucleusVersionArtifacts();
190+
if (!localNucleusExecutablePaths.isEmpty()) {
191+
Optional<Path> validNucleusExecutablePath = localNucleusExecutablePaths.stream()
192+
.filter(path -> Files.exists(path.resolve(KERNEL_BIN_DIR)
193+
.resolve(Platform.getInstance().loaderFilename())))
194+
.findFirst();
195+
if (validNucleusExecutablePath.isPresent()) {
196+
logger.atDebug().kv("path", validNucleusExecutablePath.get())
197+
.log("Un-archived current Nucleus artifact");
198+
relinkInitLaunchDir(validNucleusExecutablePath.get(), true);
199+
return true;
200+
}
201+
}
202+
throw new PackageLoadingException("Could not find a valid Nucleus package to recover launch dir setup");
203+
}
204+
165205
/**
166206
* Validate that launch directory is set up.
167207
*
168208
* @throws DirectoryValidationException when a file is missing
169209
* @throws DeploymentException when user is not allowed to change file permission
170210
*/
171211
public void validateLaunchDirSetupVerbose() throws DirectoryValidationException, DeploymentException {
172-
Path currentDir = getCurrentDir();
173-
if (!Files.isSymbolicLink(currentDir)) {
174-
throw new DirectoryValidationException("Missing symlink to current nucleus launch directory");
212+
try {
213+
if (!Files.isSymbolicLink(getCurrentDir()) || !Files.exists(getLoaderPathFromLaunchDir(getCurrentDir()))) {
214+
logger.atInfo().log("Current launch dir setup is missing, attempting to recover");
215+
canRecoverMissingLaunchDirSetup();
216+
}
217+
} catch (PackageLoadingException | IOException ex) {
218+
throw new DirectoryValidationException("Unable to relink init launch directory", ex);
219+
} catch (URISyntaxException ex) {
220+
// TODO: Fix usage of root path with spaces on linux
221+
throw new DeploymentException("Could not parse init launch directory path", ex);
175222
}
223+
224+
Path currentDir = getCurrentDir();
176225
Path loaderPath = getLoaderPathFromLaunchDir(currentDir);
177-
if (Files.exists(loaderPath)) {
178-
if (!loaderPath.toFile().canExecute()) {
179-
// Ensure that the loader is executable so that we can exec it when restarting Nucleus
180-
try {
181-
Platform.getInstance().setPermissions(OWNER_RWX_EVERYONE_RX, loaderPath);
182-
} catch (IOException e) {
183-
throw new DeploymentException(
184-
String.format("Unable to set loader script at %s as executable", loaderPath), e)
185-
.withErrorContext(e, DeploymentErrorCode.SET_PERMISSION_ERROR);
186-
}
226+
if (!loaderPath.toFile().canExecute()) {
227+
// Ensure that the loader is executable so that we can exec it when restarting Nucleus
228+
try {
229+
Platform.getInstance().setPermissions(OWNER_RWX_EVERYONE_RX, loaderPath);
230+
} catch (IOException e) {
231+
throw new DeploymentException(
232+
String.format("Unable to set loader script at %s as executable", loaderPath), e)
233+
.withErrorContext(e, DeploymentErrorCode.SET_PERMISSION_ERROR);
187234
}
188-
} else {
189-
throw new DirectoryValidationException("Missing loader file at " + currentDir.toAbsolutePath());
190235
}
191236
}
192237

src/main/java/com/aws/greengrass/lifecyclemanager/exceptions/DirectoryValidationException.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,9 @@ public DirectoryValidationException(String message) {
1515
super(message);
1616
super.addErrorCode(DeploymentErrorCode.LAUNCH_DIRECTORY_CORRUPTED);
1717
}
18+
19+
public DirectoryValidationException(String message, Throwable throwable) {
20+
super(message, throwable);
21+
super.addErrorCode(DeploymentErrorCode.LAUNCH_DIRECTORY_CORRUPTED);
22+
}
1823
}

src/test/java/com/aws/greengrass/builtin/services/pubsub/SubscriptionTrieTest.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,51 @@ public void GIVEN_subscription_wildcard_WHEN_remove_topic_THEN_no_matches() {
137137
assertEquals(0, trie.size());
138138
}
139139

140+
@Test
141+
public void GIVEN_subscriptions_with_wildcards_WHEN_remove_topics_THEN_clean_up_trie() {
142+
assertEquals(0, trie.size());
143+
SubscriptionCallback cb1 = generateSubscriptionCallback();
144+
SubscriptionCallback cb2 = generateSubscriptionCallback();
145+
SubscriptionCallback cb3 = generateSubscriptionCallback();
146+
String topic = "foo/+/bar/#";
147+
trie.add("bar", cb1);
148+
trie.add(topic, cb1);
149+
trie.add(topic, cb2);
150+
// Topic is not registered with the callback, mark it as removed when requested to remove
151+
assertThat("remove topic", trie.remove(topic, cb3), is(false));
152+
assertThat(trie.get(topic), containsInAnyOrder(cb1, cb2));
153+
154+
trie.add("foo/#", cb3);
155+
trie.add("foo/+", cb2);
156+
157+
assertThat(trie.get(topic), containsInAnyOrder(cb1, cb2, cb3));
158+
assertEquals(5, trie.size());
159+
160+
assertThat("remove topic", trie.remove("foo/+", cb2), is(true));
161+
assertEquals(4, trie.size());
162+
assertThat(trie.get("foo/+"), containsInAnyOrder(cb3)); // foo/+ still matches with foo/#
163+
assertThat(trie.get(topic), containsInAnyOrder(cb1, cb2, cb3)); // foo/+/bar/# still exists
164+
165+
assertThat("remove topic", trie.remove("foo/#", cb3), is(true));
166+
assertFalse(trie.containsKey("foo/#"));
167+
assertThat(trie.get("foo/#"), is(empty()));
168+
assertThat(trie.get("foo/+"), is(empty())); // foo/+ doesn't match with any existing topic
169+
assertThat(trie.get(topic), containsInAnyOrder(cb1, cb2)); // foo/+/bar/# still exists
170+
assertEquals(3, trie.size());
171+
172+
assertThat("remove topic", trie.remove(topic, cb1), is(true));
173+
assertThat(trie.get(topic), contains(cb2));
174+
assertEquals(2, trie.size());
175+
assertTrue(trie.containsKey("foo/+"));
176+
assertTrue(trie.containsKey("foo/+/bar/#"));
177+
178+
assertThat("remove topic", trie.remove(topic, cb2), is(true));
179+
assertThat(trie.get(topic), is(empty()));
180+
assertEquals(1, trie.size());
181+
assertFalse(trie.containsKey("foo/+"));
182+
assertFalse(trie.containsKey("foo/+/bar/#"));
183+
}
184+
140185
@Test
141186
void GIVEN_topics_WHEN_isWildcard_THEN_returns_whether_it_uses_wildcard() {
142187
assertTrue(SubscriptionTrie.isWildcard("+"));

src/test/java/com/aws/greengrass/deployment/activator/KernelUpdateActivatorTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ void GIVEN_launch_dir_corrupted_WHEN_deployment_activate_THEN_deployment_fail(Ex
227227
assertEquals(mockException, result.getFailureCause().getCause());
228228

229229
List<String> expectedStack = Arrays.asList("DEPLOYMENT_FAILURE", "LAUNCH_DIRECTORY_CORRUPTED");
230-
List<String> expectedTypes = Collections.singletonList("NUCLEUS_ERROR");
230+
List<String> expectedTypes = Collections.singletonList("DEVICE_ERROR");
231231
TestUtils.validateGenerateErrorReport(result.getFailureCause(), expectedStack, expectedTypes);
232232
}
233233

0 commit comments

Comments
 (0)