This is an automated email from the ASF dual-hosted git repository.
GGraziadei 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 b9f9b182a Deliver upstream feedback to remote tasks and stop spurious
"No remote destination available" warnings (#9175)
b9f9b182a is described below
commit b9f9b182a92c6fb333160fa088533e489f8b3645
Author: ErnestasVaiciukeviciusAdform <[email protected]>
AuthorDate: Fri Oct 23 20:59:02 2026 +0300
Deliver upstream feedback to remote tasks and stop spurious "No remote
destination available" warnings (#9175)
* Deliver upstream feedback to remote tasks and stop spurious "No remote
destination available" warnings
When topology.upstream.feedback.enable is true, feedback tuples are routed
directly to upstream (source) component tasks, but a worker's routable
remote
destinations (cachedTaskToNodePort) and connections are built only from
outboundTasks, which contained just the downstream-target components of
local
tasks. Feedback recipients were therefore missing, so TransferDrainer logged
"No remote destination available for task" and silently dropped the tuple;
cross-worker feedback was never delivered. Whether the warning appeared
depended
on task placement, which was misleading.
Include the non-system source (upstream) components of local tasks in
outboundTasks when feedback is enabled, mirroring
Executor.computeUpstreamTaskIds
at worker granularity, so feedback recipients are routable and connected.
Co-authored-by: Copilot <[email protected]>
* Remove unnecessary test, cleanup comments
* Upstream feedback setup: use topologyConf and exclude system components
* Enhance WorkerOutboundTasksTest: add tests for feedback routing and acker
task behavior
* Refactor WorkerState test utilities: enhance getWorkerState method and
add feedback configuration tests
* Apply suggestions from compiler warnings
* Move testTopologyConfigEnabledFeedback/testTopologyConfigDisabledFeedback
to WorkerOutboundTasksTest
* Improve testTopologyConfigDisabledFeedback testcase
* Refactor NoopSpout and NoopBolt classes: remove unnecessary interface
implementations and simplify method signatures
* Enhance documentation for upstream feedback: clarify connection overhead
and security implications in Config.java and Performance.md
* WorkerOutboundTasksTest: made test naming consistent
* New test cases for sink-only worker, with/without ackers
* Extracted common logic of upstream components for feedback from
WorkerState and Executor into StormCommon.upstreamFeedbackSourceComponents
---------
Co-authored-by: Copilot <[email protected]>
---
docs/Performance.md | 13 ++
storm-client/src/jvm/org/apache/storm/Config.java | 6 +
.../jvm/org/apache/storm/daemon/StormCommon.java | 19 ++
.../apache/storm/daemon/worker/WorkerState.java | 26 ++-
.../jvm/org/apache/storm/executor/Executor.java | 7 +-
.../org/apache/storm/daemon/StormCommonTest.java | 66 ++++++
.../daemon/worker/TestUtilsForWorkerState.java | 31 +--
.../daemon/worker/WorkerOutboundTasksTest.java | 254 +++++++++++++++++++++
8 files changed, 400 insertions(+), 22 deletions(-)
diff --git a/docs/Performance.md b/docs/Performance.md
index 5774a8876..393ae0fca 100644
--- a/docs/Performance.md
+++ b/docs/Performance.md
@@ -187,6 +187,19 @@ Impact of this on performance is dependent on the topology
and its deployment fo
Consequently it is useful to assess the impact of setting
`topology.disable.loadaware.messaging` to `true` or `false` for your
specific case.
+### Jitter-aware messaging
+
+`org.apache.storm.grouping.JitterAwareStreamGrouping` is an opt-in grouping
that favors downstream
+tasks reporting lower jitter, falling back to load-aware shuffle when jitter
feedback cannot
+distinguish the candidates. It requires both `topology.stats.ewma.enable=true`
and
+`topology.upstream.feedback.enable=true` (both default to `false`). Enabling
feedback alone
+does not select this grouping.
+
+Enabling upstream feedback increases inter-worker connection fan-in: each
worker hosting a user
+bolt establishes an outbound Netty client connection to every remote worker
hosting any task of
+that bolt's immediate upstream components (spouts or bolts), regardless of
stream grouping.
+Connections are shared across tasks targeting the same worker, and existing
outbound connections
+are reused. Same-worker feedback does not require a network connection.
## 7. Sampling Rate
Sampling rate is used to control how often certain metrics are computed on the
Spout and Bolt executors. This is configured using `topology.stats.sample.rate`
diff --git a/storm-client/src/jvm/org/apache/storm/Config.java
b/storm-client/src/jvm/org/apache/storm/Config.java
index 557872754..de051f2f7 100644
--- a/storm-client/src/jvm/org/apache/storm/Config.java
+++ b/storm-client/src/jvm/org/apache/storm/Config.java
@@ -615,6 +615,12 @@ public class Config extends HashMap<String, Object> {
* Flag to enable or disable the feedback channel for upstream
communication.
* When true, components can send unanchored tuples back to their source
tasks.
*
+ * <p><b>Connection overhead:</b> when enabled, each worker hosting a user
bolt establishes
+ * an outbound Netty client connection to every remote worker hosting a
task of that bolt's
+ * immediate upstream components, regardless of stream grouping.
Connections are shared
+ * across tasks targeting the same worker, and existing outbound
connections are reused;
+ * same-worker feedback requires no network connection.</p>
+ *
* <p><b>Security:</b> feedback tuples carry a routing control signal
(e.g. per-task EWMA jitter
* used by grouping decisions), so a peer that can inject messages on the
worker transport could
* forge feedback and deterministically steer a topology's traffic to a
chosen task. This stays
diff --git a/storm-client/src/jvm/org/apache/storm/daemon/StormCommon.java
b/storm-client/src/jvm/org/apache/storm/daemon/StormCommon.java
index 0776e1eea..0d82e99f6 100644
--- a/storm-client/src/jvm/org/apache/storm/daemon/StormCommon.java
+++ b/storm-client/src/jvm/org/apache/storm/daemon/StormCommon.java
@@ -44,6 +44,7 @@ import org.apache.storm.security.auth.IAuthorizer;
import org.apache.storm.serialization.SerializationFactory;
import org.apache.storm.shade.net.minidev.json.JSONValue;
import org.apache.storm.shade.org.apache.commons.lang3.StringUtils;
+import org.apache.storm.task.GeneralTopologyContext;
import org.apache.storm.task.IBolt;
import org.apache.storm.task.WorkerTopologyContext;
import org.apache.storm.tuple.Fields;
@@ -374,6 +375,24 @@ public class StormCommon {
return Arrays.asList("task-info", "feedback");
}
+ /**
+ * Returns the distinct upstream source component ids eligible to receive
feedback, excluding system components.
+ * System components do not send upstream feedback and therefore have no
eligible sources.
+ */
+ public static Set<String>
upstreamFeedbackSourceComponents(GeneralTopologyContext context, String
componentId) {
+ Set<String> components = new HashSet<>();
+ if (Utils.isSystemId(componentId)) {
+ return components;
+ }
+ for (GlobalStreamId source : context.getSources(componentId).keySet())
{
+ String sourceComponentId = source.get_componentId();
+ if (!Utils.isSystemId(sourceComponentId)) {
+ components.add(sourceComponentId);
+ }
+ }
+ return components;
+ }
+
@SuppressWarnings("unchecked")
public static Map<String, Bolt> metricsConsumerBoltSpecs(Map<String,
Object> conf, StormTopology topology) {
Map<String, Bolt> metricsConsumerBolts = new HashMap<>();
diff --git
a/storm-client/src/jvm/org/apache/storm/daemon/worker/WorkerState.java
b/storm-client/src/jvm/org/apache/storm/daemon/worker/WorkerState.java
index 59aceb8d6..82e234fc0 100644
--- a/storm-client/src/jvm/org/apache/storm/daemon/worker/WorkerState.java
+++ b/storm-client/src/jvm/org/apache/storm/daemon/worker/WorkerState.java
@@ -68,8 +68,10 @@ import org.apache.storm.policy.IWaitStrategy;
import org.apache.storm.security.auth.IAutoCredentials;
import org.apache.storm.serialization.ITupleSerializer;
import org.apache.storm.serialization.KryoTupleSerializer;
+import org.apache.storm.shade.com.google.common.annotations.VisibleForTesting;
import org.apache.storm.shade.com.google.common.collect.ImmutableMap;
import org.apache.storm.shade.com.google.common.collect.Sets;
+import org.apache.storm.task.GeneralTopologyContext;
import org.apache.storm.task.WorkerTopologyContext;
import org.apache.storm.task.WorkerUserContext;
import org.apache.storm.tuple.AddressedTuple;
@@ -800,12 +802,32 @@ public class WorkerState {
* @return seq of task ids that receive messages from this worker
*/
private Set<Integer> workerOutboundTasks() {
- WorkerTopologyContext context = getWorkerTopologyContext();
+ return computeOutboundTasks(getWorkerTopologyContext(), localTaskIds,
taskToComponent,
+ ConfigUtils.upstreamFeedbackEnable(topologyConf));
+ }
+
+ /**
+ * Computes the set of task ids this worker sends messages to.
+ *
+ * <p>Normally these are the tasks of the components that are downstream
targets of the
+ * locally-hosted components. When upstream feedback is enabled, the
source (upstream)
+ * components of the locally-hosted components are also included: feedback
tuples are routed
+ * directly to those upstream tasks (bypassing stream grouping), so they
must be reachable
+ * remote destinations and have connections established. Otherwise the
feedback is
+ * dropped and {@code TransferDrainer} logs "No remote destination
available for task".
+ */
+ @VisibleForTesting
+ static Set<Integer> computeOutboundTasks(GeneralTopologyContext context,
Collection<Integer> localTaskIds,
+ Map<Integer, String>
taskToComponent, boolean feedbackEnabled) {
Set<String> components = new HashSet<>();
for (Integer taskId : localTaskIds) {
- for (Map<String, Grouping> value :
context.getTargets(context.getComponentId(taskId)).values()) {
+ String componentId = context.getComponentId(taskId);
+ for (Map<String, Grouping> value :
context.getTargets(componentId).values()) {
components.addAll(value.keySet());
}
+ if (feedbackEnabled) {
+
components.addAll(StormCommon.upstreamFeedbackSourceComponents(context,
componentId));
+ }
}
Set<Integer> outboundTasks = new HashSet<>();
diff --git a/storm-client/src/jvm/org/apache/storm/executor/Executor.java
b/storm-client/src/jvm/org/apache/storm/executor/Executor.java
index 9a319bf07..e40d19cb8 100644
--- a/storm-client/src/jvm/org/apache/storm/executor/Executor.java
+++ b/storm-client/src/jvm/org/apache/storm/executor/Executor.java
@@ -60,7 +60,6 @@ import org.apache.storm.executor.spout.SpoutExecutor;
import org.apache.storm.generated.Bolt;
import org.apache.storm.generated.Credentials;
import org.apache.storm.generated.DebugOptions;
-import org.apache.storm.generated.GlobalStreamId;
import org.apache.storm.generated.Grouping;
import org.apache.storm.generated.SpoutSpec;
import org.apache.storm.generated.StormTopology;
@@ -603,11 +602,7 @@ public abstract class Executor implements Callable,
JCQueue.Consumer {
*/
private List<Integer> computeUpstreamTaskIds() {
Set<Integer> taskIds = new HashSet<>();
- for (GlobalStreamId source :
workerTopologyContext.getSources(componentId).keySet()) {
- String sourceComponentId = source.get_componentId();
- if (Utils.isSystemId(sourceComponentId)) {
- continue;
- }
+ for (String sourceComponentId :
StormCommon.upstreamFeedbackSourceComponents(workerTopologyContext,
componentId)) {
taskIds.addAll(workerTopologyContext.getComponentTasks(sourceComponentId));
}
return new ArrayList<>(taskIds);
diff --git a/storm-client/test/jvm/org/apache/storm/daemon/StormCommonTest.java
b/storm-client/test/jvm/org/apache/storm/daemon/StormCommonTest.java
new file mode 100644
index 000000000..a0746a509
--- /dev/null
+++ b/storm-client/test/jvm/org/apache/storm/daemon/StormCommonTest.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership. The
ASF licenses this file to you under the Apache License, Version
+ * 2.0 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions
+ * and limitations under the License.
+ */
+
+package org.apache.storm.daemon;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Map;
+import java.util.Set;
+import org.apache.storm.Constants;
+import org.apache.storm.generated.GlobalStreamId;
+import org.apache.storm.generated.Grouping;
+import org.apache.storm.task.GeneralTopologyContext;
+import org.junit.jupiter.api.Test;
+
+public class StormCommonTest {
+ @Test
+ public void
testUpstreamFeedbackSourceComponentsFiltersSystemSourcesAndDeduplicatesComponents()
{
+ GeneralTopologyContext context = mock(GeneralTopologyContext.class);
+ when(context.getSources("sink")).thenReturn(Map.of(
+ new GlobalStreamId("source", "first"), new Grouping(),
+ new GlobalStreamId("source", "second"), new Grouping(),
+ new GlobalStreamId("other-source",
StormCommon.SYSTEM_STREAM_ID), new Grouping(),
+ new GlobalStreamId(Acker.ACKER_COMPONENT_ID, "ack"), new
Grouping(),
+ new GlobalStreamId(Constants.SYSTEM_COMPONENT_ID,
StormCommon.SYSTEM_STREAM_ID), new Grouping()));
+
+ assertEquals(Set.of("source", "other-source"),
StormCommon.upstreamFeedbackSourceComponents(context, "sink"));
+ }
+
+ @Test
+ public void testUpstreamFeedbackSourceComponentsForSystemComponent() {
+ GeneralTopologyContext context = mock(GeneralTopologyContext.class);
+ when(context.getSources(Acker.ACKER_COMPONENT_ID)).thenReturn(Map.of(
+ new GlobalStreamId("source", "default"), new Grouping()));
+
+ assertEquals(Set.of(),
StormCommon.upstreamFeedbackSourceComponents(context,
Acker.ACKER_COMPONENT_ID));
+ }
+
+ @Test
+ public void testUpstreamFeedbackSourceComponentsWithoutSources() {
+ GeneralTopologyContext context = mock(GeneralTopologyContext.class);
+ when(context.getSources("spout")).thenReturn(Map.of());
+
+ assertEquals(Set.of(),
StormCommon.upstreamFeedbackSourceComponents(context, "spout"));
+ }
+
+ @Test
+ public void testUpstreamFeedbackSourceComponentsWithOnlySystemSources() {
+ GeneralTopologyContext context = mock(GeneralTopologyContext.class);
+ when(context.getSources("sink")).thenReturn(Map.of(
+ new GlobalStreamId(Acker.ACKER_COMPONENT_ID, "ack"), new
Grouping()));
+
+ assertEquals(Set.of(),
StormCommon.upstreamFeedbackSourceComponents(context, "sink"));
+ }
+}
diff --git
a/storm-client/test/jvm/org/apache/storm/daemon/worker/TestUtilsForWorkerState.java
b/storm-client/test/jvm/org/apache/storm/daemon/worker/TestUtilsForWorkerState.java
index 3941f1778..145c28070 100644
---
a/storm-client/test/jvm/org/apache/storm/daemon/worker/TestUtilsForWorkerState.java
+++
b/storm-client/test/jvm/org/apache/storm/daemon/worker/TestUtilsForWorkerState.java
@@ -46,20 +46,6 @@ public class TestUtilsForWorkerState {
public static final String RESOURCE_VALUE = "resource-value";
public static WorkerState getWorkerState(Map<String, Object> conf, String
topologyId) throws IOException, TException {
- IContext context = mock(IContext.class);
-
- String assignmentId = null;
- Assignment assignment = mock(Assignment.class);
- when(assignment.get_executor_node_port()).thenReturn(new HashMap<>());
- Supervisor.Iface supervisorIface = mock(Supervisor.Iface.class);
-
when(supervisorIface.getLocalAssignmentForStorm(anyString())).thenReturn(assignment);
- SupervisorIfaceFactory supervisorIfaceFactory =
mock(SupervisorIfaceFactory.class);
- when(supervisorIfaceFactory.getIface()).thenReturn(supervisorIface);
- Supplier<SupervisorIfaceFactory> supervisorIfaceSupplier = () ->
supervisorIfaceFactory;
-
- int port = 0;
- String workerId = null;
-
Map<String, Object> topologyConf = new HashMap<>();
topologyConf.put(Config.STORM_MESSAGING_TRANSPORT,
"org.apache.storm.messaging.netty.Context");
topologyConf.put(Config.STORM_MESSAGING_NETTY_CLIENT_WORKER_THREADS,
1);
@@ -78,6 +64,23 @@ public class TestUtilsForWorkerState {
topologyConf.put(Config.TOPOLOGY_TRANSFER_BUFFER_SIZE, 1000);
topologyConf.put(Config.TOPOLOGY_TRANSFER_BATCH_SIZE, 1);
+ Assignment assignment = mock(Assignment.class);
+ when(assignment.get_executor_node_port()).thenReturn(new HashMap<>());
+ return getWorkerState(conf, topologyId, topologyConf, null,
assignment);
+ }
+
+ public static WorkerState getWorkerState(Map<String, Object> conf, String
topologyId,
+ Map<String, Object> topologyConf,
String assignmentId, Assignment assignment)
+ throws IOException, TException {
+ IContext context = mock(IContext.class);
+ Supervisor.Iface supervisorIface = mock(Supervisor.Iface.class);
+
when(supervisorIface.getLocalAssignmentForStorm(anyString())).thenReturn(assignment);
+ SupervisorIfaceFactory supervisorIfaceFactory =
mock(SupervisorIfaceFactory.class);
+ when(supervisorIfaceFactory.getIface()).thenReturn(supervisorIface);
+ Supplier<SupervisorIfaceFactory> supervisorIfaceSupplier = () ->
supervisorIfaceFactory;
+ int port = 0;
+ String workerId = null;
+
IStateStorage stateStorage = null;
IStormClusterState stormClusterState = null;
Collection<IAutoCredentials> autoCreds = null;
diff --git
a/storm-client/test/jvm/org/apache/storm/daemon/worker/WorkerOutboundTasksTest.java
b/storm-client/test/jvm/org/apache/storm/daemon/worker/WorkerOutboundTasksTest.java
new file mode 100644
index 000000000..2a66996cb
--- /dev/null
+++
b/storm-client/test/jvm/org/apache/storm/daemon/worker/WorkerOutboundTasksTest.java
@@ -0,0 +1,254 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership. The
ASF licenses this file to you under the Apache License, Version
+ * 2.0 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions
+ * and limitations under the License.
+ */
+
+package org.apache.storm.daemon.worker;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.storm.Config;
+import org.apache.storm.Constants;
+import org.apache.storm.daemon.Acker;
+import org.apache.storm.daemon.StormCommon;
+import org.apache.storm.daemon.supervisor.AdvancedFSOps;
+import org.apache.storm.generated.Assignment;
+import org.apache.storm.generated.InvalidTopologyException;
+import org.apache.storm.generated.NodeInfo;
+import org.apache.storm.generated.StormTopology;
+import org.apache.storm.spout.SpoutOutputCollector;
+import org.apache.storm.task.GeneralTopologyContext;
+import org.apache.storm.task.OutputCollector;
+import org.apache.storm.task.TopologyContext;
+import org.apache.storm.testing.TestWordCounter;
+import org.apache.storm.testing.TestWordSpout;
+import org.apache.storm.thrift.TException;
+import org.apache.storm.topology.IRichBolt;
+import org.apache.storm.topology.IRichSpout;
+import org.apache.storm.topology.OutputFieldsDeclarer;
+import org.apache.storm.topology.TopologyBuilder;
+import org.apache.storm.topology.base.BaseRichBolt;
+import org.apache.storm.topology.base.BaseRichSpout;
+import org.apache.storm.tuple.Fields;
+import org.apache.storm.tuple.Tuple;
+import org.apache.storm.utils.ConfigUtils;
+import org.apache.storm.utils.Utils;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for worker outbound routing and topology-level feedback
configuration. The key behaviour under test:
+ * when upstream feedback is enabled, the upstream (source) component
+ * tasks of locally-hosted components must be included, because feedback
tuples are routed directly to them. Otherwise
+ * they are missing from {@code cachedTaskToNodePort}, the feedback is dropped
and {@code TransferDrainer} logs the
+ * spurious "No remote destination available for task" warning.
+ */
+public class WorkerOutboundTasksTest {
+
+ // Linear topology: spout -> bolt1 -> bolt2. Task ids are topology-global
and unique per component.
+ private static final List<Integer> SPOUT_TASKS = Arrays.asList(1, 2);
+ private static final List<Integer> BOLT1_TASKS = Arrays.asList(3, 4);
+ private static final List<Integer> BOLT2_TASKS = Arrays.asList(5, 6);
+ private static final List<Integer> ACKER_TASKS = List.of(7);
+
+ private GeneralTopologyContext context() throws InvalidTopologyException {
+ TopologyBuilder builder = new TopologyBuilder();
+ builder.setSpout("spout", new NoopSpout());
+ builder.setBolt("bolt1", new NoopBolt()).shuffleGrouping("spout");
+ builder.setBolt("bolt2", new NoopBolt()).shuffleGrouping("bolt1");
+ Map<String, Object> topoConf = Utils.readStormConfig();
+ topoConf.put(Config.TOPOLOGY_WORKERS, 1);
+ topoConf.put(Config.TOPOLOGY_ACKER_EXECUTORS, ACKER_TASKS.size());
+ StormTopology topology = StormCommon.systemTopology(topoConf,
builder.createTopology());
+
+ Map<Integer, String> taskToComponent = new HashMap<>();
+ SPOUT_TASKS.forEach(t -> taskToComponent.put(t, "spout"));
+ BOLT1_TASKS.forEach(t -> taskToComponent.put(t, "bolt1"));
+ BOLT2_TASKS.forEach(t -> taskToComponent.put(t, "bolt2"));
+ ACKER_TASKS.forEach(t -> taskToComponent.put(t,
Acker.ACKER_COMPONENT_ID));
+
+ Map<String, List<Integer>> componentToSortedTasks = new HashMap<>();
+ componentToSortedTasks.put("spout", SPOUT_TASKS);
+ componentToSortedTasks.put("bolt1", BOLT1_TASKS);
+ componentToSortedTasks.put("bolt2", BOLT2_TASKS);
+ componentToSortedTasks.put(Acker.ACKER_COMPONENT_ID, ACKER_TASKS);
+
+ GeneralTopologyContext context = new GeneralTopologyContext(topology,
topoConf, taskToComponent, componentToSortedTasks,
+ new HashMap<>(), "topology-id");
+
assertTrue(context.getComponentIds().contains(Acker.ACKER_COMPONENT_ID));
+ return context;
+ }
+
+ @Test
+ public void testFeedbackDisabledOnlyDownstreamTargets() throws
InvalidTopologyException {
+ // Worker hosts a bolt1 task. Its only downstream target is bolt2, so
the upstream spout tasks are not routable.
+ GeneralTopologyContext context = context();
+ Set<Integer> outbound = WorkerState.computeOutboundTasks(context,
BOLT1_TASKS.subList(0, 1),
+ context.getTaskToComponent(), false);
+
+ BOLT2_TASKS.forEach(t -> assertTrue(outbound.contains(t), "downstream
bolt2 task " + t + " must remain routable"));
+ ACKER_TASKS.forEach(t -> assertTrue(outbound.contains(t), "acker task
" + t + " must remain routable"));
+ SPOUT_TASKS.forEach(t -> assertFalse(outbound.contains(t), "spout task
" + t + " must not be routable without feedback"));
+ }
+
+ @Test
+ public void testFeedbackEnabledIncludesUpstreamSourceTasks() throws
InvalidTopologyException {
+ // Worker hosts a bolt1 task. With feedback on, bolt1's upstream spout
tasks must also be routable so feedback
+ // tuples reach them instead of being dropped.
+ GeneralTopologyContext context = context();
+ Set<Integer> outbound = WorkerState.computeOutboundTasks(context,
BOLT1_TASKS.subList(0, 1),
+ context.getTaskToComponent(), true);
+
+ BOLT2_TASKS.forEach(t -> assertTrue(outbound.contains(t), "downstream
bolt2 task " + t + " must remain routable"));
+ ACKER_TASKS.forEach(t -> assertTrue(outbound.contains(t), "acker task
" + t + " must remain routable"));
+ SPOUT_TASKS.forEach(t -> assertTrue(outbound.contains(t), "upstream
spout task " + t + " must be routable with feedback"));
+ }
+
+ @Test
+ public void
testFeedbackDisabledColocatedSpoutMasksMissingUpstreamRouting() throws
InvalidTopologyException {
+ GeneralTopologyContext context = context();
+ List<Integer> localTasks = Arrays.asList(SPOUT_TASKS.getFirst(),
BOLT2_TASKS.getFirst());
+ Set<Integer> withoutFeedback =
WorkerState.computeOutboundTasks(context, localTasks,
+ context.getTaskToComponent(), false);
+ Set<Integer> withFeedback = WorkerState.computeOutboundTasks(context,
localTasks,
+ context.getTaskToComponent(), true);
+
+ // The spout already makes bolt1 routable for bolt2 feedback.
+ // This demonstrates how the issue with feedback delivery setup could
be hidden in particular configurations.
+ BOLT1_TASKS.forEach(t -> assertTrue(withoutFeedback.contains(t),
+ "bolt1 task " + t + " must already be routable as the
colocated spout's downstream target"));
+ BOLT1_TASKS.forEach(t -> assertTrue(withFeedback.contains(t),
+ "upstream bolt1 task " + t + " must remain routable with
feedback"));
+ ACKER_TASKS.forEach(t -> assertTrue(withoutFeedback.contains(t),
"acker task " + t + " must remain routable"));
+ ACKER_TASKS.forEach(t -> assertTrue(withFeedback.contains(t), "acker
task " + t + " must remain routable with feedback"));
+ }
+
+ @Test
+ public void
testFeedbackEnabledSystemComponentsUpstreamNotAddedAsFeedbackTasks() throws
InvalidTopologyException {
+ GeneralTopologyContext context = context();
+ Set<Integer> outbound = WorkerState.computeOutboundTasks(context,
ACKER_TASKS,
+ context.getTaskToComponent(), true);
+
+ SPOUT_TASKS.forEach(t -> assertTrue(outbound.contains(t), "spout task
" + t + " must remain routable for ack/fail messages"));
+ BOLT1_TASKS.forEach(t -> assertFalse(outbound.contains(t), "bolt1 task
" + t + " must not be routable"));
+ BOLT2_TASKS.forEach(t -> assertFalse(outbound.contains(t), "bolt2 task
" + t + " must not be routable"));
+ ACKER_TASKS.forEach(t -> assertFalse(outbound.contains(t), "acker task
" + t + " must not be routable from itself"));
+ }
+
+ @Test
+ public void testTopologyConfigEnabledFeedback() throws IOException,
TException {
+ assertOutboundTasksUseTopologyFeedbackConfiguration(true, 1);
+ }
+
+ @Test
+ public void testTopologyConfigDisabledFeedback() throws IOException,
TException {
+ assertOutboundTasksUseTopologyFeedbackConfiguration(false, 1);
+ }
+
+ @Test
+ public void
testSinkOnlyWorkerHasRemoteOutboundTasksWithFeedbackAndNoAckers() throws
IOException, TException {
+ assertOutboundTasksUseTopologyFeedbackConfiguration(true, 0);
+ }
+
+ @Test
+ public void
testSinkOnlyWorkerHasNoRemoteOutboundTasksWhenNoFeedbackAndNoAckers() throws
IOException, TException {
+ assertOutboundTasksUseTopologyFeedbackConfiguration(false, 0);
+ }
+
+ private void assertOutboundTasksUseTopologyFeedbackConfiguration(boolean
topologyFeedbackEnabled, int ackerExecutors)
+ throws IOException, TException {
+ Map<String, Object> conf = Utils.readStormConfig();
+ // set opposite to topology config to verify topology config is used
+ conf.put(Config.TOPOLOGY_UPSTREAM_FEEDBACK_ENABLE,
!topologyFeedbackEnabled);
+
+ Map<String, Object> topologyConf = new HashMap<>(conf);
+ topologyConf.put(Config.TOPOLOGY_UPSTREAM_FEEDBACK_ENABLE,
topologyFeedbackEnabled);
+ topologyConf.put(Config.TOPOLOGY_ACKER_EXECUTORS, ackerExecutors);
+ topologyConf.put(Config.TOPOLOGY_WORKERS, 1);
+
+ TopologyBuilder builder = new TopologyBuilder();
+ builder.setSpout("spout", new TestWordSpout(true)).setNumTasks(1);
+ builder.setBolt("bolt1", new
TestWordCounter()).setNumTasks(1).shuffleGrouping("spout");
+ StormTopology topology = builder.createTopology();
+ Map<String, List<Integer>> componentTasks =
Utils.reverseMap(StormCommon.stormTaskInfo(topology, topologyConf));
+ int localTask = componentTasks.get("bolt1").getFirst();
+ String assignmentId = "supervisor";
+ Assignment assignment = new Assignment();
+ assignment.set_executor_node_port(Collections.singletonMap(
+ Arrays.asList((long) localTask, (long) localTask),
+ new NodeInfo(assignmentId, Collections.singleton(0L))));
+
+ ConfigUtils mockedConfigUtils = mock(ConfigUtils.class);
+ ConfigUtils previousConfigUtils =
ConfigUtils.setInstance(mockedConfigUtils);
+ try {
+ String topologyId = "feedback-config";
+ when(mockedConfigUtils.readSupervisorTopologyImpl(eq(conf),
eq(topologyId), any(AdvancedFSOps.class)))
+ .thenReturn(topology);
+ WorkerState workerState = TestUtilsForWorkerState.getWorkerState(
+ conf, topologyId, topologyConf, assignmentId, assignment);
+ try {
+ assertEquals(Set.of((int) Constants.SYSTEM_TASK_ID,
localTask), Set.copyOf(workerState.getLocalTaskIds()),
+ "worker must host only the sink bolt and the system
task");
+ assertEquals(topologyFeedbackEnabled || ackerExecutors > 0,
workerState.hasRemoteOutboundTasks(),
+ "sink-only worker must have remote outbound tasks only
if feedback or acking is enabled");
+ Set<Integer> outbound = workerState.getOutboundTasks();
+ componentTasks.get("spout").forEach(t ->
+ assertEquals(topologyFeedbackEnabled,
outbound.contains(t),
+ "upstream routing to spout task " + t + " must
follow topology configuration"));
+ } finally {
+ workerState.closeResources();
+ }
+ } finally {
+ ConfigUtils.setInstance(previousConfigUtils);
+ }
+ }
+
+ private static class NoopSpout extends BaseRichSpout {
+ @Override
+ public void open(Map<String, Object> conf, TopologyContext context,
SpoutOutputCollector collector) {
+ }
+
+ @Override
+ public void nextTuple() {
+ }
+
+ @Override
+ public void declareOutputFields(OutputFieldsDeclarer declarer) {
+ declarer.declare(new Fields("value"));
+ }
+ }
+
+ private static class NoopBolt extends BaseRichBolt {
+ @Override
+ public void prepare(Map<String, Object> topoConf, TopologyContext
context, OutputCollector collector) {
+ }
+
+ @Override
+ public void execute(Tuple input) {
+ }
+
+ @Override
+ public void declareOutputFields(OutputFieldsDeclarer declarer) {
+ declarer.declare(new Fields("value"));
+ }
+ }
+}