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 8001ca972 Zstd compression for thrift serialization (storm cluster
state) (#8653)
8001ca972 is described below
commit 8001ca97284141c5ad8c6c77ce73f7249cf48f34
Author: Gianluca Graziadei <[email protected]>
AuthorDate: Wed May 20 19:39:45 2026 +0200
Zstd compression for thrift serialization (storm cluster state) (#8653)
* zstd compression for thrift serialization
* rename zstd compression level parm
* remove zstd-jni compile scope
* remove zstd-jni compile scope
* delegation chain, manage exceptions, manage zip bomb, unit test coverage
* increase to 100MB default max uncompressed topology bytes
* docs
* add `ZstdBridgeThriftSerializationDelegateRoundTripTest`
---
conf/defaults.yaml | 5 +-
docs/Cluster-State-Serialization.md | 124 +++++++++++
docs/Serialization.md | 2 +
pom.xml | 6 +
storm-client/pom.xml | 8 +
storm-client/src/jvm/org/apache/storm/Config.java | 19 +-
.../GzipBridgeThriftSerializationDelegate.java | 2 +-
.../serialization/GzipSerializationDelegate.java | 35 ++-
.../GzipThriftSerializationDelegate.java | 12 +-
.../ZstdBridgeThriftSerializationDelegate.java | 50 +++++
.../ZstdThriftSerializationDelegate.java | 87 ++++++++
.../src/jvm/org/apache/storm/utils/Utils.java | 201 +++++++++++++++--
.../apache/storm/validation/ConfigValidation.java | 20 ++
.../GzipBridgeThriftSerializationDelegateTest.java | 2 +
...eThriftSerializationDelegateRoundTripTest.java} | 50 ++---
.../ZstdBridgeThriftSerializationDelegateTest.java | 245 +++++++++++++++++++++
.../ZstdThriftSerializationDelegateTest.java | 186 ++++++++++++++++
.../test/jvm/org/apache/storm/utils/UtilsTest.java | 201 +++++++++++++++++
.../daemon/supervisor/BasicContainerTest.java | 6 +-
19 files changed, 1191 insertions(+), 70 deletions(-)
diff --git a/conf/defaults.yaml b/conf/defaults.yaml
index bc9a5979d..2c3bb9e06 100644
--- a/conf/defaults.yaml
+++ b/conf/defaults.yaml
@@ -54,7 +54,10 @@ storm.nimbus.zookeeper.acls.fixup: true
storm.auth.simple-white-list.users: [ ]
storm.cluster.state.store: "org.apache.storm.cluster.ZKStateStorageFactory"
-storm.meta.serialization.delegate:
"org.apache.storm.serialization.GzipThriftSerializationDelegate"
+storm.meta.serialization.delegate:
"org.apache.storm.serialization.ZstdBridgeThriftSerializationDelegate"
+storm.compression.zstd.level: 3
+storm.compression.zstd.max.decompressed.bytes: 104857600
+storm.compression.gzip.max.decompressed.bytes: 104857600
storm.codedistributor.class:
"org.apache.storm.codedistributor.LocalFileSystemCodeDistributor"
storm.workers.artifacts.dir: "workers-artifacts"
storm.health.check.dir: "healthchecks"
diff --git a/docs/Cluster-State-Serialization.md
b/docs/Cluster-State-Serialization.md
new file mode 100644
index 000000000..8581448f4
--- /dev/null
+++ b/docs/Cluster-State-Serialization.md
@@ -0,0 +1,124 @@
+---
+title: Cluster State Serialization
+layout: documentation
+documentation: true
+---
+
+This page describes how Storm serializes the *meta* state it persists in
+ZooKeeper (and other configured state stores) such as topology assignments,
Nimbus
+summaries, `StormBase` records, log configs, credentials, worker heartbeats,
+profile requests, errors, etc.
+
+It is distinct from
+[tuple serialization](Serialization.html), which covers payloads exchanged
+between spouts and bolts at runtime via Kryo.
+
+## Background
+
+All cluster state writes go through `Utils.serialize(...)` /
+`Utils.deserialize(...)`, which in turn delegate to a pluggable
+`SerializationDelegate` selected by the
+`storm.meta.serialization.delegate` config.
+
+## Configuration
+
+| Key | Default | Range | Description |
+|---|---|---|---|
+| `storm.meta.serialization.delegate` |
`org.apache.storm.serialization.ZstdBridgeThriftSerializationDelegate` | any
`SerializationDelegate` impl | Class used to (de)serialize cluster state. |
+| `storm.compression.zstd.level` | `3` | `1`–`19` | Zstandard compression
level. Higher = smaller + slower. Levels 20–22 are rejected by the validator. |
+| `storm.compression.zstd.max.decompressed.bytes` | `104857600` (100 MiB) | `>
0` | Hard cap on the size of any zstd-decompressed payload. |
+| `storm.compression.gzip.max.decompressed.bytes` | `104857600` (100 MiB) | `>
0` | Hard cap on the size of any gzip-decompressed payload. Also enforced by
`GzipSerializationDelegate`. |
+
+## Choosing a delegate
+
+* **`ZstdBridgeThriftSerializationDelegate`** *(default)* — recommended.
+ Writes zstd, reads anything previously written. Use this unless you
+ have a specific reason not to.
+* **`ZstdThriftSerializationDelegate`** — pure zstd, refuses non-zstd
+ input. Only safe to deploy after every znode in your state store has
+ been rewritten by a bridge delegate (e.g. by submitting / killing each
+ topology, or by force-rewriting Nimbus state). Use only when you want
+ to *enforce* the new format.
+* **`GzipBridgeThriftSerializationDelegate`** — legacy default; still
+ available for clusters that want to roll forward without touching the
+ codec.
+* **`ThriftSerializationDelegate`** — raw Thrift.
+
+## Migration to Zstandard compression
+
+Starting with Apache Storm 3.X, Zstandard is supported as the default
+compression codec for cluster state, replacing gzip for better
+performance — faster compression and decompression at comparable or
+better ratios. Earlier versions used `GzipThriftSerializationDelegate`,
+wrapped by `GzipBridgeThriftSerializationDelegate` to allow rolling
+upgrades from clusters that had previously stored raw Thrift bytes; the
+new `ZstdBridgeThriftSerializationDelegate` plays the equivalent bridge
+role for the gzip to zstd transition.
+
+| Area | Gzip | Zstandard
|
+|---|---------------------------------------------------------|-----------------------------------------------------|
+| Default delegate | `GzipThriftSerializationDelegate` (via `GzipBridge...`) |
`ZstdBridgeThriftSerializationDelegate` |
+| Compression codec | gzip (`java.util.zip`)
| Zstandard (via `commons-compress` + `zstd-jni`) |
+| Decompression bound | none
| bounded (`BoundedInputStream`), default 100 MiB |
+| Format detection | gzip magic only |
gzip magic *and* zstd magic |
+| Config validation | none for compression
| `ZstdLevelValidator` (1–19), positive bounds checks |
+
+### Zstandard `SerializationDelegate` implementations
+
+* `ZstdThriftSerializationDelegate`: pure zstd Thrift codec. Serializes
+ any `TBase` with zstd at the configured level; deserialization
+ requires the input to begin with the zstd magic number
+ (`0xFD2FB528`).
+* `ZstdBridgeThriftSerializationDelegate`: the new default, implemented to
+ allow rolling upgrades from clusters that had previously stored payloads
+ as gzip-compressed. Always *writes* zstd. On read, dispatches based on a
+ magic-byte sniff:
+
+```
+ZstdBridgeThriftSerializationDelegate.deserialize(bytes)
+ ├── bytes starts with zstd magic (0xFD2FB528) delegates to
ZstdThriftSerializationDelegate
+ └── otherwise, delegates to
GzipBridgeThriftSerializationDelegate.deserialize(bytes)
+ ├── bytes starts with gzip magic (0x1F8B)
delegates to GzipThriftSerializationDelegate
+ └── otherwise delegates to
ThriftSerializationDelegate (raw Thrift)
+```
+
+This delegation chain is the key property that makes the new default
+rolling-upgrade safe: nodes running the new code can still read every
+older payload that may already exist in ZooKeeper, while new writes use
+zstd.
+
+### Zip-bomb protection
+
+`GzipUtils.decompress` and `ZstdUtils.decompress` (both in
+`org.apache.storm.utils.Utils`) wrap the decompressor stream in an Apache
+Commons `BoundedInputStream` with `maxCount` set to the configured cap.
+After draining the bounded stream, the underlying decompressor is probed
+with one extra `read()`; if any byte remains, the call fails with:
+
+```
+Decompression threshold exceeded! Possible security risk or invalid data size.
+```
+
+The same guard is applied to the legacy `GzipSerializationDelegate` (the
+non-Thrift Java-serialization variant).
+
+### Upgrading an existing cluster
+
+1. **Roll Nimbus and Supervisors onto the new build.** The bridge
+ delegate is the default, so no config change is required for a safe
+ upgrade.
+2. **(Optional) Tune `storm.compression.zstd.level`** if you want a
+ tighter compression / latency trade-off. Most state writes are
+ infrequent; level 3 is a good default.
+3. **(Optional) Tune `storm.compression.zstd.max.decompressed.bytes`** if
+ you legitimately persist payloads larger than 100 MiB. The cap
+ guards against malformed or hostile data, raise it deliberately.
+4. **(Optional) Switch to the strict `ZstdThriftSerializationDelegate`**
+ *only* after every legacy payload has been rewritten. The bridge
+ delegate is sufficient for the vast majority of deployments.
+
+### Dependencies
+
+The zstd codec is provided by Apache Commons Compress
+(`org.apache.commons:commons-compress`) backed by the
`com.github.luben:zstd-jni`
+native binding.
diff --git a/docs/Serialization.md b/docs/Serialization.md
index 4d2749105..0e7cc2e87 100644
--- a/docs/Serialization.md
+++ b/docs/Serialization.md
@@ -5,6 +5,8 @@ documentation: true
---
This page is about how the serialization system in Storm works for versions
0.6.0 and onwards. Storm used a different serialization system prior to 0.6.0
which is documented on [Serialization (prior to
0.6.0)](Serialization-\(prior-to-0.6.0\).html).
+> This page covers **tuple** serialization (data flowing between spouts and
bolts). For how Storm serializes the meta state it persists in ZooKeeper and
related configuration, see [Cluster State
Serialization](Cluster-State-Serialization.html).
+
Tuples can be comprised of objects of any types. Since Storm is a distributed
system, it needs to know how to serialize and deserialize objects when they're
passed between tasks.
Storm uses [Kryo](https://github.com/EsotericSoftware/kryo) for serialization.
Kryo is a flexible and fast serialization library that produces small
serializations.
diff --git a/pom.xml b/pom.xml
index 326bf4543..d98a27c85 100644
--- a/pom.xml
+++ b/pom.xml
@@ -82,6 +82,7 @@
<!-- dependency versions -->
<commons-compress.version>1.28.0</commons-compress.version>
+ <zstd-jni.version>1.5.7-8</zstd-jni.version>
<commons-io.version>2.22.0</commons-io.version>
<commons-lang3.version>3.20.0</commons-lang3.version>
<commons-exec.version>1.6.0</commons-exec.version>
@@ -514,6 +515,11 @@
<artifactId>commons-compress</artifactId>
<version>${commons-compress.version}</version>
</dependency>
+ <dependency>
+ <groupId>com.github.luben</groupId>
+ <artifactId>zstd-jni</artifactId>
+ <version>${zstd-jni.version}</version>
+ </dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-exec</artifactId>
diff --git a/storm-client/pom.xml b/storm-client/pom.xml
index 62d6c20d9..3a7ee315d 100644
--- a/storm-client/pom.xml
+++ b/storm-client/pom.xml
@@ -135,6 +135,14 @@
<artifactId>curator-test</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-compress</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.github.luben</groupId>
+ <artifactId>zstd-jni</artifactId>
+ </dependency>
</dependencies>
<build>
diff --git a/storm-client/src/jvm/org/apache/storm/Config.java
b/storm-client/src/jvm/org/apache/storm/Config.java
index 62770b62e..0d7046585 100644
--- a/storm-client/src/jvm/org/apache/storm/Config.java
+++ b/storm-client/src/jvm/org/apache/storm/Config.java
@@ -1485,7 +1485,24 @@ public class Config extends HashMap<String, Object> {
*/
@IsString
public static final String STORM_META_SERIALIZATION_DELEGATE =
"storm.meta.serialization.delegate";
-
+ /**
+ * GZIP max decompression bytes. Defaults to 104857600 (100MB).
+ */
+ @IsPositiveNumber(includeZero = false)
+ public static final String STORM_COMPRESSION_GZIP_MAX_DECOMPRESSED_BYTES =
"storm.compression.gzip.max.decompressed.bytes";
+ /**
+ * Zstandard compression level.
+ * Supported range: 1 to 19. Default: 3.
+ * <b>Prohibited:</b> Levels 20-22 (Ultra mode) are not allowed as they
+ * require dramatically more working memory per call.
+ */
+ @CustomValidator(validatorClass =
ConfigValidation.ZstdLevelValidator.class)
+ public static final String STORM_COMPRESSION_ZSTD_LEVEL =
"storm.compression.zstd.level";
+ /**
+ * Zstandard max decompression bytes. Defaults to 104857600 (100MB).
+ */
+ @IsPositiveNumber(includeZero = false)
+ public static final String STORM_COMPRESSION_ZSTD_MAX_DECOMPRESSED_BYTES =
"storm.compression.zstd.max.decompressed.bytes";
/**
* Configure the topology metrics reporters to be used on workers.
*/
diff --git
a/storm-client/src/jvm/org/apache/storm/serialization/GzipBridgeThriftSerializationDelegate.java
b/storm-client/src/jvm/org/apache/storm/serialization/GzipBridgeThriftSerializationDelegate.java
index bc9661124..67ae15ada 100644
---
a/storm-client/src/jvm/org/apache/storm/serialization/GzipBridgeThriftSerializationDelegate.java
+++
b/storm-client/src/jvm/org/apache/storm/serialization/GzipBridgeThriftSerializationDelegate.java
@@ -1,4 +1,4 @@
-/**
+/*
* 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
diff --git
a/storm-client/src/jvm/org/apache/storm/serialization/GzipSerializationDelegate.java
b/storm-client/src/jvm/org/apache/storm/serialization/GzipSerializationDelegate.java
index 9c4045854..d12c2b018 100644
---
a/storm-client/src/jvm/org/apache/storm/serialization/GzipSerializationDelegate.java
+++
b/storm-client/src/jvm/org/apache/storm/serialization/GzipSerializationDelegate.java
@@ -1,4 +1,4 @@
-/**
+/*
* 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
@@ -20,15 +20,22 @@ import java.io.ObjectOutputStream;
import java.util.Map;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
+import org.apache.storm.Config;
+import org.apache.storm.shade.org.apache.commons.io.input.BoundedInputStream;
+import org.apache.storm.utils.ObjectReader;
/**
* Note, this assumes it's deserializing a gzip byte stream, and will err if
it encounters any other serialization.
*/
public class GzipSerializationDelegate implements SerializationDelegate {
+ private static final int DEFAULT_MAX_DECOMPRESSED_BYTES = 100 * 1024 *
1024;
+ private int maxDecompressedBytes;
+
@Override
public void prepare(Map<String, Object> topoConf) {
- // No-op
+ this.maxDecompressedBytes =
ObjectReader.getInt(topoConf.getOrDefault(Config.STORM_COMPRESSION_GZIP_MAX_DECOMPRESSED_BYTES,
+ DEFAULT_MAX_DECOMPRESSED_BYTES));
}
@Override
@@ -47,17 +54,21 @@ public class GzipSerializationDelegate implements
SerializationDelegate {
@Override
public <T> T deserialize(byte[] bytes, Class<T> clazz) {
- try {
- ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
- GZIPInputStream gis = new GZIPInputStream(bis);
- ObjectInputStream ois = new ObjectInputStream(gis);
+ try (ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
+ GZIPInputStream gis = new GZIPInputStream(bis);
+ BoundedInputStream lis = BoundedInputStream.builder()
+ .setMaxCount(this.maxDecompressedBytes)
+ .setInputStream(gis)
+ .setPropagateClose(true)
+ .get();
+ ObjectInputStream ois = new ObjectInputStream(lis)) {
Object ret = ois.readObject();
- ois.close();
- return (T) ret;
- } catch (IOException ioe) {
- throw new RuntimeException(ioe);
- } catch (ClassNotFoundException e) {
- throw new RuntimeException(e);
+ if (gis.read() != -1) {
+ throw new IOException("Decompression threshold exceeded!
Possible security risk or invalid data size.");
+ }
+ return clazz.cast(ret);
+ } catch (IOException | ClassNotFoundException e) {
+ throw new RuntimeException("Deserialization failed: " +
e.getMessage(), e);
}
}
}
diff --git
a/storm-client/src/jvm/org/apache/storm/serialization/GzipThriftSerializationDelegate.java
b/storm-client/src/jvm/org/apache/storm/serialization/GzipThriftSerializationDelegate.java
index f518628a8..ae3bc7ac1 100644
---
a/storm-client/src/jvm/org/apache/storm/serialization/GzipThriftSerializationDelegate.java
+++
b/storm-client/src/jvm/org/apache/storm/serialization/GzipThriftSerializationDelegate.java
@@ -19,10 +19,12 @@
package org.apache.storm.serialization;
import java.util.Map;
+import org.apache.storm.Config;
import org.apache.storm.thrift.TBase;
import org.apache.storm.thrift.TDeserializer;
import org.apache.storm.thrift.TException;
import org.apache.storm.thrift.TSerializer;
+import org.apache.storm.utils.ObjectReader;
import org.apache.storm.utils.Utils;
/**
@@ -30,15 +32,19 @@ import org.apache.storm.utils.Utils;
*/
public class GzipThriftSerializationDelegate implements SerializationDelegate {
+ private static final int DEFAULT_MAX_DECOMPRESSED_BYTES = 100 * 1024 *
1024;
+ private int maxDecompressedBytes;
+
@Override
public void prepare(Map<String, Object> topoConf) {
- // No-op
+ this.maxDecompressedBytes =
ObjectReader.getInt(topoConf.getOrDefault(Config.STORM_COMPRESSION_GZIP_MAX_DECOMPRESSED_BYTES,
+ DEFAULT_MAX_DECOMPRESSED_BYTES));
}
@Override
public byte[] serialize(Object object) {
try {
- return Utils.gzip(new TSerializer().serialize((TBase) object));
+ return Utils.GzipUtils.compress(new
TSerializer().serialize((TBase) object));
} catch (TException e) {
throw new RuntimeException(e);
}
@@ -48,7 +54,7 @@ public class GzipThriftSerializationDelegate implements
SerializationDelegate {
public <T> T deserialize(byte[] bytes, Class<T> clazz) {
try {
TBase instance = (TBase) clazz.newInstance();
- new TDeserializer().deserialize(instance, Utils.gunzip(bytes));
+ new TDeserializer().deserialize(instance,
Utils.GzipUtils.decompress(bytes, this.maxDecompressedBytes));
return (T) instance;
} catch (Exception e) {
throw new RuntimeException(e);
diff --git
a/storm-client/src/jvm/org/apache/storm/serialization/ZstdBridgeThriftSerializationDelegate.java
b/storm-client/src/jvm/org/apache/storm/serialization/ZstdBridgeThriftSerializationDelegate.java
new file mode 100644
index 000000000..a3484b7fd
--- /dev/null
+++
b/storm-client/src/jvm/org/apache/storm/serialization/ZstdBridgeThriftSerializationDelegate.java
@@ -0,0 +1,50 @@
+/*
+ * 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.serialization;
+
+import java.util.Map;
+import org.apache.storm.utils.Utils;
+
+/**
+ * Always writes Zstd out, but tests incoming bytes to determine the format.
+ * If Zstd magic is found, it uses {@link ZstdThriftSerializationDelegate}.
+ * If not, it falls back to {@link ThriftSerializationDelegate} for raw Thrift.
+ */
+public class ZstdBridgeThriftSerializationDelegate implements
SerializationDelegate {
+
+ private final GzipBridgeThriftSerializationDelegate defaultDelegate = new
GzipBridgeThriftSerializationDelegate();
+ private final ZstdThriftSerializationDelegate zstdDelegate = new
ZstdThriftSerializationDelegate();
+
+ @Override
+ public void prepare(Map<String, Object> topoConf) {
+ defaultDelegate.prepare(topoConf);
+ zstdDelegate.prepare(topoConf);
+ }
+
+ @Override
+ public byte[] serialize(Object object) {
+ // Always compress new data with Zstd
+ return zstdDelegate.serialize(object);
+ }
+
+ @Override
+ public <T> T deserialize(byte[] bytes, Class<T> clazz) {
+ if (Utils.ZstdUtils.isZstd(bytes)) {
+ return zstdDelegate.deserialize(bytes, clazz);
+ } else {
+ // Fallback to ZstdBridgeThriftSerializationDelegate
+ // it delegates to the proper SerializationDelegate
(GzipThriftSerializationDelegate or ThriftSerializationDelegate)
+ return defaultDelegate.deserialize(bytes, clazz);
+ }
+ }
+}
diff --git
a/storm-client/src/jvm/org/apache/storm/serialization/ZstdThriftSerializationDelegate.java
b/storm-client/src/jvm/org/apache/storm/serialization/ZstdThriftSerializationDelegate.java
new file mode 100644
index 000000000..d3b990a7b
--- /dev/null
+++
b/storm-client/src/jvm/org/apache/storm/serialization/ZstdThriftSerializationDelegate.java
@@ -0,0 +1,87 @@
+/*
+ * 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.serialization;
+
+import java.util.Map;
+import org.apache.storm.Config;
+import org.apache.storm.thrift.TBase;
+import org.apache.storm.thrift.TDeserializer;
+import org.apache.storm.thrift.TException;
+import org.apache.storm.thrift.TSerializer;
+import org.apache.storm.thrift.transport.TTransportException;
+import org.apache.storm.utils.ObjectReader;
+import org.apache.storm.utils.Utils;
+
+/**
+ * Note, this assumes it's deserializing a zstd byte stream, and will err if
it encounters any other serialization.
+ */
+public class ZstdThriftSerializationDelegate implements SerializationDelegate {
+
+ private static final int DEFAULT_MAX_DECOMPRESSED_BYTES = 100 * 1024 *
1024;
+ private static final int DEFAULT_ZSTD_COMPRESSION_LEVEL = 3;
+
+ private int zstdCompressionLevel;
+ private int maxDecompressedBytes;
+
+ @Override
+ public void prepare(Map<String, Object> topoConf) {
+ this.zstdCompressionLevel =
ObjectReader.getInt(topoConf.getOrDefault(Config.STORM_COMPRESSION_ZSTD_LEVEL,
+ DEFAULT_ZSTD_COMPRESSION_LEVEL));
+ this.maxDecompressedBytes =
ObjectReader.getInt(topoConf.getOrDefault(Config.STORM_COMPRESSION_ZSTD_MAX_DECOMPRESSED_BYTES,
+ DEFAULT_MAX_DECOMPRESSED_BYTES));
+ }
+
+ @Override
+ public byte[] serialize(Object object) {
+ if (!(object instanceof TBase)) {
+ throw new IllegalArgumentException("Object must be an instance of
TBase");
+ }
+ try {
+ TSerializer serializer = new TSerializer();
+ byte[] thriftData = serializer.serialize((TBase<?, ?>) object);
+ return Utils.ZstdUtils.compress(thriftData,
this.zstdCompressionLevel);
+ } catch (TTransportException e) {
+ throw new RuntimeException("Failed to initialize Thrift
Serializer", e);
+ } catch (TException e) {
+ throw new RuntimeException("Failed to serialize Thrift object", e);
+ }
+ }
+
+ @Override
+ public <T> T deserialize(byte[] bytes, Class<T> clazz) {
+ if (!Utils.ZstdUtils.isZstd(bytes)) {
+ throw new RuntimeException(
+ String.format("Cannot deserialize [%s]. Expected zstd
compressed bytes, but received unknown format.",
+ clazz.getSimpleName())
+ );
+ }
+ try {
+ TDeserializer deserializer = new TDeserializer();
+ byte[] decompressed = Utils.ZstdUtils.decompress(bytes,
this.maxDecompressedBytes);
+ TBase<?, ?> instance =
clazz.asSubclass(TBase.class).getDeclaredConstructor().newInstance();
+ deserializer.deserialize(instance, decompressed);
+ return (T) instance;
+ } catch (TTransportException e) {
+ throw new RuntimeException("Failed to initialize Thrift
Deserializer", e);
+ } catch (ReflectiveOperationException | TException e) {
+ throw new RuntimeException("Failed to deserialize bytes to " +
clazz.getName(), e);
+ }
+ }
+
+}
diff --git a/storm-client/src/jvm/org/apache/storm/utils/Utils.java
b/storm-client/src/jvm/org/apache/storm/utils/Utils.java
index b639863f8..7ba3298fe 100644
--- a/storm-client/src/jvm/org/apache/storm/utils/Utils.java
+++ b/storm-client/src/jvm/org/apache/storm/utils/Utils.java
@@ -34,6 +34,8 @@ import java.io.OutputStreamWriter;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.lang.Thread.UncaughtExceptionHandler;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.VarHandle;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.net.InetAddress;
@@ -43,6 +45,7 @@ import java.net.URLDecoder;
import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
@@ -72,6 +75,9 @@ import java.util.zip.GZIPOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import javax.security.auth.Subject;
+import
org.apache.commons.compress.compressors.zstandard.ZstdCompressorInputStream;
+import
org.apache.commons.compress.compressors.zstandard.ZstdCompressorOutputStream;
+import org.apache.commons.io.IOUtils;
import org.apache.storm.Config;
import org.apache.storm.blobstore.BlobStore;
import org.apache.storm.blobstore.ClientBlobStore;
@@ -96,6 +102,7 @@ import org.apache.storm.shade.com.google.common.collect.Maps;
import org.apache.storm.shade.net.minidev.json.JSONValue;
import org.apache.storm.shade.net.minidev.json.parser.ParseException;
import org.apache.storm.shade.org.apache.commons.io.FileUtils;
+import org.apache.storm.shade.org.apache.commons.io.input.BoundedInputStream;
import
org.apache.storm.shade.org.apache.commons.io.input.ClassLoaderObjectInputStream;
import org.apache.storm.shade.org.apache.commons.lang3.StringUtils;
import org.apache.storm.shade.org.apache.zookeeper.ZooDefs;
@@ -930,33 +937,181 @@ public class Utils {
return ret;
}
- public static byte[] gzip(byte[] data) {
- try {
- ByteArrayOutputStream bos = new ByteArrayOutputStream();
- GZIPOutputStream out = new GZIPOutputStream(bos);
- out.write(data);
- out.close();
- return bos.toByteArray();
- } catch (IOException e) {
- throw new RuntimeException(e);
+ /**
+ * Static utility class for GZIP compression and decompression.
+ */
+ public static final class GzipUtils {
+
+ // GZIP magic number (first two bytes): 0x1F8B.
+ private static final byte GZIP_MAGIC_0 = (byte) 0x1F;
+ private static final byte GZIP_MAGIC_1 = (byte) 0x8B;
+
+ private static final int BUFFER_SIZE = 64 * 1024;
+
+ /**
+ * Private constructor to prevent instantiation.
+ * @throws UnsupportedOperationException if an attempt is made to
instantiate this class.
+ */
+ private GzipUtils() {
+ throw new UnsupportedOperationException("Utility class should not
be instantiated.");
+ }
+
+ /**
+ * Compresses the provided byte array using GZIP.
+ *
+ * @param data the raw byte array to compress.
+ * @return a compressed byte array, or the original array if
null/empty.
+ * @throws RuntimeException wrapping an {@link IOException} if the
compression fails.
+ */
+ public static byte[] compress(byte[] data) {
+ if (data == null || data.length == 0) {
+ return data;
+ }
+ try (ByteArrayOutputStream bos = new
ByteArrayOutputStream(data.length);
+ GZIPOutputStream gzipOut = new GZIPOutputStream(bos,
BUFFER_SIZE)) {
+ gzipOut.write(data);
+ gzipOut.finish();
+ return bos.toByteArray();
+ } catch (IOException e) {
+ throw new RuntimeException("GZIP compression failed", e);
+ }
+ }
+
+ /**
+ * Decompresses a GZIP-compressed byte array.
+ *
+ * @param data the compressed byte array (GZIP frame).
+ * @param maxDecompressedBytes maximum number of bytes allowed after
decompression,
+ * as a safeguard against zip-bomb attacks.
+ * @return the original decompressed byte array, or the input if
null/empty.
+ * @throws RuntimeException wrapping an {@link IOException} if the
decompression fails,
+ * if the data is not a valid GZIP stream, or
if the decompressed
+ * size exceeds {@code maxDecompressedBytes}.
+ */
+ public static byte[] decompress(byte[] data, int maxDecompressedBytes)
{
+ if (data == null || data.length == 0) {
+ return data;
+ }
+ try (ByteArrayInputStream bis = new ByteArrayInputStream(data);
+ GZIPInputStream gzipIn = new GZIPInputStream(bis,
BUFFER_SIZE);
+ BoundedInputStream limitedIn = BoundedInputStream.builder()
+ .setInputStream(gzipIn)
+ .setMaxCount(maxDecompressedBytes)
+ .get();
+ ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
+ IOUtils.copy(limitedIn, bos);
+ if (gzipIn.read() != -1) {
+ throw new IOException("Decompression threshold exceeded!
Possible security risk or invalid data size.");
+ }
+ return bos.toByteArray();
+ } catch (IOException e) {
+ throw new RuntimeException("GZIP decompression failed", e);
+ }
+ }
+
+ /**
+ * Checks the first 2 bytes of the array against the GZIP magic number
(0x1F8B).
+ *
+ * @param bytes the data payload.
+ * @return {@code true} if the payload starts with a valid GZIP
header, {@code false} otherwise.
+ */
+ public static boolean isGzip(byte[] bytes) {
+ if (bytes == null || bytes.length < 2) {
+ return false;
+ }
+ return bytes[0] == GZIP_MAGIC_0 && bytes[1] == GZIP_MAGIC_1;
}
}
- public static byte[] gunzip(byte[] data) {
- try {
- ByteArrayOutputStream bos = new ByteArrayOutputStream();
- ByteArrayInputStream bis = new ByteArrayInputStream(data);
- GZIPInputStream in = new GZIPInputStream(bis);
- byte[] buffer = new byte[1024];
- int len = 0;
- while ((len = in.read(buffer)) >= 0) {
- bos.write(buffer, 0, len);
+ /**
+ * Static utility class for Zstandard (Zstd) compression and decompression.
+ */
+ public static final class ZstdUtils {
+
+ /**
+ * Zstandard magic number 0xFD2FB528.
+ */
+ private static final int ZSTD_MAGIC_INT = 0xFD2FB528;
+ private static final VarHandle INT_HANDLE =
MethodHandles.byteArrayViewVarHandle(int[].class, ByteOrder.LITTLE_ENDIAN);
+ private static final int BUFFER_SIZE = 64 * 1024;
+
+ /**
+ * Private constructor to prevent instantiation.
+ * @throws UnsupportedOperationException if an attempt is made to
instantiate this class.
+ */
+ private ZstdUtils() {
+ throw new UnsupportedOperationException("Utility class should not
be instantiated.");
+ }
+
+ /**
+ * Compresses the provided byte array using Zstandard.
+ *
+ * <p>The output includes the standard Zstandard frame header, making
it
+ * self-describing for the decompression phase.</p>
+ *
+ * @param data the raw byte array to compress.
+ * @param compressionLevel the zstd compression level.
+ * @return a compressed byte array, or the original array if
null/empty.
+ * @throws RuntimeException wrapping an {@link IOException} if the
compression fails.
+ */
+ public static byte[] compress(byte[] data, int compressionLevel) {
+ if (data == null || data.length == 0) {
+ return data;
}
- in.close();
- bos.close();
- return bos.toByteArray();
- } catch (IOException e) {
- throw new RuntimeException(e);
+
+ try (ByteArrayOutputStream bos = new
ByteArrayOutputStream(data.length)) {
+ try (ZstdCompressorOutputStream zstdOut =
ZstdCompressorOutputStream.builder()
+ .setOutputStream(bos)
+ .setBufferSize(BUFFER_SIZE) // impacts on compression
ratio
+ .setLevel(compressionLevel)
+ .get()) {
+ zstdOut.write(data);
+ zstdOut.finish();
+ }
+ return bos.toByteArray();
+ } catch (IOException e) {
+ throw new RuntimeException("Zstd compression failed", e);
+ }
+ }
+
+ /**
+ * Decompresses a Zstandard-compressed byte array.
+ *
+ * @param data the compressed byte array (Zstd frame).
+ * @return the original decompressed byte array, or the input if
null/empty.
+ * @throws RuntimeException wrapping an {@link IOException} if the
decompression fails
+ * or if the data is not a valid Zstd frame.
+ */
+ public static byte[] decompress(byte[] data, int maxDecompressedBytes)
{
+ if (data == null || data.length == 0) {
+ return data;
+ }
+
+ try (ByteArrayInputStream bis = new ByteArrayInputStream(data);
+ ZstdCompressorInputStream zstdIn = new
ZstdCompressorInputStream(bis);
+ BoundedInputStream limitedIn =
BoundedInputStream.builder().setInputStream(zstdIn).setMaxCount(maxDecompressedBytes).get();
+ ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
+ IOUtils.copy(limitedIn, bos);
+ if (zstdIn.read() != -1) {
+ throw new IOException("Decompression threshold exceeded!
Possible security risk or invalid data size.");
+ }
+ return bos.toByteArray();
+ } catch (IOException e) {
+ throw new RuntimeException("Zstd decompression failed", e);
+ }
+ }
+
+ /**
+ * Checks the first 4 bytes of the array against the Zstd Magic Number.
+ *
+ * @param bytes The data payload
+ * @return true if the payload contains a valid zstd header, false
otherwise.
+ */
+ public static boolean isZstd(byte[] bytes) {
+ if (bytes == null || bytes.length < 4) {
+ return false;
+ }
+ return (int) INT_HANDLE.get(bytes, 0) == ZSTD_MAGIC_INT;
}
}
diff --git
a/storm-client/src/jvm/org/apache/storm/validation/ConfigValidation.java
b/storm-client/src/jvm/org/apache/storm/validation/ConfigValidation.java
index 0206ec458..7f2d2462c 100644
--- a/storm-client/src/jvm/org/apache/storm/validation/ConfigValidation.java
+++ b/storm-client/src/jvm/org/apache/storm/validation/ConfigValidation.java
@@ -804,6 +804,26 @@ public class ConfigValidation {
}
}
+ public static class ZstdLevelValidator extends Validator {
+ private static final int MIN_LEVEL = 1;
+ private static final int MAX_LEVEL = 19;
+
+ @Override
+ public void validateField(String name, Object o) {
+ if (o == null) {
+ return;
+ }
+ SimpleTypeValidator.validateField(name, Integer.class, o);
+ int level = (Integer) o;
+ if (level < MIN_LEVEL || level > MAX_LEVEL) {
+ throw new IllegalArgumentException(
+ String.format("Field '%s' is invalid: %d. Zstd compression
level must be between %d and %d.",
+ name, level, MIN_LEVEL, MAX_LEVEL)
+ );
+ }
+ }
+ }
+
public static class EventLoggerRegistryValidator extends Validator {
@Override
diff --git
a/storm-client/test/jvm/org/apache/storm/serialization/GzipBridgeThriftSerializationDelegateTest.java
b/storm-client/test/jvm/org/apache/storm/serialization/GzipBridgeThriftSerializationDelegateTest.java
index 7a5634dcc..47c9147d5 100644
---
a/storm-client/test/jvm/org/apache/storm/serialization/GzipBridgeThriftSerializationDelegateTest.java
+++
b/storm-client/test/jvm/org/apache/storm/serialization/GzipBridgeThriftSerializationDelegateTest.java
@@ -12,6 +12,7 @@
package org.apache.storm.serialization;
+import java.util.Collections;
import org.apache.storm.generated.GlobalStreamId;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -25,6 +26,7 @@ public class GzipBridgeThriftSerializationDelegateTest {
@BeforeEach
public void setUp() throws Exception {
testDelegate = new GzipBridgeThriftSerializationDelegate();
+ testDelegate.prepare(Collections.emptyMap());
}
@Test
diff --git
a/storm-client/test/jvm/org/apache/storm/serialization/GzipBridgeThriftSerializationDelegateTest.java
b/storm-client/test/jvm/org/apache/storm/serialization/ZstdBridgeThriftSerializationDelegateRoundTripTest.java
similarity index 56%
copy from
storm-client/test/jvm/org/apache/storm/serialization/GzipBridgeThriftSerializationDelegateTest.java
copy to
storm-client/test/jvm/org/apache/storm/serialization/ZstdBridgeThriftSerializationDelegateRoundTripTest.java
index 7a5634dcc..eac87d844 100644
---
a/storm-client/test/jvm/org/apache/storm/serialization/GzipBridgeThriftSerializationDelegateTest.java
+++
b/storm-client/test/jvm/org/apache/storm/serialization/ZstdBridgeThriftSerializationDelegateRoundTripTest.java
@@ -1,4 +1,4 @@
-/**
+/*
* 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
@@ -12,54 +12,52 @@
package org.apache.storm.serialization;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.Collections;
import org.apache.storm.generated.GlobalStreamId;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-
-public class GzipBridgeThriftSerializationDelegateTest {
- SerializationDelegate testDelegate;
+class ZstdBridgeThriftSerializationDelegateRoundTripTest {
+ SerializationDelegate bridge;
@BeforeEach
- public void setUp() throws Exception {
- testDelegate = new GzipBridgeThriftSerializationDelegate();
+ public void setUp() {
+ bridge = new ZstdBridgeThriftSerializationDelegate();
+ bridge.prepare(Collections.emptyMap());
}
@Test
public void testDeserialize_readingFromGzip() {
GlobalStreamId id = new GlobalStreamId("first", "second");
-
byte[] serialized = new
GzipThriftSerializationDelegate().serialize(id);
- GlobalStreamId id2 = testDelegate.deserialize(serialized,
GlobalStreamId.class);
+ GlobalStreamId out = bridge.deserialize(serialized,
GlobalStreamId.class);
- assertEquals(id2.get_componentId(), id.get_componentId());
- assertEquals(id2.get_streamId(), id.get_streamId());
+ assertEquals(id.get_componentId(), out.get_componentId());
+ assertEquals(id.get_streamId(), out.get_streamId());
}
@Test
- public void testDeserialize_readingFromGzipBridge() {
- GlobalStreamId id = new GlobalStreamId("first", "second");
-
- byte[] serialized = new
GzipBridgeThriftSerializationDelegate().serialize(id);
+ public void testDeserialize_readingFromRawThrift() {
+ GlobalStreamId id = new GlobalStreamId("A", "B");
+ byte[] serialized = new ThriftSerializationDelegate().serialize(id);
- GlobalStreamId id2 = testDelegate.deserialize(serialized,
GlobalStreamId.class);
+ GlobalStreamId out = bridge.deserialize(serialized,
GlobalStreamId.class);
- assertEquals(id2.get_componentId(), id.get_componentId());
- assertEquals(id2.get_streamId(), id.get_streamId());
+ assertEquals(id.get_componentId(), out.get_componentId());
+ assertEquals(id.get_streamId(), out.get_streamId());
}
@Test
- public void testDeserialize_readingFromDefault() {
- GlobalStreamId id = new GlobalStreamId("A", "B");
-
- byte[] serialized = new ThriftSerializationDelegate().serialize(id);
+ public void testRoundTrip_throughBridge() {
+ GlobalStreamId id = new GlobalStreamId("x", "y");
+ byte[] serialized = bridge.serialize(id);
- GlobalStreamId id2 = testDelegate.deserialize(serialized,
GlobalStreamId.class);
+ GlobalStreamId out = bridge.deserialize(serialized,
GlobalStreamId.class);
- assertEquals(id2.get_componentId(), id.get_componentId());
- assertEquals(id2.get_streamId(), id.get_streamId());
+ assertEquals(id.get_componentId(), out.get_componentId());
+ assertEquals(id.get_streamId(), out.get_streamId());
}
}
diff --git
a/storm-client/test/jvm/org/apache/storm/serialization/ZstdBridgeThriftSerializationDelegateTest.java
b/storm-client/test/jvm/org/apache/storm/serialization/ZstdBridgeThriftSerializationDelegateTest.java
new file mode 100644
index 000000000..13bf7515c
--- /dev/null
+++
b/storm-client/test/jvm/org/apache/storm/serialization/ZstdBridgeThriftSerializationDelegateTest.java
@@ -0,0 +1,245 @@
+/*
+ * 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.serialization;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import java.lang.reflect.Field;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.storm.Config;
+import org.apache.storm.utils.Utils;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+
+@ExtendWith(MockitoExtension.class)
+class ZstdBridgeThriftSerializationDelegateTest {
+
+ @Mock
+ private GzipBridgeThriftSerializationDelegate defaultDelegate;
+
+ @Mock
+ private ZstdThriftSerializationDelegate zstdDelegate;
+
+ private ZstdBridgeThriftSerializationDelegate delegate;
+
+ private static final Map<String, Object> TOPO_CONF =
Collections.emptyMap();
+
+ private static final byte[] ZSTD_BYTES = {(byte) 0x28, (byte) 0xB5,
(byte) 0x2F, (byte) 0xFD, 0x00};
+ private static final byte[] PLAIN_BYTES = {0x00, 0x01, 0x02, 0x03, 0x04};
+ private static final byte[] RESULT_BYTES = {(byte) 0xAA, (byte) 0xBB};
+
+
+ @BeforeEach
+ void setUp() throws Exception {
+ delegate = new ZstdBridgeThriftSerializationDelegate();
+
+ Field defaultField =
ZstdBridgeThriftSerializationDelegate.class.getDeclaredField("defaultDelegate");
+ defaultField.setAccessible(true);
+ defaultField.set(delegate, defaultDelegate);
+
+ Field zstdField =
ZstdBridgeThriftSerializationDelegate.class.getDeclaredField("zstdDelegate");
+ zstdField.setAccessible(true);
+ zstdField.set(delegate, zstdDelegate);
+ }
+
+ @Test
+ void prepare_delegatesToBothDelegates() {
+ Map<String, Object> conf = new HashMap<>();
+ conf.put("key", "value");
+
+ // user defined conf
+ conf.put(Config.STORM_COMPRESSION_ZSTD_LEVEL, 3);
+ conf.put(Config.STORM_COMPRESSION_ZSTD_MAX_DECOMPRESSED_BYTES, 2 *
1024 * 1024);
+ conf.put(Config.STORM_COMPRESSION_GZIP_MAX_DECOMPRESSED_BYTES, 2 *
1024 * 1024);
+
+ delegate.prepare(conf);
+
+ verify(defaultDelegate).prepare(conf);
+ verify(zstdDelegate).prepare(conf);
+ verifyNoMoreInteractions(defaultDelegate, zstdDelegate);
+ }
+
+ @Test
+ void prepare_emptyConf_doesNotThrow() {
+ assertDoesNotThrow(() -> delegate.prepare(TOPO_CONF));
+ verify(defaultDelegate).prepare(TOPO_CONF);
+ verify(zstdDelegate).prepare(TOPO_CONF);
+ }
+
+ @Test
+ void prepare_nullConf_propagatesToBothDelegates() {
+ delegate.prepare(null);
+ verify(defaultDelegate).prepare(null);
+ verify(zstdDelegate).prepare(null);
+ }
+
+ @Test
+ void serialize_alwaysUsesZstdDelegate() {
+ Object payload = new Object();
+ when(zstdDelegate.serialize(payload)).thenReturn(RESULT_BYTES);
+
+ byte[] result = delegate.serialize(payload);
+
+ assertArrayEquals(RESULT_BYTES, result);
+ verify(zstdDelegate).serialize(payload);
+ verifyNoInteractions(defaultDelegate);
+ }
+
+ @Test
+ void serialize_nullObject_delegatedToZstd() {
+ when(zstdDelegate.serialize(null)).thenReturn(RESULT_BYTES);
+
+ byte[] result = delegate.serialize(null);
+
+ assertArrayEquals(RESULT_BYTES, result);
+ verify(zstdDelegate).serialize(null);
+ verifyNoInteractions(defaultDelegate);
+ }
+
+ @Test
+ void serialize_neverUsesDefaultDelegate() {
+ when(zstdDelegate.serialize(any())).thenReturn(RESULT_BYTES);
+
+ delegate.serialize("anything");
+
+ verifyNoInteractions(defaultDelegate);
+ }
+
+ @Test
+ void deserialize_zstdMagic_usesZstdDelegate() {
+ try (MockedStatic<Utils.ZstdUtils> mocked =
mockStatic(Utils.ZstdUtils.class)) {
+ mocked.when(() ->
Utils.ZstdUtils.isZstd(ZSTD_BYTES)).thenReturn(true);
+ when(zstdDelegate.deserialize(ZSTD_BYTES,
String.class)).thenReturn("zstd-result");
+
+ String result = delegate.deserialize(ZSTD_BYTES, String.class);
+
+ assertEquals("zstd-result", result);
+ verify(zstdDelegate).deserialize(ZSTD_BYTES, String.class);
+ verifyNoInteractions(defaultDelegate);
+ }
+ }
+
+ @Test
+ void deserialize_zstdMagic_doesNotTouchDefaultDelegate() {
+ try (MockedStatic<Utils.ZstdUtils> mocked =
mockStatic(Utils.ZstdUtils.class)) {
+ mocked.when(() ->
Utils.ZstdUtils.isZstd(ZSTD_BYTES)).thenReturn(true);
+ when(zstdDelegate.deserialize(any(), any())).thenReturn(new
Object());
+
+ delegate.deserialize(ZSTD_BYTES, Object.class);
+
+ verifyNoInteractions(defaultDelegate);
+ }
+ }
+
+ // fallback
+ @Test
+ void deserialize_noZstdMagic_usesDefaultDelegate() {
+ try (MockedStatic<Utils.ZstdUtils> mocked =
mockStatic(Utils.ZstdUtils.class)) {
+ mocked.when(() ->
Utils.ZstdUtils.isZstd(PLAIN_BYTES)).thenReturn(false);
+ when(defaultDelegate.deserialize(PLAIN_BYTES,
String.class)).thenReturn("plain-result");
+
+ String result = delegate.deserialize(PLAIN_BYTES, String.class);
+
+ assertEquals("plain-result", result);
+ verify(defaultDelegate).deserialize(PLAIN_BYTES, String.class);
+ verifyNoInteractions(zstdDelegate);
+ }
+ }
+
+ @Test
+ void deserialize_noZstdMagic_doesNotTouchZstdDelegate() {
+ try (MockedStatic<Utils.ZstdUtils> mocked =
mockStatic(Utils.ZstdUtils.class)) {
+ mocked.when(() ->
Utils.ZstdUtils.isZstd(PLAIN_BYTES)).thenReturn(false);
+ when(defaultDelegate.deserialize(any(), any())).thenReturn(new
Object());
+
+ delegate.deserialize(PLAIN_BYTES, Object.class);
+
+ verifyNoInteractions(zstdDelegate);
+ }
+ }
+
+ @Test
+ void deserialize_nullBytes_routedToDefaultDelegate() {
+ try (MockedStatic<Utils.ZstdUtils> mocked =
mockStatic(Utils.ZstdUtils.class)) {
+ mocked.when(() -> Utils.ZstdUtils.isZstd(null)).thenReturn(false);
+ when(defaultDelegate.deserialize(null,
String.class)).thenReturn("fallback");
+
+ String result = delegate.deserialize(null, String.class);
+
+ assertEquals("fallback", result);
+ verify(defaultDelegate).deserialize(null, String.class);
+ verifyNoInteractions(zstdDelegate);
+ }
+ }
+
+ // exceptions propagation
+ @Test
+ void deserialize_zstdDelegateThrows_exceptionPropagates() {
+ try (MockedStatic<Utils.ZstdUtils> mocked =
mockStatic(Utils.ZstdUtils.class)) {
+ mocked.when(() ->
Utils.ZstdUtils.isZstd(ZSTD_BYTES)).thenReturn(true);
+ when(zstdDelegate.deserialize(any(), any()))
+ .thenThrow(new RuntimeException("decompression error"));
+
+ RuntimeException ex = assertThrows(RuntimeException.class,
+ () -> delegate.deserialize(ZSTD_BYTES, String.class));
+ assertEquals("decompression error", ex.getMessage());
+ }
+ }
+
+ @Test
+ void deserialize_defaultDelegateThrows_exceptionPropagates() {
+ try (MockedStatic<Utils.ZstdUtils> mocked =
mockStatic(Utils.ZstdUtils.class)) {
+ mocked.when(() ->
Utils.ZstdUtils.isZstd(PLAIN_BYTES)).thenReturn(false);
+ when(defaultDelegate.deserialize(any(), any()))
+ .thenThrow(new RuntimeException("thrift error"));
+
+ RuntimeException ex = assertThrows(RuntimeException.class,
+ () -> delegate.deserialize(PLAIN_BYTES, String.class));
+ assertEquals("thrift error", ex.getMessage());
+ }
+ }
+
+ // delegation chain
+ @Test
+ void deserialize_isZstdDeterminesRouting_trueThenFalse() {
+ try (MockedStatic<Utils.ZstdUtils> mocked =
mockStatic(Utils.ZstdUtils.class)) {
+ byte[] bytes = PLAIN_BYTES;
+
+ // First call: treated as Zstd
+ mocked.when(() -> Utils.ZstdUtils.isZstd(bytes)).thenReturn(true);
+ when(zstdDelegate.deserialize(bytes,
String.class)).thenReturn("via-zstd");
+ assertEquals("via-zstd", delegate.deserialize(bytes,
String.class));
+
+ // Second call: treated as non-Zstd
+ mocked.when(() -> Utils.ZstdUtils.isZstd(bytes)).thenReturn(false);
+ when(defaultDelegate.deserialize(bytes,
String.class)).thenReturn("via-default");
+ assertEquals("via-default", delegate.deserialize(bytes,
String.class));
+ }
+ }
+}
diff --git
a/storm-client/test/jvm/org/apache/storm/serialization/ZstdThriftSerializationDelegateTest.java
b/storm-client/test/jvm/org/apache/storm/serialization/ZstdThriftSerializationDelegateTest.java
new file mode 100644
index 000000000..24faf86e1
--- /dev/null
+++
b/storm-client/test/jvm/org/apache/storm/serialization/ZstdThriftSerializationDelegateTest.java
@@ -0,0 +1,186 @@
+/*
+ * 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.serialization;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.storm.Config;
+import org.apache.storm.generated.StormTopology;
+import org.apache.storm.utils.Utils;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class ZstdThriftSerializationDelegateTest {
+
+ private ZstdThriftSerializationDelegate delegate;
+
+ private static StormTopology validTopology() {
+ StormTopology t = new StormTopology();
+ t.set_spouts(Collections.emptyMap());
+ t.set_bolts(Collections.emptyMap());
+ t.set_state_spouts(Collections.emptyMap());
+ return t;
+ }
+
+ private static final StormTopology topology = validTopology();
+
+ @BeforeEach
+ void setUp() {
+ delegate = new ZstdThriftSerializationDelegate();
+ delegate.prepare(Collections.emptyMap()); // uses defaults
+ }
+
+ @Test
+ void prepare_emptyConf_usesDefaults() {
+ // Verify that prepare with empty conf doesn't throw and produces
+ // a delegate that can still serialize/deserialize correctly.
+ ZstdThriftSerializationDelegate d = new
ZstdThriftSerializationDelegate();
+ assertDoesNotThrow(() -> d.prepare(Collections.emptyMap()));
+
+ byte[] serialized = d.serialize(topology);
+ assertTrue(Utils.ZstdUtils.isZstd(serialized));
+ }
+
+ @Test
+ void prepare_customLevel_isUsed() {
+ Map<String, Object> conf = new HashMap<>();
+ conf.put(Config.STORM_COMPRESSION_ZSTD_LEVEL, 1);
+
+ ZstdThriftSerializationDelegate d = new
ZstdThriftSerializationDelegate();
+ d.prepare(conf);
+
+ // Both level 1 and level 3 produce valid zstd output — verify it is
zstd and round-trips
+ byte[] serialized = d.serialize(topology);
+ assertTrue(Utils.ZstdUtils.isZstd(serialized));
+ assertNotNull(d.deserialize(serialized, StormTopology.class));
+ }
+
+ @Test
+ void prepare_customMaxDecompressedBytes_isRespected() {
+ // Set a limit of 1 byte — decompressing anything non-trivial must fail
+ Map<String, Object> conf = new HashMap<>();
+ conf.put(Config.STORM_COMPRESSION_ZSTD_MAX_DECOMPRESSED_BYTES, 1);
+
+ ZstdThriftSerializationDelegate d = new
ZstdThriftSerializationDelegate();
+ d.prepare(conf);
+
+ // Serialize with the default delegate (no limit) to get valid zstd
bytes
+ byte[] serialized = delegate.serialize(topology);
+
+ RuntimeException ex = assertThrows(RuntimeException.class,
+ () -> d.deserialize(serialized, StormTopology.class));
+ assertEquals("Zstd decompression failed", ex.getMessage());
+ }
+
+ @Test
+ void serialize_tBaseObject_producesZstdFrame() {
+ byte[] result = delegate.serialize(topology);
+
+ assertNotNull(result);
+ assertTrue(result.length > 0);
+ assertTrue(Utils.ZstdUtils.isZstd(result), "Serialized output must
start with zstd magic");
+ }
+
+ @Test
+ void serialize_nonTBaseObject_throwsIllegalArgumentException() {
+ IllegalArgumentException ex =
assertThrows(IllegalArgumentException.class,
+ () -> delegate.serialize("not a TBase"));
+ assertEquals("Object must be an instance of TBase", ex.getMessage());
+ }
+
+ @Test
+ void serialize_nullObject_throwsIllegalArgumentException() {
+ assertThrows(IllegalArgumentException.class,
+ () -> delegate.serialize(null));
+ }
+
+ @Test
+ void serialize_sameTBaseObject_producesDeterministicOutput() {
+ byte[] first = delegate.serialize(topology);
+ byte[] second = delegate.serialize(topology);
+ assertArrayEquals(first, second, "Serialization of the same object
must be deterministic");
+ }
+
+ @Test
+ void deserialize_validZstdBytes_returnsCorrectType() {
+ byte[] serialized = delegate.serialize(topology);
+
+ StormTopology result = delegate.deserialize(serialized,
StormTopology.class);
+
+ assertNotNull(result);
+ }
+
+ @Test
+ void deserialize_corruptedBytes_throwsRuntimeException() {
+ byte[] garbage = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07};
+ RuntimeException ex = assertThrows(RuntimeException.class,
+ () -> delegate.deserialize(garbage, StormTopology.class));
+ assertEquals("Cannot deserialize ["
+StormTopology.class.getSimpleName() + "]. " +
+ "Expected zstd compressed bytes, but received unknown
format.", ex.getMessage());
+ }
+
+ @Test
+ void deserialize_wrongTargetClass_throwsRuntimeException() {
+ byte[] serialized = delegate.serialize(validTopology());
+ assertThrows(RuntimeException.class,
+ () -> delegate.deserialize(serialized, String.class));
+ }
+
+ @Test
+ void roundTrip_emptyTopology_isLossless() {
+ StormTopology original = topology;
+ byte[] serialized = delegate.serialize(original);
+ StormTopology recovered = delegate.deserialize(serialized,
StormTopology.class);
+
+ assertEquals(original, recovered);
+ }
+
+ @Test
+ void roundTrip_afterPrepareWithCustomConf_isLossless() {
+ Map<String, Object> conf = new HashMap<>();
+ conf.put(Config.STORM_COMPRESSION_ZSTD_LEVEL, 9);
+ conf.put(Config.STORM_COMPRESSION_ZSTD_MAX_DECOMPRESSED_BYTES, 100);
+
+ ZstdThriftSerializationDelegate d = new
ZstdThriftSerializationDelegate();
+ d.prepare(conf);
+
+ StormTopology original = topology;
+ byte[] serialized = d.serialize(original);
+ StormTopology recovered = d.deserialize(serialized,
StormTopology.class);
+
+ assertEquals(original, recovered);
+ }
+
+ @Test
+ void roundTrip_prepareNotCalled_throwsNullPointerOrRuntime() {
+ // Without prepare(), topoConf fields are zero/null — behaviour is
defined by ObjectReader.
+ // At minimum, serialize must not silently corrupt data.
+ ZstdThriftSerializationDelegate unprepared = new
ZstdThriftSerializationDelegate();
+ // Either it works (ObjectReader tolerates null map) or throws — must
not return wrong data.
+ try {
+ byte[] serialized = unprepared.serialize(topology);
+ StormTopology recovered = unprepared.deserialize(serialized,
StormTopology.class);
+ assertNotNull(recovered);
+ } catch (RuntimeException e) {
+ // Acceptable: prepare() was never called
+ }
+ }
+}
diff --git a/storm-client/test/jvm/org/apache/storm/utils/UtilsTest.java
b/storm-client/test/jvm/org/apache/storm/utils/UtilsTest.java
index c07a05423..d29f41d50 100644
--- a/storm-client/test/jvm/org/apache/storm/utils/UtilsTest.java
+++ b/storm-client/test/jvm/org/apache/storm/utils/UtilsTest.java
@@ -20,6 +20,7 @@ package org.apache.storm.utils;
import java.io.IOException;
import java.net.SocketException;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -517,4 +518,204 @@ public class UtilsTest {
fail(expectationMessage, unexpected);
}
}
+
+ @Test
+ void compress_nullInput_returnsNull() {
+ assertNull(Utils.ZstdUtils.compress(null, 3));
+ }
+
+ @Test
+ void compress_emptyInput_returnsEmptyArray() {
+ byte[] result = Utils.ZstdUtils.compress(new byte[0], 3);
+ assertNotNull(result);
+ assertEquals(0, result.length);
+ }
+
+ @Test
+ void compress_singleByte_producesZstdFrame() {
+ byte[] input = {0x42};
+ byte[] compressed = Utils.ZstdUtils.compress(input, 3);
+ assertTrue(Utils.ZstdUtils.isZstd(compressed),
+ "Compressed output should start with the Zstd magic number");
+ }
+
+ @Test
+ void compress_smallPayload_roundtrips() {
+ byte[] input = "Hello, ZSTD!".getBytes(StandardCharsets.UTF_8);
+ byte[] compressed = Utils.ZstdUtils.compress(input, 3);
+ byte[] decompressed = Utils.ZstdUtils.decompress(compressed, 1024 *
1024);
+ assertArrayEquals(input, decompressed);
+ }
+
+ @Test
+ void compress_largeRepetitivePayload_isSmallerThanOriginal() {
+ // Highly compressible: 64 KB of zeros
+ byte[] input = new byte[64 * 1024];
+ byte[] compressed = Utils.ZstdUtils.compress(input, 3);
+ assertTrue(compressed.length < input.length,
+ "Compressed size should be smaller than original for
repetitive data");
+ }
+
+ void compress_variousLevels_roundtrip() {
+ byte[] input = zstdSampleData(4096);
+ for (int level : Set.of(1, 3, 9, 19)) {
+ byte[] compressed = Utils.ZstdUtils.compress(input, level);
+ byte[] decompressed = Utils.ZstdUtils.decompress(compressed,
input.length * 2);
+ assertArrayEquals(input, decompressed,
+ "Round-trip must be lossless at compression level " +
level);
+ }
+ }
+
+ @Test
+ void compress_highEntropData_doesNotThrow() {
+ // Random-ish bytes — Zstd may not shrink them, but must not fail
+ byte[] input = zstdSampleData(8192);
+ assertDoesNotThrow(() -> Utils.ZstdUtils.compress(input, 3));
+ }
+
+ @Test
+ void decompress_nullInput_returnsNull() {
+ assertNull(Utils.ZstdUtils.decompress(null, 1024 * 1024));
+ }
+
+ @Test
+ void decompress_emptyInput_returnsEmptyArray() {
+ byte[] result = Utils.ZstdUtils.decompress(new byte[0], 1024 * 1024);
+ assertNotNull(result);
+ assertEquals(0, result.length);
+ }
+
+ @Test
+ void decompress_validFrame_recoversOriginal() {
+ byte[] original = "Unit test payload".getBytes(StandardCharsets.UTF_8);
+ byte[] compressed = Utils.ZstdUtils.compress(original, 3);
+ byte[] recovered = Utils.ZstdUtils.decompress(compressed, 1024 * 1024);
+ assertArrayEquals(original, recovered);
+ }
+
+ @Test
+ void decompress_corruptedData_throwsRuntimeException() {
+ byte[] garbage = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07};
+ RuntimeException ex = assertThrows(RuntimeException.class,
+ () -> Utils.ZstdUtils.decompress(garbage, 1024 * 1024));
+ assertTrue(ex.getMessage().contains("Zstd decompression failed"),
+ "Exception message should describe the failure");
+ }
+
+ @Test
+ void decompress_exceedsMaxBytes_throwsRuntimeException() {
+ // Compress a 4 KB payload but only allow 10 bytes out
+ byte[] input = zstdSampleData(4096);
+ byte[] compressed = Utils.ZstdUtils.compress(input, 3);
+
+ RuntimeException ex = assertThrows(RuntimeException.class,
+ () -> Utils.ZstdUtils.decompress(compressed, 10));
+ assertTrue(ex.getMessage().contains("Zstd decompression failed"),
+ "Should throw when decompressed size exceeds the limit");
+ assertTrue(ex.getCause().getMessage().contains("Decompression
threshold exceeded"),
+ "Root cause should describe the threshold breach");
+ }
+
+ @Test
+ void decompress_exactlyAtLimit_succeeds() {
+ byte[] input = "abc".getBytes(StandardCharsets.UTF_8); // 3 bytes
+ byte[] compressed = Utils.ZstdUtils.compress(input, 3);
+ // Allow exactly 3 bytes — should succeed
+ byte[] result = Utils.ZstdUtils.decompress(compressed, 3);
+ assertArrayEquals(input, result);
+ }
+
+ @Test
+ void decompress_truncatedFrame_throwsRuntimeException() {
+ byte[] input = zstdSampleData(256);
+ byte[] compressed = Utils.ZstdUtils.compress(input, 3);
+ // Slice off the last third to simulate truncation
+ byte[] truncated = new byte[compressed.length * 2 / 3];
+ System.arraycopy(compressed, 0, truncated, 0, truncated.length);
+
+ assertThrows(RuntimeException.class,
+ () -> Utils.ZstdUtils.decompress(truncated, 1024 * 1024));
+ }
+
+ @Test
+ void isZstd_nullInput_returnsFalse() {
+ assertFalse(Utils.ZstdUtils.isZstd(null));
+ }
+
+ @Test
+ void isZstd_emptyArray_returnsFalse() {
+ assertFalse(Utils.ZstdUtils.isZstd(new byte[0]));
+ }
+
+ @Test
+ void isZstd_arrayTooShort_returnsFalse() {
+ // Only 3 bytes — not enough for the 4-byte magic
+ assertFalse(Utils.ZstdUtils.isZstd(new byte[]{(byte) 0x28, (byte)
0xB5, (byte) 0x2F}));
+ }
+
+ @Test
+ void isZstd_validMagicBytes_returnsTrue() {
+ // Pad with enough trailing bytes so INT_HANDLE.get(bytes, 0) does not
throw
+ byte[] magic = new byte[]{(byte) 0x28, (byte) 0xB5, (byte) 0x2F,
(byte) 0xFD, 0x00};
+ assertTrue(Utils.ZstdUtils.isZstd(magic));
+ }
+
+ @Test
+ void isZstd_validCompressedOutput_returnsTrue() {
+ byte[] compressed =
Utils.ZstdUtils.compress("test".getBytes(StandardCharsets.UTF_8), 3);
+ assertTrue(Utils.ZstdUtils.isZstd(compressed),
+ "Output of compress() must be recognised as Zstd");
+ }
+
+ @Test
+ void isZstd_plainText_returnsFalse() {
+ byte[] plain = "Hello, world!".getBytes(StandardCharsets.UTF_8);
+ assertFalse(Utils.ZstdUtils.isZstd(plain));
+ }
+
+ @Test
+ void isZstd_wrongMagicOrder_returnsFalse() {
+ // Big-endian order of the same bytes — should NOT match
+ byte[] wrongEndian = {(byte) 0xFD, (byte) 0x2F, (byte) 0xB5, (byte)
0x28, 0x00};
+ assertFalse(Utils.ZstdUtils.isZstd(wrongEndian));
+ }
+
+ @Test
+ void isZstd_almostCorrectMagic_returnsFalse() {
+ // One byte off
+ byte[] close = {(byte) 0x28, (byte) 0xB5, (byte) 0x2F, (byte) 0xFE,
0x00};
+ assertFalse(Utils.ZstdUtils.isZstd(close));
+ }
+
+ @Test
+ void roundTrip_emptyStringBytes_succeeds() {
+ byte[] input = "".getBytes(StandardCharsets.UTF_8);
+ // compress() returns the original array for empty input
+ byte[] compressed = Utils.ZstdUtils.compress(input, 3);
+ assertArrayEquals(input, compressed); // empty → empty, no compression
+ }
+
+ @Test
+ void roundTrip_binaryData_succeeds() {
+ byte[] input = zstdSampleData(1024);
+ byte[] compressed = Utils.ZstdUtils.compress(input, 3);
+ byte[] decompressed = Utils.ZstdUtils.decompress(compressed,
input.length * 2);
+ assertArrayEquals(input, decompressed);
+ }
+
+ @Test
+ void roundTrip_unicodePayload_succeeds() {
+ byte[] input = "apache storm - storm -
apache".getBytes(StandardCharsets.UTF_8);
+ byte[] compressed = Utils.ZstdUtils.compress(input, 3);
+ byte[] decompressed = Utils.ZstdUtils.decompress(compressed,
input.length * 4);
+ assertArrayEquals(input, decompressed);
+ }
+
+ private static byte[] zstdSampleData(int size) {
+ byte[] data = new byte[size];
+ for (int i = 0; i < size; i++) {
+ data[i] = (byte) (i % 256);
+ }
+ return data;
+ }
}
diff --git
a/storm-server/src/test/java/org/apache/storm/daemon/supervisor/BasicContainerTest.java
b/storm-server/src/test/java/org/apache/storm/daemon/supervisor/BasicContainerTest.java
index b458c9164..a4f6ec93a 100644
---
a/storm-server/src/test/java/org/apache/storm/daemon/supervisor/BasicContainerTest.java
+++
b/storm-server/src/test/java/org/apache/storm/daemon/supervisor/BasicContainerTest.java
@@ -322,7 +322,7 @@ public class BasicContainerTest {
st.set_spouts(new HashMap<>());
st.set_bolts(new HashMap<>());
st.set_state_spouts(new HashMap<>());
- byte[] serializedState = Utils.gzip(Utils.thriftSerialize(st));
+ byte[] serializedState =
Utils.GzipUtils.compress(Utils.thriftSerialize(st));
final Map<String, Object> superConf = new HashMap<>();
superConf.put(Config.STORM_LOCAL_DIR, stormLocal);
@@ -426,7 +426,7 @@ public class BasicContainerTest {
// minimum 1.x version of supporting STORM-2448 would be 1.0.4
st.set_storm_version("1.0.4");
- byte[] serializedState = Utils.gzip(Utils.thriftSerialize(st));
+ byte[] serializedState =
Utils.GzipUtils.compress(Utils.thriftSerialize(st));
final Map<String, Object> superConf = new HashMap<>();
superConf.put(Config.STORM_LOCAL_DIR, stormLocal);
@@ -529,7 +529,7 @@ public class BasicContainerTest {
// minimum 0.x version of supporting STORM-2448 would be 0.10.3
st.set_storm_version("0.10.3");
- byte[] serializedState = Utils.gzip(Utils.thriftSerialize(st));
+ byte[] serializedState =
Utils.GzipUtils.compress(Utils.thriftSerialize(st));
final Map<String, Object> superConf = new HashMap<>();
superConf.put(Config.STORM_LOCAL_DIR, stormLocal);