Skip to content

Commit 7654c7b

Browse files
committed
Throw a typed exception for tuples with unknown task or stream ids and add a deserialization strict mode
1 parent 7cfc562 commit 7654c7b

6 files changed

Lines changed: 81 additions & 4 deletions

File tree

conf/defaults.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ storm.compression.zstd.level: 3
6161
storm.compression.zstd.max.decompressed.bytes: 104857600
6262
storm.compression.gzip.max.decompressed.bytes: 104857600
6363
topology.tuple.compression.max.decompressed.bytes: 10485760
64+
topology.tuple.deserialization.strict.enable: false
6465
storm.codedistributor.class: "org.apache.storm.codedistributor.LocalFileSystemCodeDistributor"
6566
storm.workers.artifacts.dir: "workers-artifacts"
6667
storm.health.check.dir: "healthchecks"

storm-client/src/jvm/org/apache/storm/Config.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1648,6 +1648,15 @@ public class Config extends HashMap<String, Object> {
16481648
*/
16491649
@IsPositiveNumber(includeZero = false)
16501650
public static final String TOPOLOGY_TUPLE_COMPRESSION_MAX_DECOMPRESSED_BYTES = "topology.tuple.compression.max.decompressed.bytes";
1651+
/**
1652+
* Topology configuration to make tuple deserialization failures fatal instead of dropping the undecodable message.
1653+
* By default a message that fails to decode on the receiving worker is dropped and counted, and the worker keeps
1654+
* running. When set to {@code true}, any deserialization failure propagates and the worker exits, restoring the
1655+
* pre-3.1.0 behavior.
1656+
* Default: {@code false}.
1657+
*/
1658+
@IsBoolean
1659+
public static final String TOPOLOGY_TUPLE_DESERIALIZATION_STRICT_ENABLE = "topology.tuple.deserialization.strict.enable";
16511660
/**
16521661
* Configure the topology metrics reporters to be used on workers.
16531662
*/

storm-client/src/jvm/org/apache/storm/messaging/DeserializingConnectionCallback.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.apache.storm.daemon.worker.WorkerState;
2929
import org.apache.storm.metric.api.IMetric;
3030
import org.apache.storm.serialization.KryoTupleDeserializer;
31+
import org.apache.storm.serialization.TupleDeserializationException;
3132
import org.apache.storm.task.GeneralTopologyContext;
3233
import org.apache.storm.tuple.AddressedTuple;
3334
import org.apache.storm.tuple.Tuple;
@@ -43,8 +44,10 @@ public class DeserializingConnectionCallback implements IConnectionCallback, IMe
4344
private static final Logger LOG = LoggerFactory.getLogger(DeserializingConnectionCallback.class);
4445

4546
// A tuple that cannot be decoded is dropped instead of killing the worker; anything outside this set keeps
46-
// the fatal handling in StormServerHandler.
47+
// the fatal handling in StormServerHandler. TupleDeserializationException is thrown by KryoTupleDeserializer
48+
// for unknown task or stream ids.
4749
private static final Set<Class<?>> TOLERATED_DESERIALIZATION_FAILURES = new HashSet<>(Arrays.asList(
50+
TupleDeserializationException.class,
4851
IOException.class,
4952
KryoException.class,
5053
IllegalArgumentException.class,
@@ -71,6 +74,8 @@ protected KryoTupleDeserializer initialValue() {
7174
}
7275
};
7376

77+
private final boolean strictMode;
78+
7479
// Track serialized size of messages.
7580
private final boolean sizeMetricsEnabled;
7681
private final ConcurrentHashMap<String, AtomicLong> byteCounts = new ConcurrentHashMap<>();
@@ -87,6 +92,7 @@ public DeserializingConnectionCallback(final Map<String, Object> conf, final Gen
8792
this.context = context;
8893
cb = callback;
8994
sizeMetricsEnabled = ObjectReader.getBoolean(conf.get(Config.TOPOLOGY_SERIALIZED_MESSAGE_SIZE_METRICS), false);
95+
strictMode = ObjectReader.getBoolean(conf.get(Config.TOPOLOGY_TUPLE_DESERIALIZATION_STRICT_ENABLE), false);
9096

9197
}
9298

@@ -104,7 +110,7 @@ public void recv(List<TaskMessage> batch) {
104110
try {
105111
tuple = des.deserialize(message.message());
106112
} catch (Exception e) {
107-
if (!isToleratedDeserializationFailure(e)) {
113+
if (strictMode || !isToleratedDeserializationFailure(e)) {
108114
throw e;
109115
}
110116
deserializationFailures.incrementAndGet();

storm-client/src/jvm/org/apache/storm/serialization/KryoTupleDeserializer.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,12 @@ private TupleImpl deserializeTuple(byte[] data) {
7979
int streamId = kryoInput.readInt(true);
8080
String componentName = context.getComponentId(taskId);
8181
if (componentName == null) {
82-
throw new IllegalArgumentException("Received a tuple from unknown task " + taskId);
82+
throw new TupleDeserializationException("Received a tuple from unknown task " + taskId);
8383
}
8484
String streamName = ids.getStreamName(componentName, streamId);
85+
if (streamName == null) {
86+
throw new TupleDeserializationException("Component " + componentName + " has no stream with id " + streamId);
87+
}
8588
MessageId id = MessageId.deserialize(kryoInput);
8689
List<Object> values = kryo.deserializeFrom(kryoInput);
8790
return new TupleImpl(context, values, componentName, taskId, streamName, id);
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with
3+
* this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version
4+
* 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
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
10+
* and limitations under the License.
11+
*/
12+
13+
package org.apache.storm.serialization;
14+
15+
/**
16+
* Thrown when a serialized tuple names a source task or stream that the receiving topology cannot resolve.
17+
*/
18+
public class TupleDeserializationException extends RuntimeException {
19+
public TupleDeserializationException(String message) {
20+
super(message);
21+
}
22+
}

storm-client/test/jvm/org/apache/storm/messaging/DeserializingConnectionCallbackTest.java

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.apache.storm.daemon.worker.WorkerState;
2828
import org.apache.storm.serialization.KryoTupleDeserializer;
2929
import org.apache.storm.serialization.KryoTupleSerializer;
30+
import org.apache.storm.serialization.TupleDeserializationException;
3031
import org.apache.storm.task.GeneralTopologyContext;
3132
import org.apache.storm.testing.TestWordCounter;
3233
import org.apache.storm.testing.TestWordSpout;
@@ -137,11 +138,46 @@ public void testUnknownSourceTaskDroppedAndBatchContinues() {
137138
out.writeInt(1, true); // default stream id
138139
byte[] unknownTask = out.toBytes();
139140

140-
assertThrows(IllegalArgumentException.class, () -> new KryoTupleDeserializer(conf, context).deserialize(unknownTask));
141+
TupleDeserializationException thrown = assertThrows(TupleDeserializationException.class,
142+
() -> new KryoTupleDeserializer(conf, context).deserialize(unknownTask));
143+
assertTrue(thrown.getMessage().contains("9999"),
144+
"expected the task id in the message but was: " + thrown.getMessage());
141145

142146
assertBatchDeliversOnlyValidMessages(conf, unknownTask);
143147
}
144148

149+
@Test
150+
public void testUnknownStreamIdDroppedAndBatchContinues() {
151+
Map<String, Object> conf = baseConf();
152+
Output out = new Output(16, 32);
153+
out.writeInt(SOURCE_TASK_ID, true); // source task that exists in the topology
154+
out.writeInt(3, true); // stream id the source component does not declare
155+
byte[] unknownStream = out.toBytes();
156+
157+
TupleDeserializationException thrown = assertThrows(TupleDeserializationException.class,
158+
() -> new KryoTupleDeserializer(conf, context).deserialize(unknownStream));
159+
assertTrue(thrown.getMessage().contains("id 3"),
160+
"expected the stream id in the message but was: " + thrown.getMessage());
161+
162+
assertBatchDeliversOnlyValidMessages(conf, unknownStream);
163+
}
164+
165+
@Test
166+
public void testStrictModeMakesFailuresFatal() {
167+
Map<String, Object> conf = baseConf();
168+
conf.put(Config.TOPOLOGY_TUPLE_DESERIALIZATION_STRICT_ENABLE, true);
169+
byte[] full = serializedTuple(conf, new Values("a-string-long-enough-to-survive-truncation", 7));
170+
byte[] truncated = Arrays.copyOf(full, full.length - 10);
171+
172+
WorkerState.ILocalTransferCallback transfer = mock(WorkerState.ILocalTransferCallback.class);
173+
DeserializingConnectionCallback callback = new DeserializingConnectionCallback(conf, context, transfer);
174+
175+
assertThrows(KryoException.class, () -> callback.recv(Collections.singletonList(taskMessage(truncated))));
176+
177+
verify(transfer, never()).transfer(any());
178+
assertEquals(0L, callback.getAndResetDeserializationFailures());
179+
}
180+
145181
@Test
146182
public void testJavaFallbackMissingClassDroppedAndBatchContinues() {
147183
Map<String, Object> conf = baseConf();

0 commit comments

Comments
 (0)