This is an automated email from the ASF dual-hosted git repository.
rzo1 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/storm.git
The following commit(s) were added to refs/heads/master by this push:
new 4a1c21a8b STORM-3871: remove a topology's dependency artifact blobs
when it is cleaned up (#9011)
4a1c21a8b is described below
commit 4a1c21a8bd7fbb1f419aed5fec919e2349ae3816
Author: Richard Zowalla <[email protected]>
AuthorDate: Mon Aug 24 12:57:05 2026 +0200
STORM-3871: remove a topology's dependency artifact blobs when it is
cleaned up (#9011)
* STORM-3871: remove a topology's dependency artifact blobs when it is
cleaned up
* Cover the reference scan continuing past a candidate topology with no
code blob
Review feedback on #9011: a KeyNotFoundException while collecting dependency
references means the candidate topology contributes no references, unlike a
read failure it does not abort the scan or trigger the conservative
fallback.
Co-Authored-By: Claude Fable 5 <[email protected]>
---------
Co-authored-by: Claude Fable 5 <[email protected]>
---
.../src/jvm/org/apache/storm/StormSubmitter.java | 12 +-
.../storm/dependency/DependencyUploader.java | 5 +
.../storm/dependency/DependencyUploaderTest.java | 5 +-
.../org/apache/storm/daemon/nimbus/Nimbus.java | 123 ++++++++++++++--
.../storm/daemon/nimbus/NimbusClojurePortTest.java | 4 +-
.../org/apache/storm/daemon/nimbus/NimbusTest.java | 156 +++++++++++++++++++++
6 files changed, 289 insertions(+), 16 deletions(-)
diff --git a/storm-client/src/jvm/org/apache/storm/StormSubmitter.java
b/storm-client/src/jvm/org/apache/storm/StormSubmitter.java
index 18ff3ba60..1bc5627e1 100644
--- a/storm-client/src/jvm/org/apache/storm/StormSubmitter.java
+++ b/storm-client/src/jvm/org/apache/storm/StormSubmitter.java
@@ -271,7 +271,7 @@ public class StormSubmitter {
// Dependency uploading only makes sense for distributed mode
List<String> jarsBlobKeys = Collections.emptyList();
- List<String> artifactsBlobKeys;
+ List<String> artifactsBlobKeys = Collections.emptyList();
DependencyUploader uploader = new DependencyUploader();
try {
@@ -281,8 +281,10 @@ public class StormSubmitter {
artifactsBlobKeys =
uploadDependencyArtifactsToBlobStore(uploader);
} catch (Throwable e) {
- // remove uploaded jars blobs, not artifacts since they're
shared across the cluster
+ // every uploaded blob carries a key unique to this
submission, and no topology refers to
+ // them yet, so nothing else can be using them
uploader.deleteBlobs(jarsBlobKeys);
+ uploader.deleteBlobs(artifactsBlobKeys);
uploader.shutdown();
throw e;
}
@@ -291,10 +293,12 @@ public class StormSubmitter {
setDependencyBlobsToTopology(topology, jarsBlobKeys,
artifactsBlobKeys);
submitTopologyInDistributeMode(name, topology, opts,
progressListener, asUser, conf, serConf, client);
} catch (AlreadyAliveException | InvalidTopologyException |
AuthorizationException e) {
- // remove uploaded jars blobs, not artifacts since they're
shared across the cluster
- // Note that we don't handle TException to delete jars
blobs
+ // the topology was rejected, so the blobs it refers to
are unreachable; their keys are
+ // unique to this submission, so nothing else can be using
them
+ // Note that we don't handle TException to delete the blobs
// because it's safer to leave some blobs instead of
topology not running
uploader.deleteBlobs(jarsBlobKeys);
+ uploader.deleteBlobs(artifactsBlobKeys);
throw e;
} finally {
uploader.shutdown();
diff --git
a/storm-client/src/jvm/org/apache/storm/dependency/DependencyUploader.java
b/storm-client/src/jvm/org/apache/storm/dependency/DependencyUploader.java
index de976ad3d..cf89990c1 100644
--- a/storm-client/src/jvm/org/apache/storm/dependency/DependencyUploader.java
+++ b/storm-client/src/jvm/org/apache/storm/dependency/DependencyUploader.java
@@ -131,6 +131,11 @@ public class DependencyUploader {
keys.add(key);
}
} catch (Throwable e) {
+ // the keys are unique to this upload and no topology refers to
them, so the ones that made it to
+ // the blob store are only reachable from here
+ if (getBlobStore() != null) {
+ deleteBlobs(keys);
+ }
throw new RuntimeException(e);
}
diff --git
a/storm-client/test/jvm/org/apache/storm/dependency/DependencyUploaderTest.java
b/storm-client/test/jvm/org/apache/storm/dependency/DependencyUploaderTest.java
index 1821e2edc..e549fc83b 100644
---
a/storm-client/test/jvm/org/apache/storm/dependency/DependencyUploaderTest.java
+++
b/storm-client/test/jvm/org/apache/storm/dependency/DependencyUploaderTest.java
@@ -264,8 +264,9 @@ public class DependencyUploaderTest {
verify(mockBlobStore).getBlobMeta(contains(expectedBlobKeyForArtifact));
verify(mockBlobStore).getBlobMeta(contains(expectedBlobKeyForArtifact2));
- // never rollback
- verify(mockBlobStore,
never()).deleteBlob(contains(expectedBlobKeyForArtifact));
+ // the artifacts uploaded before the failure are rolled back: their
keys are unique to this upload,
+ // so nothing else can be referring to them and leaving them behind
leaks blob store space forever
+ verify(mockBlobStore).deleteBlob(contains(expectedBlobKeyForArtifact));
verify(mockBlobStore,
never()).deleteBlob(contains(expectedBlobKeyForArtifact2));
}
diff --git
a/storm-server/src/main/java/org/apache/storm/daemon/nimbus/Nimbus.java
b/storm-server/src/main/java/org/apache/storm/daemon/nimbus/Nimbus.java
index 682db2338..ca45abc24 100644
--- a/storm-server/src/main/java/org/apache/storm/daemon/nimbus/Nimbus.java
+++ b/storm-server/src/main/java/org/apache/storm/daemon/nimbus/Nimbus.java
@@ -62,6 +62,7 @@ import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.function.UnaryOperator;
+import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.security.auth.Subject;
@@ -411,6 +412,9 @@ public class Nimbus implements Iface, Shutdownable,
DaemonCommon {
.build();
private static final List<String> EMPTY_STRING_LIST =
Collections.unmodifiableList(Collections.emptyList());
private static final Set<String> EMPTY_STRING_SET =
Collections.unmodifiableSet(Collections.emptySet());
+ //A dependency blob key whose file name part ends with a canonical UUID,
which a client splices in per upload.
+ private static final Pattern UNIQUE_DEPENDENCY_KEY = Pattern.compile(
+
"^dep-.+-[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}(\\..+)?$");
private static final RotatingMap<String, Long> topologyCleanupDetected =
new RotatingMap<>(2);
private static long topologyCleanupRotationTime = 0L;
@@ -2884,22 +2888,114 @@ public class Nimbus implements Iface, Shutdownable,
DaemonCommon {
return ret;
}
+ /**
+ * Collect the dependency blob keys that are still referenced by a
topology other than the ones being cleaned up.
+ *
+ * <p>Dependency blobs uploaded by a current client are unique to a single
submission, but a cluster upgraded from an
+ * older release can still hold artifact blobs whose key is derived from
the maven coordinate alone, and those are
+ * listed by every topology that was submitted with the same artifact.
Deleting one of those while a topology still
+ * needs it breaks worker launch and makes every nimbus refuse leadership,
so the reference set has to be exact.
+ *
+ * <p>This deliberately does not swallow read failures: a topology whose
code blob cannot be read has unknown
+ * references, and treating it as referencing nothing would allow a live
topology's dependencies to be deleted. Only
+ * a missing code blob is treated as contributing no references, because
such a topology cannot be launched anyway.
+ *
+ * @param excludedTopoIds the topologies being cleaned up, which must not
count as referencing anything
+ * @return every dependency blob key referenced by a topology that is not
being cleaned up
+ */
+ @VisibleForTesting
+ Set<String> referencedDependencyKeys(Set<String> excludedTopoIds) throws
Exception {
+ Set<String> candidateTopoIds = new
HashSet<>(Utils.OR(blobStore.storedTopoIds(), EMPTY_STRING_SET));
+ candidateTopoIds.addAll(Utils.OR(stormClusterState.activeStorms(),
EMPTY_STRING_LIST));
+ candidateTopoIds.removeAll(excludedTopoIds);
+
+ Set<String> referenced = new HashSet<>();
+ for (String topoId : candidateTopoIds) {
+ StormTopology topo;
+ try {
+ topo = readStormTopologyAsNimbus(topoId, topoCache);
+ } catch (KeyNotFoundException e) {
+ //The topology has no code blob, so it references no
dependencies and cannot be launched.
+ LOG.debug("No code found for {} while collecting dependency
blob references", topoId);
+ continue;
+ }
+ if (topo.is_set_dependency_jars()) {
+ referenced.addAll(topo.get_dependency_jars());
+ }
+ if (topo.is_set_dependency_artifacts()) {
+ referenced.addAll(topo.get_dependency_artifacts());
+ }
+ }
+ return referenced;
+ }
+
+ /**
+ * Tell whether a dependency blob key proves, by its shape alone, that no
other topology can refer to it.
+ *
+ * <p>A client that uploads a dependency splices a freshly generated UUID
into the file name before prefixing it
+ * with {@code dep-}, so a key of that shape belongs to exactly one upload
and therefore to exactly one topology.
+ * Artifact keys written by older clients are derived from the maven
coordinate alone
+ * ({@code dep-<group>-<artifact>-<version>.jar}) and are listed by every
topology built against that artifact, so
+ * they do not match: a coordinate would have to end in five dash
separated hexadecimal groups of exactly
+ * 8-4-4-4-12 characters for that.
+ *
+ * <p>This lives here rather than next to the key generator on purpose. It
is not a restatement of what the current
+ * client writes, but nimbus' own list of the key shapes it is willing to
treat as unique, and it has to keep
+ * recognising the shapes written by every client version whose blobs may
still be in the store even if the
+ * generator changes.
+ *
+ * @param key a dependency blob key listed by a topology
+ * @return true only if no other topology can be referring to the key
+ */
+ @VisibleForTesting
+ static boolean isProvablyUniqueDependencyKey(String key) {
+ return key != null && UNIQUE_DEPENDENCY_KEY.matcher(key).matches();
+ }
+
+ /**
+ * Remove the dependency blobs of a topology that is being cleaned up,
keeping the ones another topology still uses.
+ *
+ * <p>When {@code stillReferenced} is null the cluster-wide reference scan
failed, so it is unknown which blobs
+ * other topologies use. The topology's code blob is deleted right after
this call and a dependency blob key
+ * carries no topology id, so anything left behind now can never be found
again. Instead of giving up, the keys
+ * whose shape proves that no other topology can refer to them, that is
the ones carrying a generated UUID, are
+ * still reclaimed; keys that could be shared are kept.
+ *
+ * @param topoId the topology being cleaned up
+ * @param stillReferenced the dependency blob keys referenced by
topologies that are not being cleaned up, or null
+ * if that could not be determined
+ */
@VisibleForTesting
- public void rmDependencyJarsInTopology(String topoId) {
+ public void rmDependencyBlobsInTopology(String topoId, Set<String>
stillReferenced) {
try {
BlobStore store = blobStore;
IStormClusterState state = stormClusterState;
StormTopology topo = readStormTopologyAsNimbus(topoId, topoCache);
- List<String> dependencyJars = topo.get_dependency_jars();
- LOG.info("Removing dependency jars from blobs - {}",
dependencyJars);
- if (dependencyJars != null && !dependencyJars.isEmpty()) {
- for (String key : dependencyJars) {
+ Set<String> dependencies = new HashSet<>();
+ if (topo.is_set_dependency_jars()) {
+ dependencies.addAll(topo.get_dependency_jars());
+ }
+ if (topo.is_set_dependency_artifacts()) {
+ dependencies.addAll(topo.get_dependency_artifacts());
+ }
+ LOG.info("Removing dependency blobs of {} - {}", topoId,
dependencies);
+ for (String key : dependencies) {
+ if (stillReferenced == null) {
+ if (isProvablyUniqueDependencyKey(key)) {
+ rmBlobKey(store, key, state);
+ } else {
+ LOG.warn("Keeping dependency blob {} of {}, another
topology may refer to it and the references "
+ + "could not be read", key, topoId);
+ }
+ } else if (stillReferenced.contains(key)) {
+ LOG.info("Keeping dependency blob {} of {}, it is still
referenced by another topology", key, topoId);
+ } else {
rmBlobKey(store, key, state);
}
}
} catch (Exception e) {
- //Yes eat the exception
- LOG.info("Exception {}", e);
+ //Yes eat the exception, cleaning up the rest of the topology
matters more, but this leaves blobs behind.
+ LOG.warn("Could not remove the dependency blobs of {}, they will
be left in the blob store", topoId, e);
}
}
@@ -2940,13 +3036,24 @@ public class Nimbus implements Iface, Shutdownable,
DaemonCommon {
Set<String> toClean = new HashSet<>(topoIdsToClean(state,
blobStore, this.conf));
long topoIdSelectionDurationMs = Time.deltaMs(cleanupStartMs);
+ //Computed once for the whole pass, with the dying topologies
excluded so that two of them sharing a
+ //dependency blob do not keep it alive for each other. A null
result means the references are unknown and
+ //only the blobs that are provably unique to one topology are
reclaimed below.
+ Set<String> stillReferenced = null;
+ try {
+ stillReferenced = referencedDependencyKeys(toClean);
+ } catch (Exception e) {
+ LOG.warn("Could not determine which dependency blobs are still
in use, only the dependency blobs whose "
+ + "key proves they belong to a single topology are
reclaimed in this pass, the others are kept", e);
+ }
+
for (String topoId : toClean) {
LOG.info("Cleaning up {}", topoId);
state.teardownHeartbeats(topoId);
state.teardownTopologyErrors(topoId);
state.removeAllPrivateWorkerKeys(topoId);
state.removeBackpressure(topoId);
- rmDependencyJarsInTopology(topoId);
+ rmDependencyBlobsInTopology(topoId, stillReferenced);
forceDeleteTopoDistDir(topoId);
rmTopologyKeys(topoId);
heartbeatsCache.removeTopo(topoId);
diff --git
a/storm-server/src/test/java/org/apache/storm/daemon/nimbus/NimbusClojurePortTest.java
b/storm-server/src/test/java/org/apache/storm/daemon/nimbus/NimbusClojurePortTest.java
index 526cb0924..0975e52a5 100644
---
a/storm-server/src/test/java/org/apache/storm/daemon/nimbus/NimbusClojurePortTest.java
+++
b/storm-server/src/test/java/org/apache/storm/daemon/nimbus/NimbusClojurePortTest.java
@@ -455,8 +455,8 @@ public class NimbusClojurePortTest {
Mockito.verify(nimbus).rmTopologyKeys("topo3");
// removed topology dependencies
- Mockito.verify(nimbus).rmDependencyJarsInTopology("topo2");
- Mockito.verify(nimbus).rmDependencyJarsInTopology("topo3");
+
Mockito.verify(nimbus).rmDependencyBlobsInTopology(Mockito.eq("topo2"),
Mockito.anySet());
+
Mockito.verify(nimbus).rmDependencyBlobsInTopology(Mockito.eq("topo3"),
Mockito.anySet());
// remove topos from heartbeat cache
assertEquals(0, nimbus.getHeartbeatsCache().getNumToposCached());
diff --git
a/storm-server/src/test/java/org/apache/storm/daemon/nimbus/NimbusTest.java
b/storm-server/src/test/java/org/apache/storm/daemon/nimbus/NimbusTest.java
index f24468d4c..92d1a9ecf 100644
--- a/storm-server/src/test/java/org/apache/storm/daemon/nimbus/NimbusTest.java
+++ b/storm-server/src/test/java/org/apache/storm/daemon/nimbus/NimbusTest.java
@@ -18,6 +18,7 @@
package org.apache.storm.daemon.nimbus;
+import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -37,9 +38,11 @@ import org.apache.commons.io.FileUtils;
import org.apache.storm.Config;
import org.apache.storm.DaemonConfig;
import org.apache.storm.blobstore.BlobStore;
+import org.apache.storm.blobstore.BlobStoreAclHandler;
import org.apache.storm.blobstore.KeySequenceNumber;
import org.apache.storm.blobstore.LocalFsBlobStore;
import org.apache.storm.cluster.IStormClusterState;
+import org.apache.storm.dependency.DependencyBlobStoreUtils;
import org.apache.storm.generated.AuthorizationException;
import org.apache.storm.generated.Credentials;
import org.apache.storm.generated.InvalidTopologyException;
@@ -80,6 +83,7 @@ import org.mockito.MockedConstruction;
import org.mockito.MockitoAnnotations;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
@@ -91,6 +95,7 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockConstruction;
+import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -99,6 +104,10 @@ class NimbusTest {
private static final String BLOB_FILE_KEY = "file-key";
private static final String TOPO_NAME = "topo";
private static final String TOPO_ID = "topology1-1-1";
+ // an artifact blob key from before the key carried a uuid, which several
topologies can share
+ private static final String LEGACY_ARTIFACT_KEY =
"dep-group-artifact-1.0.0.jar";
+ // a dependency key written by a current client, which splices a generated
uuid into the file name
+ private static final String UNIQUE_JAR_KEY =
"dep-lib-11111111-1111-1111-1111-111111111111.jar";
@Mock
private StormMetricsRegistry metricRegistry;
@@ -443,6 +452,134 @@ class NimbusTest {
}
}
+ /**
+ * Register a topology in the blob store mock so that Nimbus can read its
dependency lists back.
+ */
+ private static void storeTopology(BlobStore store, String topoId,
List<String> jars, List<String> artifacts)
+ throws Exception {
+ StormTopology topo = new StormTopology();
+ topo.set_spouts(new HashMap<>());
+ topo.set_bolts(new HashMap<>());
+ topo.set_state_spouts(new HashMap<>());
+ topo.set_dependency_jars(jars);
+ topo.set_dependency_artifacts(artifacts);
+ String key = ConfigUtils.masterStormCodeKey(topoId);
+ when(store.readBlob(eq(key), any())).thenReturn(Utils.serialize(topo));
+ when(store.getBlobMeta(eq(key), any()))
+ .thenReturn(new ReadableBlobMeta(new
SettableBlobMeta(BlobStoreAclHandler.WORLD_EVERYTHING), 1));
+ }
+
+ private Nimbus cleanupNimbus(BlobStore store, IStormClusterState state)
throws Exception {
+ Map<String, Object> conf = new HashMap<>();
+ conf.put(DaemonConfig.NIMBUS_MONITOR_FREQ_SECS, 10);
+ conf.put(DaemonConfig.NIMBUS_TOPOLOGY_BLOBSTORE_DELETION_DELAY_MS, 0);
+ conf.put(Config.NIMBUS_THRIFT_TLS_PORT, 0);
+ when(leaderElector.isLeader()).thenReturn(true);
+ return new Nimbus(conf, iNimbus, state, nimbusInfo, store,
leaderElector, groupMapper, new StormMetricsRegistry());
+ }
+
+ @Test
+ void doCleanupRemovesBothDependencyJarsAndArtifactsOfADeadTopology()
throws Exception {
+ BlobStore store = mock(BlobStore.class);
+ IStormClusterState state = mock(IStormClusterState.class);
+ when(store.storedTopoIds()).thenReturn(Set.of("dead-topo"));
+ when(state.activeStorms()).thenReturn(List.of());
+ storeTopology(store, "dead-topo",
List.of("dep-lib-11111111-1111-1111-1111-111111111111.jar"),
+
List.of("dep-group-artifact-1.0.0-22222222-2222-2222-2222-222222222222.jar"));
+
+ cleanupNimbus(store, state).doCleanup();
+
+
verify(store).deleteBlob(eq("dep-lib-11111111-1111-1111-1111-111111111111.jar"),
any());
+
verify(store).deleteBlob(eq("dep-group-artifact-1.0.0-22222222-2222-2222-2222-222222222222.jar"),
any());
+ }
+
+ @Test
+ void doCleanupKeepsADependencyBlobThatAnotherLiveTopologyStillReferences()
throws Exception {
+ BlobStore store = mock(BlobStore.class);
+ IStormClusterState state = mock(IStormClusterState.class);
+ when(store.storedTopoIds()).thenReturn(Set.of("dead-topo",
"live-topo"));
+ when(state.activeStorms()).thenReturn(List.of("live-topo"));
+ // both were submitted before artifact keys carried a uuid, so they
share one artifact blob
+ storeTopology(store, "dead-topo",
List.of("dep-lib-11111111-1111-1111-1111-111111111111.jar"),
+ List.of(LEGACY_ARTIFACT_KEY));
+ storeTopology(store, "live-topo",
List.of("dep-lib-33333333-3333-3333-3333-333333333333.jar"),
+ List.of(LEGACY_ARTIFACT_KEY));
+
+ cleanupNimbus(store, state).doCleanup();
+
+ verify(store, never()).deleteBlob(eq(LEGACY_ARTIFACT_KEY), any());
+ // the blob that is unique to the dead topology is still reclaimed
+
verify(store).deleteBlob(eq("dep-lib-11111111-1111-1111-1111-111111111111.jar"),
any());
+ }
+
+ @Test
+ void
doCleanupRemovesADependencyBlobSharedOnlyBetweenTopologiesThatAreAllBeingCleanedUp()
throws Exception {
+ BlobStore store = mock(BlobStore.class);
+ IStormClusterState state = mock(IStormClusterState.class);
+ when(store.storedTopoIds()).thenReturn(Set.of("dead-one", "dead-two"));
+ when(state.activeStorms()).thenReturn(List.of());
+ storeTopology(store, "dead-one", List.of(),
List.of(LEGACY_ARTIFACT_KEY));
+ storeTopology(store, "dead-two", List.of(),
List.of(LEGACY_ARTIFACT_KEY));
+
+ cleanupNimbus(store, state).doCleanup();
+
+ verify(store, atLeastOnce()).deleteBlob(eq(LEGACY_ARTIFACT_KEY),
any());
+ }
+
+ @Test
+ void
doCleanupReclaimsOnlyProvablyUniqueDependencyBlobsWhenTheReferencesCannotBeRead()
throws Exception {
+ BlobStore store = mock(BlobStore.class);
+ IStormClusterState state = mock(IStormClusterState.class);
+ when(store.storedTopoIds()).thenReturn(Set.of("dead-topo",
"live-topo"));
+ when(state.activeStorms()).thenReturn(List.of("live-topo"));
+ storeTopology(store, "dead-topo", List.of(UNIQUE_JAR_KEY),
List.of(LEGACY_ARTIFACT_KEY));
+ // the live topology's code blob cannot be read, so what it references
is unknown
+ when(store.readBlob(eq(ConfigUtils.masterStormCodeKey("live-topo")),
any()))
+ .thenThrow(new IOException("blob store is unhappy"));
+
+ cleanupNimbus(store, state).doCleanup();
+
+ // the dead topology's code blob goes away in this pass and a
dependency key carries no topology id, so a
+ // blob that is not reclaimed now can never be found again; the key
that carries a uuid cannot be shared
+ verify(store).deleteBlob(eq(UNIQUE_JAR_KEY), any());
+ // the legacy key could still be listed by the topology whose
references could not be read
+ verify(store, never()).deleteBlob(eq(LEGACY_ARTIFACT_KEY), any());
+ // the rest of the cleanup still runs
+
verify(store).deleteBlob(eq(ConfigUtils.masterStormJarKey("dead-topo")), any());
+ }
+
+ @Test
+ void
doCleanupContinuesTheReferenceScanWhenACandidateTopologyHasNoCodeBlob() throws
Exception {
+ BlobStore store = mock(BlobStore.class);
+ IStormClusterState state = mock(IStormClusterState.class);
+ when(store.storedTopoIds()).thenReturn(Set.of("dead-topo",
"live-topo", "gone-topo"));
+ when(state.activeStorms()).thenReturn(List.of("live-topo",
"gone-topo"));
+ storeTopology(store, "dead-topo", List.of(UNIQUE_JAR_KEY),
+ List.of(LEGACY_ARTIFACT_KEY, "dep-other-artifact-2.0.0.jar"));
+ storeTopology(store, "live-topo", List.of(),
List.of(LEGACY_ARTIFACT_KEY));
+ // one candidate topology has no code blob at all, so it references no
dependencies; unlike a read
+ // failure this does not abort the scan, the remaining topologies'
references are still collected
+ when(store.readBlob(eq(ConfigUtils.masterStormCodeKey("gone-topo")),
any()))
+ .thenThrow(new
KeyNotFoundException(ConfigUtils.masterStormCodeKey("gone-topo")));
+
+ cleanupNimbus(store, state).doCleanup();
+
+ // the scan succeeded, so even a shareable-shaped key is reclaimed
once nothing references it
+ verify(store).deleteBlob(eq("dep-other-artifact-2.0.0.jar"), any());
+ verify(store).deleteBlob(eq(UNIQUE_JAR_KEY), any());
+ // while the reference of the topology that could be read is honoured
+ verify(store, never()).deleteBlob(eq(LEGACY_ARTIFACT_KEY), any());
+ }
+
+ @Test
+ void
everyDependencyKeyACurrentClientGeneratesIsRecognisedAsUniqueToOneTopology() {
+ for (String fileName : List.of("commons-lang3-3.12.0.jar",
"some.lib.tar.gz", "noextension")) {
+ String key = DependencyBlobStoreUtils.generateDependencyBlobKey(
+ DependencyBlobStoreUtils.applyUUIDToFileName(fileName));
+ assertTrue(Nimbus.isProvablyUniqueDependencyKey(key), key + "
should be recognised as unique");
+ }
+ }
+
@Test
void testGetTopologyHistoryIsAuthorized() throws Exception {
Map<String, Object> conf = new HashMap<>();
@@ -459,6 +596,25 @@ class NimbusTest {
}
}
+ @Test
+ void
aDependencyKeyThatDoesNotCarryAUuidIsNotTreatedAsUniqueToOneTopology() {
+ // the shapes an older client wrote for an artifact, dep- plus the
maven coordinate with : replaced by -
+ for (String key : List.of("dep-group-artifact-1.0.0.jar",
+
"dep-org.apache.commons-commons-lang3-3.12.0.jar",
+ "dep-a-b-1.2.3-SNAPSHOT.jar",
+ // hexadecimal looking coordinates of the
wrong lengths are not a uuid either
+ "dep-com.deadbeef-cafebabe-1.0.jar",
+
"dep-abcdefab-abcd-abcd-abcd-abcdefabcdef-1.0.jar",
+ // the uuid a client splices in is the last
thing before the extension, so a
+ // coordinate that merely contains a uuid
shaped run is still shareable
+
"dep-com.acme-abcdefab-abcd-abcd-abcd-abcdefabcdef-1.0.jar",
+ // a uuid with a dot instead of a dash is
not canonical
+
"dep-lib-11111111.1111-1111-1111-111111111111.jar")) {
+ assertFalse(Nimbus.isProvablyUniqueDependencyKey(key), key + "
should not be recognised as unique");
+ }
+ assertFalse(Nimbus.isProvablyUniqueDependencyKey(null));
+ }
+
private static void setCaller(String user) {
Subject subject = new Subject();
subject.getPrincipals().add(new SingleUserPrincipal(user));