This is an automated email from the ASF dual-hosted git repository.
reiabreu 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 0cf51b679 Add tuple compression for inter-worker communication (#8707)
0cf51b679 is described below
commit 0cf51b6793b4f7eee02f09034992f3215ee7804b
Author: Gianluca Graziadei <[email protected]>
AuthorDate: Sat May 30 17:31:59 2026 +0200
Add tuple compression for inter-worker communication (#8707)
* init config
* (de)serialization logic
* deserialization logic refinement, unit tests
* docs + storm perf example
* apply new checkstyle rules
* config param `topology.tuple.compression.max.decompressed.bytes`
* improve deserialize zstd false positive collision logic
* enter the decompress branch when the topology actually uses compression
* minor changes
* docs changes + additional test case
* remove unnecessary arrays allocation in `KryoTupleSerializer`
* add LOG.debug for deserialize collision
* ad-hoc test cases for false positive isZstd deserializer
* add bench in docs
---
conf/defaults.yaml | 3 +
docs/Serialization.md | 83 +++++
.../FileReadWordCountSpoutCompressionTopo.java | 94 ++++++
.../src/main/sampledata/longrandomwords.txt | 50 +++
storm-client/src/jvm/org/apache/storm/Config.java | 26 ++
.../storm/serialization/KryoTupleDeserializer.java | 64 +++-
.../storm/serialization/KryoTupleSerializer.java | 29 +-
.../src/jvm/org/apache/storm/utils/Utils.java | 40 ++-
.../KryoTupleSerializerDeserializerTest.java | 373 +++++++++++++++++++++
.../test/jvm/org/apache/storm/utils/UtilsTest.java | 50 +++
10 files changed, 797 insertions(+), 15 deletions(-)
diff --git a/conf/defaults.yaml b/conf/defaults.yaml
index 2c3bb9e06..d5d6bb451 100644
--- a/conf/defaults.yaml
+++ b/conf/defaults.yaml
@@ -55,9 +55,12 @@ 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.ZstdBridgeThriftSerializationDelegate"
+topology.tuple.compression.threshold: 1460
+topology.tuple.compression.enable: false
storm.compression.zstd.level: 3
storm.compression.zstd.max.decompressed.bytes: 104857600
storm.compression.gzip.max.decompressed.bytes: 104857600
+topology.tuple.compression.max.decompressed.bytes: 10485760
storm.codedistributor.class:
"org.apache.storm.codedistributor.LocalFileSystemCodeDistributor"
storm.workers.artifacts.dir: "workers-artifacts"
storm.health.check.dir: "healthchecks"
diff --git a/docs/Serialization.md b/docs/Serialization.md
index 0e7cc2e87..e7af57477 100644
--- a/docs/Serialization.md
+++ b/docs/Serialization.md
@@ -61,6 +61,89 @@ Beware that Java serialization is extremely expensive, both
in terms of CPU cost
You can turn on/off the behavior to fall back on Java serialization by setting
the `Config.TOPOLOGY_FALL_BACK_ON_JAVA_SERIALIZATION` config to true/false. The
default value is false for security reasons.
+### Tuple compression
+
+For inter-worker (remote) traffic, Storm can optionally compress serialized
tuples with [Zstandard](https://facebook.github.io/zstd/) before they are sent
over the network. This is intended for one specific scenario: components that
emit **large** payloads to a remote worker, where the bytes saved on the wire
outweigh the CPU cost of compression. A good example is a spout that emits
entire lines of text to a downstream bolt running on a different worker.
+
+Compression is **disabled by default** and follows the serialization lifecycle
exactly:
+
+- **Intra-worker (local) traffic** bypasses `KryoTupleSerializer` altogether,
so it is never compressed regardless of configuration. You do not pay any CPU
cost for tuples that stay inside a worker process.
+- **Inter-worker (remote) traffic** is compressed only when compression is
enabled for the source component *and* the serialized tuple is larger than the
configured threshold. Small tuples (single words, IDs, etc.) are left
uncompressed, since the framing overhead of a compressed payload can exceed the
original size.
+
+#### Enabling compression per component
+
+Compression is controlled by the component-specific configuration
`topology.tuple.compression.enable`. Because Storm merges component-specific
configuration over the topology configuration, you can enable it for just the
components that emit large tuples, leaving the rest of the topology untouched:
+
+```java
+TopologyBuilder builder = new TopologyBuilder();
+
+builder.setSpout(SPOUT_ID, new FileReadSpout(inputFile), spoutNum)
+ .addConfiguration(Config.TOPOLOGY_TUPLE_COMPRESSION_ENABLE, true);
+
+builder.setBolt(SPLIT_ID, new SplitSentenceBolt(), spBoltNum)
+ .localOrShuffleGrouping(SPOUT_ID);
+builder.setBolt(COUNT_ID, new CountBolt(), cntBoltNum)
+ .fieldsGrouping(SPLIT_ID, new Fields(SplitSentenceBolt.FIELDS));
+```
+
+You can also enable it topology-wide (or cluster-wide via `storm.yaml`) by
setting `topology.tuple.compression.enable: true`, but enabling it only where
large tuples are actually emitted is recommended.
+
+#### Flux
+
+> **Note:** With [Flux](flux.html), only **topology-wide** enablement is
currently possible. Flux has no per-component configuration mechanism —
`FluxBuilder` applies only parallelism, number of tasks, memory/CPU load, and
groupings to the underlying declarers, and the `config:` block is
topology-scoped. There is no Flux equivalent of
`declarer.addConfiguration(...)`, so the per-component approach recommended
above cannot be expressed in a Flux YAML definition.
+
+To enable compression for a Flux topology, set it in the topology-level
`config:` block:
+
+```yaml
+config:
+ topology.tuple.compression.enable: true
+ topology.tuple.compression.threshold: 1460
+```
+
+Be aware that this enables compression for *every* remote-bound tuple in the
topology that exceeds the threshold.
+
+#### Configuration reference
+
+| Config | Default | Description |
+| --- | --- | --- |
+| `topology.tuple.compression.enable` | `false` | Enables Zstd compression of
serialized tuples before remote transfer. Best set per component via
`addConfiguration`. |
+| `topology.tuple.compression.threshold` | `1460` | Minimum serialized tuple
size, in bytes, before compression is attempted. Tuples at or below this size
are sent uncompressed. The default matches the typical Ethernet TCP MSS, so
payloads that already fit in a single network frame are never compressed. |
+| `storm.compression.zstd.level` | `3` | Zstd compression level. Supported
range is 1–19; levels 20–22 (ultra mode) are prohibited because of their memory
requirements. |
+| `topology.tuple.compression.max.decompressed.bytes` | `10485760` (10 MB) |
Upper bound on the decompressed size of a single tuple. Decompression that
would exceed this limit fails, guarding against malicious or corrupt payloads. |
+
+#### How decompression works
+
+Compression is self-describing on the wire, so **no extra configuration is
required on the receiving side**. The deserializer inspects the leading bytes
of each incoming payload: if they match the Zstd magic header it decompresses
the payload (bounded by `topology.tuple.compression.max.decompressed.bytes`)
before deserializing, otherwise it deserializes the bytes directly. A single
deserializer therefore transparently handles a mix of compressed and
uncompressed tuples.
+
+As an optimization, the deserializer determines once — when the worker starts
— whether *any* component in the topology enables compression (by scanning the
merged per-component configurations). If none does, the magic-header check is
skipped entirely and the Zstd code path is never touched, so topologies that do
not use the feature pay no per-tuple cost. The corollary is that compression
must be enabled somewhere in the topology config for compressed tuples to be
decompressed on receipt [...]
+
+#### Indicative benchmark
+
+> **Disclaimer:** These numbers were gathered in a limited capacity while
developing this feature and should be treated as a rough guide only, not as a
performance guarantee. They were produced on a specific, deliberately
favourable setup and your results will vary with topology shape, tuple size,
network characteristics, and hardware.
+
+The benchmark ran two equivalent word-count topologies defined in `storm-perf`
— one with tuple compression enabled in Spout component
(`FileReadWordCountSpoutCompressionTopo`) and one without
(`FileReadWordCountTopo`) — across workers connected by a simulated network
with **10 ms latency** and **0.5 ms jitter**. This does not represent a typical
intra-datacenter network; it deliberately emphasizes the maximum advantage the
feature can offer when configured well. The tuple size used is t [...]
+
+Sample round-trip ping between two supervisors on the Docker network:
+```
+--- cluster-supervisor2-1 ping statistics ---
+5 packets transmitted, 5 received, 0% packet loss, time 4004ms
+rtt min/avg/max/mdev = 18.767/24.353/42.486/9.083 ms
+```
+
+Results (compression vs. no compression):
+
+| Metric | Compression | No compression | Difference | Better |
+| --- | --- | --- | --- | --- |
+| Avg transfer rate (msg/s) | 776,389 | 744,544 | +31,845 (+4.3%) |
Compression |
+| Peak transfer rate (msg/s) | 805,700 | 790,300 | +15,400 | Compression |
+| Avg spout throughput (acks/s) | 98,167 | 92,844 | +5,323 (+5.8%) |
Compression |
+| Peak spout throughput (acks/s) | 100,300 | 98,666 | +1,634 | Compression |
+| Avg complete latency (ms) | 362.48 | 376.73 | -14.25 (-3.8%) | Compression |
+| Max complete latency (ms) | 366.44 | 385.72 | -19.28 | Compression |
+| Runtime stability | More consistent | More fluctuation | — | Compression |
+
+In this configuration, compression improved transfer rate and spout throughput
by roughly 4–6% and reduced complete latency by a few percent, while also
producing more consistent per-task behaviour (less jitter across tasks). The
takeaway is qualitative: when large tuples cross a high-latency link, trading
CPU for fewer bytes on the wire can pay off — but you should measure with your
own workload before enabling it broadly.
+
### Component-specific serialization registrations
Storm 0.7.0 lets you set component-specific configurations (read more about
this at [Configuration](Configuration.html)). Of course, if one component
defines a serialization that serialization will need to be available to other
bolts -- otherwise they won't be able to receive messages from that component!
diff --git
a/examples/storm-perf/src/main/java/org/apache/storm/perf/FileReadWordCountSpoutCompressionTopo.java
b/examples/storm-perf/src/main/java/org/apache/storm/perf/FileReadWordCountSpoutCompressionTopo.java
new file mode 100644
index 000000000..51c43057e
--- /dev/null
+++
b/examples/storm-perf/src/main/java/org/apache/storm/perf/FileReadWordCountSpoutCompressionTopo.java
@@ -0,0 +1,94 @@
+/*
+ * 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.perf;
+
+import java.util.Map;
+import org.apache.storm.Config;
+import org.apache.storm.generated.StormTopology;
+import org.apache.storm.perf.bolt.CountBolt;
+import org.apache.storm.perf.bolt.SplitSentenceBolt;
+import org.apache.storm.perf.spout.FileReadSpout;
+import org.apache.storm.perf.utils.Helper;
+import org.apache.storm.topology.TopologyBuilder;
+import org.apache.storm.tuple.Fields;
+import org.apache.storm.utils.Utils;
+
+/**
+ * This topo helps measure speed of word count.
+ *
+ * <p>Spout loads a file into memory on initialization, then emits the lines
in an endless loop.
+ */
+public class FileReadWordCountSpoutCompressionTopo {
+ public static final String SPOUT_ID = "spout";
+ public static final String COUNT_ID = "counter";
+ public static final String SPLIT_ID = "splitter";
+ public static final String TOPOLOGY_NAME =
"FileReadWordCountSpoutCompressionTopo";
+
+ // Config settings
+ public static final String SPOUT_NUM = "spout.count";
+ public static final String SPLIT_NUM = "splitter.count";
+ public static final String COUNT_NUM = "counter.count";
+ public static final String INPUT_FILE = "input.file";
+
+ public static final int DEFAULT_SPOUT_NUM = 1;
+ public static final int DEFAULT_SPLIT_BOLT_NUM = 2;
+ public static final int DEFAULT_COUNT_BOLT_NUM = 2;
+
+
+ static StormTopology getTopology(Map<String, Object> config) {
+
+ final int spoutNum = Helper.getInt(config, SPOUT_NUM,
DEFAULT_SPOUT_NUM);
+ final int spBoltNum = Helper.getInt(config, SPLIT_NUM,
DEFAULT_SPLIT_BOLT_NUM);
+ final int cntBoltNum = Helper.getInt(config, COUNT_NUM,
DEFAULT_COUNT_BOLT_NUM);
+ final String inputFile = Helper.getStr(config, INPUT_FILE);
+
+ TopologyBuilder builder = new TopologyBuilder();
+ // sampledata/longrandomwords.txt contains sentences with at least
1500 bytes
+ builder.setSpout(SPOUT_ID, new FileReadSpout(inputFile), spoutNum)
+ .addConfiguration(Config.TOPOLOGY_TUPLE_COMPRESSION_ENABLE,
true);
+ builder.setBolt(SPLIT_ID, new SplitSentenceBolt(),
spBoltNum).localOrShuffleGrouping(SPOUT_ID);
+ builder.setBolt(COUNT_ID, new CountBolt(),
cntBoltNum).fieldsGrouping(SPLIT_ID, new Fields(SplitSentenceBolt.FIELDS));
+
+ return builder.createTopology();
+ }
+
+ public static void main(String[] args) throws Exception {
+ int runTime = -1;
+ Config topoConf = new Config();
+ if (args.length > 0) {
+ runTime = Integer.parseInt(args[0]);
+ }
+ if (args.length > 1) {
+ topoConf.putAll(Utils.findAndReadConfigFile(args[1]));
+ }
+ topoConf.put(Config.TOPOLOGY_PRODUCER_BATCH_SIZE, 1000);
+ topoConf.put(Config.TOPOLOGY_BOLT_WAIT_STRATEGY,
"org.apache.storm.policy.WaitStrategyPark");
+ topoConf.put(Config.TOPOLOGY_BOLT_WAIT_PARK_MICROSEC, 0);
+ topoConf.put(Config.TOPOLOGY_DISABLE_LOADAWARE_MESSAGING, true);
+ topoConf.put(Config.TOPOLOGY_STATS_SAMPLE_RATE, 0.0005);
+
+ topoConf.putAll(Utils.readCommandLineOpts());
+ if (args.length > 2) {
+ System.err.println("args: [runDurationSec] [optionalConfFile]");
+ return;
+ }
+ // Submit topology to storm cluster
+ Helper.runOnClusterAndPrintMetrics(runTime, TOPOLOGY_NAME, topoConf,
getTopology(topoConf));
+ }
+}
diff --git a/examples/storm-perf/src/main/sampledata/longrandomwords.txt
b/examples/storm-perf/src/main/sampledata/longrandomwords.txt
new file mode 100644
index 000000000..e50c25207
--- /dev/null
+++ b/examples/storm-perf/src/main/sampledata/longrandomwords.txt
@@ -0,0 +1,50 @@
+bacterioblast neurotrophic ventricous involatile trillion oflete splenauxe
Vichyite scabbiness antiscolic unpredict mastication glacierist theologal
tetchy sapphiric unchatteled elemicin dastardliness apocalypst untongued
prefatorial Edo preparative experientialist Spatangoidea unfulminated orgiastic
pneumatotherapy Mycogone nonpoisonous bespin Jerusalem benthonic antihero
zenick supermarket divinator coadvice abscission zanyism inductivity subdentate
pneumonalgia pyrocatechol nebular fr [...]
+meloplasty Dodecatheon nonrepetition coadvice taurocolla eternal Fouquieria
retinize quadrennial provedore serpentinic planosubulate downthrust widdle
epauliere chooser ethmopalatal groundneedle ladhood elastivity cloy guitarist
bozal undiffusive eternal tristich familist counteralliance antivenin
redecrease meriquinoidal Bermudian paranephros projecting scrat scrubbed
monander propodiale planosubulate halloo emir oratorship slait eurythermal
unsupercilious tetragynian antideflation unsu [...]
+nonutilitarian galbulus snare slait pope Scanic overinstruct tramplike
Bushongo Bassaris foursquare Orbitolina rebilling corbel transcorporeal
allotropic Coniferae benthonic chordacentrum templar porriginous refasten testa
misthread euphonym naught theologal thorite blurredness starosta unstressedly
semiangle subfoliar doubtingness manny sonable seraphism archistome
prescriptible pyxie epauliere pomiferous unforkedness Kenipsim alveolite
dipsomaniacal admissory allotropic ethnocracy trop [...]
+havoc regardful angiopathy Cercosporella benzoperoxide imaginary waird
dialoguer exploiter tambo lineamental cheesecutter Scorpaenidae subdrainage
cartful embryotic unleavened uncontradictableness vinegarish quarried dosseret
beatable signifier impugnation Scanic playfellowship scotale Hydrangea
quadrennial unpatched introducer unswanlike mericarp peptonate euphemious lammy
phoenicochroite Lincolnlike figured times allotropic frenal arduousness
imprescribable Vichyite allotropic valvulot [...]
+euphonym monander psychofugal semiangle autobiographist kenno euphonym
danseuse nonutilitarian poleax biopsic placatory hackneyed tristich papery
orthopedical diopside Tsonecan chargeably biodynamics transcorporeal
Socraticism parmelioid pentosuria nonutilitarian shellworker collegian ordinant
testa antiadiaphorist twinling predisputant bot spermaphyte oratorship
cuproiodargyrite bismuthiferous ultrasystematic gallybeggar marten hackneyed
unrealize twinling velaric discipular hoove sawdu [...]
+ovopyriform rivethead Pithecolobium corbel pleasurehood dithery guanajuatite
tramplike asparaginic epididymitis arrowworm hepatorrhaphy Eryon neurotrophic
lophotrichic unstressedly Joachimite plugger trip vinegarish nonmanufacture
nigh trunnel cobeliever packsack diplomatize uninductive absvolt undangered
Itea eucalypteol tartrous weism infravaginal transude aquiline eternal
unbashfulness eurythermal theologicopolitical sviatonosite uncarefully
infestation Dadaism cuproiodargyrite biopsi [...]
+unurban docimastical Triconodonta Ghent collegian tautness figured sombreroed
pompiloid papery cockstone biodynamics ununiformly unfeeble eternal rechar
papery subfoliar plugger schoolmasterism focaloid monstership dunkadoo
parabolicness obolus heavenful antiabolitionist arrowworm dithery hellbender
myesthesia untongued chrysochrous depravity stereotypography photoelasticity
scotale overinstruct stronghearted tickleproof periarthritis experientialist
bugre preaffiliate participatingly ho [...]
+omniscribent acidophile erythrodextrin Dictograph instructiveness
experientialist focaloid basto psychofugal epidymides Homoiousian wandoo
meloplasty monilioid shellworker slipped antalgol scotale interfraternal
rotular misthread posterishness appetible misexposition arrendation
pleurotropous cocksuredom bromic Florissant ethnocracy angiolymphoma sangaree
pyroacetic Tsonecan osteopaedion unurban gunshop plerome byroad placatory
obolus noncrystallized oxyterpene lammy quintette rehabilita [...]
+myesthesia ell glandularly bugre seraphism undercolored mustafina
transcorporeal subfoliar reciprocation serphoid omniscribent redecrease
detractive terrestrially starer temporomastoid comparability Pishquow sapphiric
topline serpentinic Edo octogynous planosubulate timbermonger cacuminal weism
umangite warlike Russifier wandoo cumbrousness Whilkut silverhead predebit
bicorporeal Edo putative technopsychology Dictograph approbation noreast
uninductive ferrogoslarite prezygapophysial shal [...]
+stronghearted depressingly lithograph aprosopia parodist bought velaric
analgize epauliere ipomoein swearingly macropterous taver osteopaedion greave
hysterolysis visceral uninterpleaded piquantness benthonic fossilism
phoenicochroite devilwise bonze pamphlet preoral depravity scotale
chronographic overbuilt bot parodist poleax tristich unimmortal bought
repealableness Semecarpus louse Endomycetaceae apopenptic reformatory
tendomucoid soorkee parabolicness mesymnion orgiastic cubby recha [...]
+rivethead soorkee uncarefully collegian uncompromisingly cacuminal
cumbrousness uncompromisingness Aplacentalia subfebrile coadvice tetchy
pleurotropous boser stroking vinegarish ovoviviparous overstudiousness suspend
dishpan nigh oinomancy shibuichi coracomandibular pendulant Pithecolobium
Dawsonia gorilloid agglomeratic redesertion frictionlessly redesertion
opacousness entame pleasurehood putative epidymides morphiomania flatman
ultratense liberatress percent strander tendomucoid ting [...]
+unrevolting unobservantness stapedius tum serosanguineous clanned ventricous
times winterproof overinstruct waird reeveland metoxazine approbation aquiline
antalgol besagne mesophyte depressingly enterostomy erythrodextrin heavenful
excerpt yawler interruptor bacillite collegian sonable frenal squdge entame
galbulus Florissant propodiale cinque incomprehensible totaquina pachydermatoid
plugger mustafina unprovided umbellic preagitate unanatomized counteractively
relaster Italical spot Tr [...]
+almud Fameuse preagitate noncrystallized sterilely pseudohalogen dialoguer
alen chasmy obispo embryotic pony oxyterpene sequentially outhue propodiale
horsefly Dadaism insatiately sturdied eurythermal meloplasty Megaluridae
planosubulate bladderwort lampyrine naprapath benzoperoxide ethnocracy
porriginous stapedius ascitic unchatteled elemicin plugger pentosuria
returnability transcorporeal allectory Effie sterilely scotching underskin
equiconvex excerpt moodishness hypoid ramosopalmate [...]
+acocotl pumpkinification benzothiofuran toxihaemia boser rechar eternal
Tsonecan electrotechnics serphoid phallaceous tomorrowness winterproof
Megaluridae depthwise sloped gymnastic depressingly consumptional molossic
Consolamentum rainproof suspend phytoma glaumrie ventricous rainproof precostal
chordacentrum chacona haply eristically neurotrophic Mormyrus paleornithology
vitally lithotresis depressingly parabolicness propodiale relaster
theologicopolitical bot cubby michigan gorilloid [...]
+emir overcrown swoony molecule admissory chrysochrous impressor heavenful
ramosopalmate divinator Arneb phytoma adz tingly hymnic macropterous glacierist
tailoress euphonym scrubbed unprovided molecule astronomize bestill
hysterolysis guitarist subangulated thermochemically Cercosporella impressor
comprovincial pleasurehood stachyuraceous chilblain jirble metapolitics
quintette eulogization pachydermous barkometer nonpoisonous starer gunshop
molecule oratorize coldfinch preaffiliate Anim [...]
+daytime erythremia lophotrichic columniform thorite piquantness waird yawler
diurnalness carposporangial massedly sapience Protestantize toxihaemia
Saponaria Fameuse Pishquow wandoo hypochondriacism autoschediastical
pseudohalogen karyological Animalivora tendomucoid Semecarpus provedore
Florissant infrastapedial undinted stronghearted shola antihero folious
pneumonalgia endotheliomyoma subofficer Thraupidae Socraticism stroking
embryotic Ochnaceae erythrodextrin eurythermal pumpkinifica [...]
+anta sapience choralcelo Edo infestation mustafina cubit psychofugal affaite
inertly migrainoid craglike ovoviviparous sud hypochondriacism comparability
halloo trillium tonsure thiodiazole unleavened elastivity cacuminal
bismuthiferous suspend rivethead afterpressure feasibleness farrantly refasten
swangy physiologian transude figured Cercosporella outwealth naprapath massedly
counteractively balanocele groundneedle whitlowwort seditious topline overbuilt
rivethead Dawsonia plerome seei [...]
+uloid besagne dehairer technopsychology Sebastian orthopedical infestation
afterpressure imaginary potentness Cimmerianism pumpkinification defensibly
hogmace brag snare Vichyite alveolite triakistetrahedral unsupercilious
arduousness abscission stentorophonic steprelationship quintette
archididascalian Sphenodontidae insatiately ticktick flippantness pomiferous
ungouged bubble louse speckedness upswell hypochondriacism scotching coadvice
posterishness saccharogenic genii theologicopolit [...]
+stroking homotransplant posttraumatic silicize magnificently yawler Coniferae
wandoo gelatinousness wherefrom hogmace drome emir supraoesophageal arval
disilane Oryzorictinae penult quad unchatteled intuition edificator
predisputant cyanoguanidine predisputant playfellowship nonprofession
perfunctory infravaginal yeelaman magnetooptics Semecarpus aprosopia Glecoma
velaric knob refective friarhood friarhood prolificy merciful cyanophilous
serpentinic molossic eternal trailmaking projectin [...]
+redecrease antideflation splenauxe sertularian aurothiosulphuric supermarket
bugre Ghent shola massedly posterishness sviatonosite Hydrangea embryotic
omniscribent Confervales pondside mesymnion pleurotropous coracomandibular
isopelletierin oxyterpene archesporial fetlocked pterostigma abscission Dadaism
foursquare trisilicic inexistency relaster prefatorial tonsure Socraticism
doubtingness roughcast untongued macropterous corona whittle Sebastian jharal
deepmost unprovided unburnt byroa [...]
+bestill semantician unrevolting tartrous Lemuridae erlking naprapath testa
dishpan potentness avengeful abstractionism unprovided prefatorial
frontoorbital wandoo discipular terrestrially epididymitis knob byroad
Fouquieria flippantness spermaphyte oblongly Ochnaceae slangy silverhead
unaccessible putative downthrust Homoiousian thermochemically japanned
Ophiosaurus scrubbed leucophoenicite dermorhynchous bonze basto Dictograph
unfeeble ascitic undecorated ultraobscure figureheadship cya [...]
+Pishquow warlike biodynamics antineuritic chronographic timbermonger
floatability nonrepetition Joachimite chilblain nonpoisonous Munychian tautness
biodynamics bought antiadiaphorist valvula depravity bettermost preagitate
Prosobranchiata paranephros tetragynian antivenin rebilling cromlech
sertularian tonsure various temporomastoid oversand unfulminated ovoviviparous
autoschediastical lithotresis prolificy thermochemically aprosopia
Sphenodontidae merciful serosanguineous acidophile He [...]
+parastas Homoiousian Kenipsim phlogisticate lophotrichic naught penult
minniebush phallaceous Triphora afterpressure eternal carposporangial antivenin
rizzomed reciprocation leucophoenicite rosaniline umangite allectory marshiness
Italical vesperal analgic endotheliomyoma Spatangoidea sonable kerykeion
phytonic Munychian pondside supraoesophageal skyshine Llandovery pope pelf
componental calycular macropterous rehabilitative yote mammonish
autobiographist codisjunct Ludgatian sirrah mutt [...]
+exprobratory infestation analgic thermanesthesia deindividualization
generalizable zoonitic mastication phytoma scotching Babylonism Pincian
japanned transcorporeal imprescribable rebilling opacousness pneumatotherapy
nonexecutive Pithecolobium pamphlet thermoresistant unlapsing fossilism ungrave
bespin shellworker upswell jirble trailmaking unchatteled amender Protestantize
stiffish precostal stroking boser sleigher marten subfebrile depravity
involatile uniarticular tricae schoolmaster [...]
+massedly becomma pentafid orchiocatabasis propheticism constitutor ungouged
iniquitously sirrah prescriptible socioromantic omniscribent quailberry
generalizable bacterioblast asparaginic tartrous stradametrical redesertion
Zuludom misthread Passiflorales inductivity pendulant Pishquow unlapsing
balladmonger antalgol diminutively triradiated dipsomaniacal electrotechnics
brutism Babylonism tetchy Yannigan strammel saguran scabbiness unexplicit
regardful boor plugger Ghent biopsic trampli [...]
+Mormyrus preagitate aspersor Semecarpus lienteria subofficer mediateness Tamil
bunghole corelysis admissory bespin cyanophilous Semecarpus retinize sturdied
golem benzoperoxide consumptional disilane refasten reappreciate Mycogone
prezygapophysial scotale licitness oratorize groundneedle idiotize
sesquiquintile uniarticular serosanguineous abstractionism triakistetrahedral
flushgate louse yawler prepavement meriquinoidal amylogenesis abthainry Edo
untongued Cephalodiscus sombreroed quint [...]
+impugnation groundneedle goladar astucious sesquiquintile wemless spermaphyte
unpremonished metastoma dinical packsack ungrave yote percent dipsomaniacal
scabbiness dehairer paleornithology Lincolnlike corona bettermost Ludgatian
diopside Alethea Confervales pneumonalgia refective Alethea ungouged
commandingness unleavened Dawsonia transude glyphography pondside dosseret
subofficer rede gymnastic opacousness cretaceous unchatteled trabecular
outguess myesthesia sequentially doubtingness [...]
+zoonitic gymnastic epauliere ethnocracy scrat culm havoc unschematized phytoma
ununiformly nonprofession phytoma unforkedness manilla visceral calycular
atlantite lithotresis Whilkut frontoorbital bucketer lampyrine seizing uvanite
nativeness vesperal parmelioid pictorially stiffish calycular Scanic
subirrigate velaric subofficer diwata bespin cromlech sheepskin ramosopalmate
expiscate ambitus papery photoelasticity oblongly pendulant ploration
trailmaking blightbird foursquare arrendati [...]
+Jerusalem Inger Munychian opacousness genii licitness uncontradictableness
antalgol constitutor monogoneutic sequacity cretaceous antalgol licitness
dispermy seelful equiconvex codisjunct commotion subirrigate genii sequacity
trophonema pyxie bucketer ordinant octogynous uninterpleaded widdle lineamental
outhue monander naught limpet sviatonosite farrantly prescriptible antalgol
culm rebilling apocalypst paleornithology putative pelvimetry poleax
osteopaedion cockal elastivity trophonema [...]
+seminonflammable Bertat macropterous hypochondriacism mastication Whilkut
prospectiveness Socraticism raphis percent palaeotheriodont rizzomed trophonema
crystallographical upswell metapolitics Pishquow prescriptible squdge
overstudiousness rebilling abscission putative terrestrially scrubbed limpet
Chiasmodontidae Joachimite laubanite metoxazine phallaceous ovoviviparous
seelful spot orthopedical ploration serosanguineous golem knob gallybeggar
arval bespin laurinoxylon triakistetrahedr [...]
+antiabolitionist planispheric astucious transcorporeal timbermonger boser
volcano michigan tickleproof stachyuraceous dosseret guitarist concretion
timbermonger posterishness unpeople manganosiderite pseudohalogen homeotypical
angiopathy shibuichi overbuilt bugre parabolicness frameable hysterogen
decidable undecorated frameable botchedly infravaginal danseuse antiadiaphorist
gala lampyrine unexplicit choralcelo volcano diwata vitally involatile
sesquiquintile repealableness lampyrine la [...]
+inertly unstressedly unfurbelowed metapolitics periclitation beneficent
migrainoid semantician brag Florissant helminthagogic doubtingness overcrown
ovoviviparous gelatinousness hysterogen reconciliable visceral unswanlike bot
reappreciate technopsychology monander transcorporeal ploration scrubbed
Pishquow astucious corelysis tendomucoid socioromantic concretion topsail
bucketer preagitate phallaceous gorilloid naprapath Dictograph Megaluridae
karyological trunnel rotular Helvidian limp [...]
+undinted pondside guanajuatite Whilkut scotching pseudoxanthine choralcelo
sesquiquintile haply neurodegenerative sarcologist signifier columniform
superindifference supraoesophageal porriginous unburnt unchatteled mendacity
ovopyriform unstressedly lammy untongued pyroacetic erlking rechar angina
overinstruct pumpkinification astronomize provedore stormy hellbender analgize
mustafina redesertion pneumonalgia deaf quarried dastardliness Macraucheniidae
oversand comparability crystallogra [...]
+golem chrysochrous Macraucheniidae tetragynian timbermonger apopenptic ascitic
bestill ell nonlustrous posttraumatic sedentariness quailberry endotheliomyoma
prescriptible discipular adatom idiotize wingable excerpt Prosobranchiata
glandularly hysterolysis volcano Animalivora taver quintette scabbardless
abusiveness foursquare commotion commandingness Orbitolina Ghent cresylite
Savitar unreprimanded hellbender transude swoony trillion overcontribute
spookdom counteractively tetrahedral u [...]
+rotular monstership Scanic topsail stradametrical prezygapophysial parastas
ethmopalatal Harpa Dawsonia Hu frameable pelf ornithodelphous packsack merciful
pompiloid unstipulated furacious apopenptic saponaceous dispermy Cephalodiscus
stachyuraceous pansophism hemimelus corelysis pachydermatoid roughcast coadvice
pictorially overcontribute unharmed frontoorbital saccharogenic socioromantic
guitarist lifter pterostigma unpatched Vaishnavism reciprocation
subsequentially whittle dinical ne [...]
+subirrigate coldfinch bettermost laryngic Fameuse afterpressure mericarp sud
toxihaemia unpredict Russifier bladderwort unfulminated vinegarish hymnic
Hydrangea Coniferae Fameuse lineamental packsack weism Cercosporella Hydrangea
plerome predisputant subirrigate Quakerishly heavenful ramosopalmate by
unlapsing pompiloid prezygapophysial unpeople ventricous farrantly
planosubulate corelysis figureheadship diatomaceous octogynous dipsomaniacal
spiciferous angina evictor deaf docimastical o [...]
+cheesecutter exprobratory bestill hypoplastral japanned greave metaphonical
totaquina oratorize mammonish incomprehensible incomprehensible parmelioid
Alethea iniquitously kerykeion cloy sertularian plerome scyphostoma commotion
unchatteled chorograph Confervales monogoneutic upswell pope diminutively
ramosopalmate mastication topsail planispheric unfurbelowed ventricous
unpredict chasmy cumbrousness pony pyxie parabolicness archistome bestill
epauliere tetrahedral Muscicapa exprobratory [...]
+Bassaris raphis incomprehensible visceral overstaid erlking amplexifoliate
silverhead underskin periarthritis Dictograph chacona zenick toxihaemia
preagitate folious propodiale familist plerome psychofugal apocalypst
unrepealably seelful codisjunct zoonitic pentosuria nonuple bicorporeal haply
hackneyed halloo diminutively antalgol sombreroed eulogization unbashfulness
collegian almud sleigher photoelasticity chilblain familist foursquare
mustafina cacuminal misthread serpentinic putativ [...]
+lampyrine oversand magnetooptics Hu agglomeratic Cimmerianism redecrease
friarhood Pincian parastas gunshop farrantly involatile impairment unimmortal
uncompromisingly trillion endotheliomyoma subfebrile dehairer pondside topsail
subfebrile Pincian widdle percent seminonflammable Mormyrus noreast sturdied
massedly Babylonism orgiastic ineunt nativeness dinical Arneb charioteer
pompiloid overbuilt unchatteled blurredness debromination superindifference
symbiogenetically greave Semecarpus [...]
+swearingly monilioid phytonic bicorporeal ununiformly quintette magnetooptics
cheesecutter bogydom acidophile subdrainage subdrainage nonlustrous toplike
leucophoenicite timbermonger rebilling manny dermorhynchous silverhead
rivethead seraphism lyrebird pentafid farrantly analgize valvula elemicin pyxie
glyphography jirble overstudiousness sural aurothiosulphuric tricae
magnetooptics Hydrangea botchedly pictorially ovopyriform bladderwort
phallaceous divinator consumptional nebular terre [...]
+theologal uloid Hu metapolitics Bertat unprovided cresylite interfraternal
rehabilitative mediateness swearingly mericarp Protestantize drome japanned
laryngic nebular snare undiffusive insatiately nonlustrous inexistency kenno
eurythermal pyrocatechol aquiline liberatress Zuludom pelf inductivity sawdust
yeelaman cyanoguanidine antalgol redescend glacierist toplike
triakistetrahedral depressingly choralcelo dithery retinize unanatomized
aneurism hackneyed ell parastas angiolymphoma spir [...]
+unexplicit ladhood spiciferous agglomeratic veterinarian incomprehensible
allotropic sportswomanship abscission planosubulate tramplike undeterring
focaloid chilblain scrubbed furacious cromlech alen inexistency pyroacetic
selectivity pomiferous flippantness bestill wingable depressingly
sportswomanship subirrigate arrendation papery generalizable adatom ungreat
Confervales chronist collegian appetible corbel benzoperoxide experientialist
cattimandoo parquet ten nummi unleavened ascitic [...]
+pseudoxanthine pyroacetic macropterous venialness valvulotomy pictorially
Tsonecan smokefarthings Pishquow temporomastoid semiangle kenno tomorrowness
Macraucheniidae enation spookdom returnability ultrasystematic stronghearted
ovoviviparous umangite groundneedle bromate orchiocatabasis untongued throbless
seminonflammable flatman leucophoenicite bogydom Prosobranchiata raphis
inventurous Florissant collegian mutter strammel squit Bertat dunkadoo
devilwise mastication flippantness unlaps [...]
+metapolitics tendomucoid gelatinousness ultraobscure doubtingness Pyrales
isopelletierin imprescribable subdentate poleax seeingness acidophile oversand
instructiveness Caphtor electrotechnics provedore bozal tautness porencephalous
angiolymphoma Vaishnavism ovopyriform kerykeion subdrainage Auriculariales
prescriber yote knob nigh agglomeratic unbashfulness bozal almud blightbird
tendomucoid goodwill lampyrine strammel chalcites folious pseudoxanthine
discipular drome sturdied quad lien [...]
+steprelationship triakistetrahedral nonmanufacture neurodegenerative saguran
cheesecutter aconitine devilwise nativeness manilla intuition noncrystallized
warlike sheepskin asparaginic psychofugal amplexifoliate infrastapedial
outguess larklike weism chrysochrous cretaceous impressor stereotypography
helpless cattimandoo uncombable Florissant Munnopsidae parquet Inger expiscate
genii imperceptivity topline Tamil antiscolic bespin depthwise pyroacetic Mesua
bubble Animalivora flippantness [...]
+culm exprobratory signifier halloo shallowish Yannigan angiopathy
thermoresistant hymnic Bushongo roughcast Babylonism prepavement bismuthiferous
zanyism goladar phoenicochroite mesymnion wherefrom laryngic pentafid Italical
subfoliar larklike hysterogen scabbiness wemless hypoid Whilkut physiologian
lineamental manganosiderite various ventricous ethnocracy waird chilblain
chargeably taver uncarefully mutter mesymnion orthopedical rehabilitative
oinomancy metapolitics pomiferous Lentibul [...]
+ordinant uncompromisingly strander shallowish detractive cuproiodargyrite
Macraucheniidae euphemious unpeople tramplike admissory imaginary enhedge
penult fossilism totaquina beadroll skyshine perculsive Pithecolobium
Christianopaganism cubit unpredict incomprehensible Spencerism Yannigan
charioteer sterilely unsupercilious opacousness mangonism arval champer
mechanist genii monstership undeterring pelvimetry steprelationship engrain
Saponaria prescriptible plugger tristich botchedly Tam [...]
+apocalypst nummi Uraniidae unlapsing trabecular cattimandoo corona
uninhabitedness hoove floatability euphonym overcrown temporomastoid Dictograph
spiranthic unurban paranephros scotching cyanoguanidine nigh periclitation
opacousness cloy chacona twinling squdge haply inventurous cinque biodynamics
slait scapuloradial corelysis proboscidiform patroller parquet hysterogen
farrantly concretion trip beadroll diopside Endomycetaceae thermanesthesia
cacuminal undercolored drome genii toplike [...]
+Bassaris topline depthwise barkometer Passiflorales oblongly unprovided
cervisial signifier tetchy sportswomanship Dodecatheon yeat spermaphyte wandoo
crystallographical superindifference dosseret roughcast nonprofession
antineuritic debellator lebensraum gunshop Bishareen returnability Munnopsidae
shallowish arsenide stapedius sertularian overinstruct Sebastian weism
impressor pneumonalgia Hu subdentate perfunctory liberatress Mesua Hu
opacousness Inger paranephros tomorn immatchable sw [...]
+diplomatize toxihaemia bladderwort preagitate bozal pelvimetry hellbender
interruptedness pachydermatoid nigh epidymides charioteer Vaishnavism
columniform idiotize transcorporeal phallaceous rehabilitative Fouquieria
intuition valvula molecule benthonic metapolitics Fameuse porencephalous
acidophile tetchy biodynamics paranephros arrendation testa scabbardless
various nigh bettermost cockal stewardship uniarticular tonsure naught jirble
Animalivora uloid ticktick steprelationship impugn [...]
diff --git a/storm-client/src/jvm/org/apache/storm/Config.java
b/storm-client/src/jvm/org/apache/storm/Config.java
index 79142a9c2..8d3fc51cc 100644
--- a/storm-client/src/jvm/org/apache/storm/Config.java
+++ b/storm-client/src/jvm/org/apache/storm/Config.java
@@ -1485,6 +1485,27 @@ public class Config extends HashMap<String, Object> {
*/
@IsString
public static final String STORM_META_SERIALIZATION_DELEGATE =
"storm.meta.serialization.delegate";
+ /**
+ * Topology configuration to enable compression of serialized tuples
during inter-worker network transfer.
+ * When set to {@code true}, tuples emitted by this component will be
compressed prior to being sent
+ * over the network to remote worker processes. This is highly recommended
for topologies exchanging
+ * large payloads (e.g., entire lines of text or large data blocks) to
significantly reduce network I/O.
+ * Default: {@code false} (Disabled by default to prevent unexpected CPU
overhead).
+ */
+ @IsBoolean
+ public static final String TOPOLOGY_TUPLE_COMPRESSION_ENABLE =
"topology.tuple.compression.enable";
+ /**
+ * Topology configuration specifying the minimum size threshold (in bytes)
for compressing serialized tuples
+ * during inter-worker network transfer.
+ * When the serialized byte array of a tuple exceeds this threshold, it
will be compressed
+ * prior to being transmitted over the network to a remote worker process.
This optimizes network I/O
+ * for large payloads (such as text blocks or massive objects) without
wasting cycles on small data.
+ * Set to 0 to bypass the size check and compress all tuples regardless of
size.
+ * Default: {@code 1460} bytes (The typical maximum segment size [MSS] for
a standard
+ * Ethernet TCP payload, preventing compression on packets that already
fit within a single network frame).
+ */
+ @IsPositiveNumber(includeZero = true)
+ public static final String TOPOLOGY_TUPLE_COMPRESSION_THRESHOLD =
"topology.tuple.compression.threshold";
/**
* GZIP max decompression bytes. Defaults to 104857600 (100MB).
*/
@@ -1503,6 +1524,11 @@ public class Config extends HashMap<String, Object> {
*/
@IsPositiveNumber(includeZero = false)
public static final String STORM_COMPRESSION_ZSTD_MAX_DECOMPRESSED_BYTES =
"storm.compression.zstd.max.decompressed.bytes";
+ /**
+ * Max decompression bytes for tuples. Defaults to 10485760 (10MB).
+ */
+ @IsPositiveNumber(includeZero = false)
+ public static final String
TOPOLOGY_TUPLE_COMPRESSION_MAX_DECOMPRESSED_BYTES =
"topology.tuple.compression.max.decompressed.bytes";
/**
* Configure the topology metrics reporters to be used on workers.
*/
diff --git
a/storm-client/src/jvm/org/apache/storm/serialization/KryoTupleDeserializer.java
b/storm-client/src/jvm/org/apache/storm/serialization/KryoTupleDeserializer.java
index 0d9f6b9fe..a310eac9c 100644
---
a/storm-client/src/jvm/org/apache/storm/serialization/KryoTupleDeserializer.java
+++
b/storm-client/src/jvm/org/apache/storm/serialization/KryoTupleDeserializer.java
@@ -16,27 +16,65 @@ import com.esotericsoftware.kryo.io.Input;
import java.io.IOException;
import java.util.List;
import java.util.Map;
+import org.apache.storm.Config;
+import org.apache.storm.generated.ComponentCommon;
import org.apache.storm.task.GeneralTopologyContext;
import org.apache.storm.tuple.MessageId;
import org.apache.storm.tuple.TupleImpl;
+import org.apache.storm.utils.ObjectReader;
+import org.apache.storm.utils.Utils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
public class KryoTupleDeserializer implements ITupleDeserializer {
- private GeneralTopologyContext context;
- private KryoValuesDeserializer kryo;
- private SerializationFactory.IdDictionary ids;
- private Input kryoInput;
+ private static final Integer DEFAULT_MAX_DECOMPRESSED_BYTES = 10 * 1024 *
1024; // 10MBytes
+ public static final Logger LOG =
LoggerFactory.getLogger(KryoTupleDeserializer.class);
+ public static final String FAILED_TO_DESERIALIZE_TUPLE = "Failed to
deserialize tuple";
+ private final GeneralTopologyContext context;
+ private final KryoValuesDeserializer kryo;
+ private final SerializationFactory.IdDictionary ids;
+ private final Input kryoInput;
+ private final int maxZstdDecompressedBytes;
+ private final boolean anyTupleCompressionEnabled;
public KryoTupleDeserializer(final Map<String, Object> conf, final
GeneralTopologyContext context) {
kryo = new KryoValuesDeserializer(conf);
this.context = context;
ids = new SerializationFactory.IdDictionary(context.getRawTopology());
kryoInput = new Input(1);
+ maxZstdDecompressedBytes =
ObjectReader.getInt(conf.get(Config.TOPOLOGY_TUPLE_COMPRESSION_MAX_DECOMPRESSED_BYTES),
+ DEFAULT_MAX_DECOMPRESSED_BYTES);
+ anyTupleCompressionEnabled = isTupleCompressionEnabled(conf, context);
}
@Override
public TupleImpl deserialize(byte[] ser) {
+ // check zstd header if at least one component is compressing tuples.
+ if (anyTupleCompressionEnabled && Utils.ZstdUtils.isZstd(ser)) {
+ try {
+ byte[] decompressed = Utils.ZstdUtils.decompress(ser,
this.maxZstdDecompressedBytes);
+ return deserializeTuple(decompressed);
+ } catch (RuntimeException e) {
+ if (e.getMessage() != null &&
e.getMessage().contains(FAILED_TO_DESERIALIZE_TUPLE)) {
+ // isZstd() false positive: a raw Kryo tuple's first 4
bytes matched ZSTD_MAGIC_HEADER by chance.
+ // This is astronomically unlikely in practice. Because
ZSTD_MAGIC_HEADER (0xFD2FB528) is little-endian
+ // on the wire, the first byte checked is 0x28. A Kryo
writeInt(taskId, true) of 40 yields exactly 0x28.
+ // The collision is prevented not by the taskId range, but
by the second field (streamId),
+ // which would rigidly have to equal 6069 to match the
remaining magic bytes.
+ // Branch retained for correctness in case of an
accidental collision.
+ LOG.debug("isZstd() false positive: raw Kryo tuple matched
ZSTD_MAGIC_HEADER (0xFD2FB528).");
+ return deserializeTuple(ser);
+ } else {
+ throw e;
+ }
+ }
+ }
+ return deserializeTuple(ser);
+ }
+
+ private TupleImpl deserializeTuple(byte[] data) {
try {
- kryoInput.setBuffer(ser);
+ kryoInput.setBuffer(data, 0, data.length);
int taskId = kryoInput.readInt(true);
int streamId = kryoInput.readInt(true);
String componentName = context.getComponentId(taskId);
@@ -45,7 +83,21 @@ public class KryoTupleDeserializer implements
ITupleDeserializer {
List<Object> values = kryo.deserializeFrom(kryoInput);
return new TupleImpl(context, values, componentName, taskId,
streamName, id);
} catch (IOException e) {
- throw new RuntimeException(e);
+ throw new RuntimeException(FAILED_TO_DESERIALIZE_TUPLE, e);
+ }
+ }
+
+ private static boolean isTupleCompressionEnabled(final Map<String, Object>
conf, final GeneralTopologyContext context) {
+ if
(ObjectReader.getBoolean(conf.get(Config.TOPOLOGY_TUPLE_COMPRESSION_ENABLE),
false)) {
+ return true;
+ }
+ for (String componentId : context.getComponentIds()) {
+ ComponentCommon common = context.getComponentCommon(componentId);
+ Map<String, Object> componentConf =
Utils.parseJson(common.get_json_conf());
+ if
(ObjectReader.getBoolean(componentConf.get(Config.TOPOLOGY_TUPLE_COMPRESSION_ENABLE),
false)) {
+ return true;
+ }
}
+ return false;
}
}
diff --git
a/storm-client/src/jvm/org/apache/storm/serialization/KryoTupleSerializer.java
b/storm-client/src/jvm/org/apache/storm/serialization/KryoTupleSerializer.java
index 3b6a9f15a..7691faf06 100644
---
a/storm-client/src/jvm/org/apache/storm/serialization/KryoTupleSerializer.java
+++
b/storm-client/src/jvm/org/apache/storm/serialization/KryoTupleSerializer.java
@@ -14,19 +14,32 @@ package org.apache.storm.serialization;
import com.esotericsoftware.kryo.io.Output;
import java.io.IOException;
+import java.util.Arrays;
import java.util.Map;
+import org.apache.storm.Config;
import org.apache.storm.task.GeneralTopologyContext;
import org.apache.storm.tuple.Tuple;
+import org.apache.storm.utils.ObjectReader;
+import org.apache.storm.utils.Utils;
public class KryoTupleSerializer implements ITupleSerializer {
- private KryoValuesSerializer kryo;
- private SerializationFactory.IdDictionary ids;
- private Output kryoOut;
+ private static final int DEFAULT_COMPRESSION_THRESHOLD = 1460;
+ private static final Integer DEFAULT_ZSTD_COMPRESSION_LEVEL = 3;
+
+ private final KryoValuesSerializer kryo;
+ private final SerializationFactory.IdDictionary ids;
+ private final Output kryoOut;
+ private final boolean isCompressionEnabled;
+ private final int compressionThreshold;
+ private final int zstdCompressionLevel;
public KryoTupleSerializer(final Map<String, Object> conf, final
GeneralTopologyContext context) {
kryo = new KryoValuesSerializer(conf);
kryoOut = new Output(2000, 2000000000);
ids = new SerializationFactory.IdDictionary(context.getRawTopology());
+ isCompressionEnabled =
ObjectReader.getBoolean(conf.get(Config.TOPOLOGY_TUPLE_COMPRESSION_ENABLE),
false);
+ compressionThreshold =
ObjectReader.getInt(conf.get(Config.TOPOLOGY_TUPLE_COMPRESSION_THRESHOLD),
DEFAULT_COMPRESSION_THRESHOLD);
+ zstdCompressionLevel =
ObjectReader.getInt(conf.get(Config.STORM_COMPRESSION_ZSTD_LEVEL),
DEFAULT_ZSTD_COMPRESSION_LEVEL);
}
@Override
@@ -38,7 +51,15 @@ public class KryoTupleSerializer implements ITupleSerializer
{
kryoOut.writeInt(ids.getStreamId(tuple.getSourceComponent(),
tuple.getSourceStreamId()), true);
tuple.getMessageId().serialize(kryoOut);
kryo.serializeInto(tuple.getValues(), kryoOut);
- return kryoOut.toBytes();
+
+ byte[] rawBytes = kryoOut.getBuffer();
+ int dataLength = kryoOut.position();
+
+ if (this.isCompressionEnabled && dataLength >
this.compressionThreshold) {
+ return Utils.ZstdUtils.compress(rawBytes, 0, dataLength,
this.zstdCompressionLevel);
+ } else {
+ return Arrays.copyOf(rawBytes, dataLength);
+ }
} catch (IOException e) {
throw new RuntimeException(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 3590583ad..e57e7f3f0 100644
--- a/storm-client/src/jvm/org/apache/storm/utils/Utils.java
+++ b/storm-client/src/jvm/org/apache/storm/utils/Utils.java
@@ -61,6 +61,7 @@ import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NavigableMap;
+import java.util.Objects;
import java.util.Set;
import java.util.Stack;
import java.util.TreeMap;
@@ -1044,28 +1045,39 @@ public class Utils {
}
/**
- * Compresses the provided byte array using Zstandard.
+ * Compresses a slice of 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 offset the start offset of the slice to compress.
+ * @param length the number of bytes to compress starting at {@code
offset}.
* @param compressionLevel the zstd compression level.
- * @return a compressed byte array, or the original array if
null/empty.
+ * @return a compressed byte array, or an empty array if {@code data}
is null or {@code length} is 0.
+ * @throws IndexOutOfBoundsException if {@code offset} and {@code
length} describe a slice
+ * that falls outside the bounds of
{@code data}.
* @throws RuntimeException wrapping an {@link IOException} if the
compression fails.
*/
- public static byte[] compress(byte[] data, int compressionLevel) {
+ public static byte[] compress(byte[] data, int offset, int length, int
compressionLevel) {
if (data == null || data.length == 0) {
return data;
}
- try (ByteArrayOutputStream bos = new
ByteArrayOutputStream(data.length)) {
+ if (length == 0) {
+ return new byte[0];
+ }
+
+ // Validate the slice up front so we fail clearly before opening
any streams.
+ Objects.checkFromIndexSize(offset, length, data.length);
+
+ try (ByteArrayOutputStream bos = new
ByteArrayOutputStream(length)) {
try (ZstdCompressorOutputStream zstdOut =
ZstdCompressorOutputStream.builder()
.setOutputStream(bos)
.setBufferSize(BUFFER_SIZE) // impacts on compression
ratio
.setLevel(compressionLevel)
.get()) {
- zstdOut.write(data);
+ zstdOut.write(data, offset, length); // Write the slice
directly
zstdOut.finish();
}
return bos.toByteArray();
@@ -1074,6 +1086,24 @@ public class Utils {
}
}
+ /**
+ * 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;
+ }
+ return compress(data, 0, data.length, compressionLevel);
+ }
+
/**
* Decompresses a Zstandard-compressed byte array.
*
diff --git
a/storm-client/test/jvm/org/apache/storm/serialization/KryoTupleSerializerDeserializerTest.java
b/storm-client/test/jvm/org/apache/storm/serialization/KryoTupleSerializerDeserializerTest.java
new file mode 100644
index 000000000..bbd036970
--- /dev/null
+++
b/storm-client/test/jvm/org/apache/storm/serialization/KryoTupleSerializerDeserializerTest.java
@@ -0,0 +1,373 @@
+/**
+ * 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.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.storm.Config;
+import org.apache.storm.generated.ComponentCommon;
+import org.apache.storm.generated.StormTopology;
+import org.apache.storm.shade.net.minidev.json.JSONValue;
+import org.apache.storm.task.GeneralTopologyContext;
+import org.apache.storm.testing.TestWordCounter;
+import org.apache.storm.testing.TestWordSpout;
+import org.apache.storm.topology.TopologyBuilder;
+import org.apache.storm.tuple.Fields;
+import org.apache.storm.tuple.MessageId;
+import org.apache.storm.tuple.TupleImpl;
+import org.apache.storm.tuple.Values;
+import org.apache.storm.utils.Utils;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit tests for {@link KryoTupleSerializer} and {@link
KryoTupleDeserializer}, covering the compressed and
+ * uncompressed code paths, round-trip fidelity, mixing both encodings through
a single (de)serializer instance,
+ * and the negative/error paths.
+ */
+public class KryoTupleSerializerDeserializerTest {
+
+ private static final String SOURCE_COMPONENT = "1";
+ private static final String DEST_COMPONENT = "2";
+ private static final int SOURCE_TASK_ID = 1;
+
+ private GeneralTopologyContext context;
+
+ @BeforeEach
+ public void setup() {
+ StormTopology topology = createStormTopology();
+ context = mock(GeneralTopologyContext.class);
+ when(context.getRawTopology()).thenReturn(topology);
+
when(context.getComponentId(SOURCE_TASK_ID)).thenReturn(SOURCE_COMPONENT);
+ when(context.doSanityCheck()).thenReturn(false);
+ }
+
+ private StormTopology createStormTopology() {
+ TopologyBuilder builder = new TopologyBuilder();
+ builder.setSpout(SOURCE_COMPONENT, new TestWordSpout(true), 1);
+ builder.setBolt(DEST_COMPONENT, new TestWordCounter(),
1).fieldsGrouping(SOURCE_COMPONENT, new Fields("word"));
+ return builder.createTopology();
+ }
+
+ private Map<String, Object> baseConf() {
+ return new HashMap<>(Utils.readStormConfig());
+ }
+
+ private Map<String, Object> compressionEnabledConf(int threshold) {
+ Map<String, Object> conf = baseConf();
+ conf.put(Config.TOPOLOGY_TUPLE_COMPRESSION_ENABLE, true);
+ conf.put(Config.TOPOLOGY_TUPLE_COMPRESSION_THRESHOLD, threshold);
+ conf.put(Config.STORM_COMPRESSION_ZSTD_LEVEL, 3);
+ return conf;
+ }
+
+ private void enableComponentLevelCompression(String componentId) {
+ enableComponentLevelCompression(context, componentId);
+ }
+
+ private void enableComponentLevelCompression(GeneralTopologyContext ctx,
String componentId) {
+ Map<String, Object> componentConf = new HashMap<>();
+ componentConf.put(Config.TOPOLOGY_TUPLE_COMPRESSION_ENABLE, true);
+ ComponentCommon common = mock(ComponentCommon.class);
+
when(common.get_json_conf()).thenReturn(JSONValue.toJSONString(componentConf));
+
when(ctx.getComponentIds()).thenReturn(Collections.singleton(componentId));
+ when(ctx.getComponentCommon(componentId)).thenReturn(common);
+ }
+
+ private GeneralTopologyContext newContext() {
+ GeneralTopologyContext ctx = mock(GeneralTopologyContext.class);
+ when(ctx.getRawTopology()).thenReturn(createStormTopology());
+ when(ctx.getComponentId(SOURCE_TASK_ID)).thenReturn(SOURCE_COMPONENT);
+ when(ctx.doSanityCheck()).thenReturn(false);
+ return ctx;
+ }
+
+ @Test
+ public void testRoundTripUncompressedWhenCompressionDisabled() {
+ Map<String, Object> conf = baseConf(); // compression disabled by
default
+ KryoTupleSerializer serializer = new KryoTupleSerializer(conf,
context);
+ KryoTupleDeserializer deserializer = new KryoTupleDeserializer(conf,
context);
+
+ TupleImpl original = tuple(new Values("hello", 42, bigString(8192)),
MessageId.makeRootId(7L, 99L));
+ byte[] bytes = serializer.serialize(original);
+
+ assertFalse(Utils.ZstdUtils.isZstd(bytes), "compression disabled must
never emit a zstd frame");
+ assertSameTuple(original, deserializer.deserialize(bytes));
+ }
+
+ @Test
+ public void testRoundTripUncompressedWhenBelowThreshold() {
+ Map<String, Object> conf = compressionEnabledConf(1460);
+ KryoTupleSerializer serializer = new KryoTupleSerializer(conf,
context);
+ KryoTupleDeserializer deserializer = new KryoTupleDeserializer(conf,
context);
+
+ TupleImpl original = tuple(new Values("small", 1),
MessageId.makeUnanchored());
+ byte[] bytes = serializer.serialize(original);
+
+ assertFalse(Utils.ZstdUtils.isZstd(bytes), "payload below threshold
must not be compressed");
+ assertSameTuple(original, deserializer.deserialize(bytes));
+ }
+
+ @Test
+ public void testRoundTripCompressedWhenAboveThreshold() {
+ Map<String, Object> conf = compressionEnabledConf(1460);
+ KryoTupleSerializer serializer = new KryoTupleSerializer(conf,
context);
+ KryoTupleDeserializer deserializer = new KryoTupleDeserializer(conf,
context);
+
+ TupleImpl original = tuple(new Values(bigString(64 * 1024)),
MessageId.makeRootId(5L, 11L));
+ byte[] bytes = serializer.serialize(original);
+
+ assertTrue(Utils.ZstdUtils.isZstd(bytes), "payload above threshold
must be compressed");
+ assertSameTuple(original, deserializer.deserialize(bytes));
+ }
+
+ @Test
+ public void testCompressionAtExactThreshold() {
+ Map<String, Object> conf = compressionEnabledConf(0);
+ KryoTupleSerializer serializer = new KryoTupleSerializer(conf,
context);
+ KryoTupleDeserializer deserializer = new KryoTupleDeserializer(conf,
context);
+
+ TupleImpl original = tuple(new Values("x"),
MessageId.makeUnanchored());
+ byte[] bytes = serializer.serialize(original);
+
+ assertTrue(Utils.ZstdUtils.isZstd(bytes), "threshold 0 must compress
every non-empty payload");
+ assertSameTuple(original, deserializer.deserialize(bytes));
+ }
+
+ @Test
+ public void testMixedCompressedAndUncompressedSameInstances() {
+ Map<String, Object> conf = compressionEnabledConf(1460);
+ KryoTupleSerializer serializer = new KryoTupleSerializer(conf,
context);
+ KryoTupleDeserializer deserializer = new KryoTupleDeserializer(conf,
context);
+
+ TupleImpl small = tuple(new Values("tiny", 1),
MessageId.makeRootId(1L, 2L));
+ TupleImpl large = tuple(new Values(bigString(32 * 1024), "tail"),
MessageId.makeRootId(3L, 4L));
+
+ // Buffer reuse across compressed and uncompressed payloads.
+ byte[] smallBytes1 = serializer.serialize(small);
+ byte[] largeBytes1 = serializer.serialize(large);
+ byte[] smallBytes2 = serializer.serialize(small);
+ byte[] largeBytes2 = serializer.serialize(large);
+
+ assertFalse(Utils.ZstdUtils.isZstd(smallBytes1));
+ assertTrue(Utils.ZstdUtils.isZstd(largeBytes1));
+ assertFalse(Utils.ZstdUtils.isZstd(smallBytes2));
+ assertTrue(Utils.ZstdUtils.isZstd(largeBytes2));
+
+ // deserializer must transparently handle both encodings, in any order.
+ assertSameTuple(large, deserializer.deserialize(largeBytes1));
+ assertSameTuple(small, deserializer.deserialize(smallBytes1));
+ assertSameTuple(small, deserializer.deserialize(smallBytes2));
+ assertSameTuple(large, deserializer.deserialize(largeBytes2));
+ }
+
+ @Test
+ public void testComponentLevelCompressionEnablesDecompressPath() {
+ enableComponentLevelCompression(SOURCE_COMPONENT);
+ KryoTupleSerializer serializer = new
KryoTupleSerializer(compressionEnabledConf(0), context);
+ KryoTupleDeserializer deserializer = new
KryoTupleDeserializer(baseConf(), context);
+
+ TupleImpl original = tuple(new Values(bigString(16 * 1024)),
MessageId.makeRootId(8L, 9L));
+ byte[] bytes = serializer.serialize(original);
+
+ assertTrue(Utils.ZstdUtils.isZstd(bytes));
+ assertSameTuple(original, deserializer.deserialize(bytes));
+ }
+
+ @Test
+ public void testDecompressPathGatedPerTopology() {
+ // Two distinct topologies sharing the exact same compressed frame on
the wire. The gating decision is
+ // per-topology (workers are per-topology), so the same bytes must be
decompressed by one and not the other.
+ GeneralTopologyContext compressingTopology = newContext();
+ enableComponentLevelCompression(compressingTopology,
SOURCE_COMPONENT); // at least one component compresses
+ GeneralTopologyContext plainTopology = newContext();
// no component enables compression
+
+ KryoTupleSerializer serializer = new
KryoTupleSerializer(compressionEnabledConf(0), compressingTopology);
+ TupleImpl original = tuple(new Values(bigString(16 * 1024)),
MessageId.makeRootId(8L, 9L));
+ byte[] bytes = serializer.serialize(original);
+ assertTrue(Utils.ZstdUtils.isZstd(bytes), "precondition: serializer
produced a compressed frame");
+
+ KryoTupleDeserializer compressingDeser = new
KryoTupleDeserializer(baseConf(), compressingTopology);
+ KryoTupleDeserializer plainDeser = new
KryoTupleDeserializer(baseConf(), plainTopology);
+
+ // Compressing topology: decompress path is taken, the frame
round-trips.
+ assertSameTuple(original, compressingDeser.deserialize(bytes));
+ // Plain topology: decompress path is skipped, the frame is treated as
a raw kryo tuple and fails to parse.
+ assertThrows(RuntimeException.class, () ->
plainDeser.deserialize(bytes));
+ }
+
+ @Test
+ public void testNoComponentCompressionSkipsDecompressPath() {
+ KryoTupleSerializer serializer = new
KryoTupleSerializer(compressionEnabledConf(0), context);
+ KryoTupleDeserializer deserializer = new
KryoTupleDeserializer(baseConf(), context);
+
+ byte[] bytes = serializer.serialize(tuple(new Values(bigString(16 *
1024)), MessageId.makeRootId(8L, 9L)));
+ assertTrue(Utils.ZstdUtils.isZstd(bytes), "precondition: serializer
produced a compressed frame");
+ assertThrows(RuntimeException.class, () ->
deserializer.deserialize(bytes));
+ }
+
+ @Test
+ public void testDecompressFailureFallsBackToRawTupleParsing() {
+ // Exercises the false-positive fallback in
KryoTupleDeserializer#deserialize: compression is enabled and
+ // isZstd() reports a match, but the decompress path surfaces a
"Failed to deserialize tuple" error.
+ enableComponentLevelCompression(SOURCE_COMPONENT); //
anyTupleCompressionEnabled == true
+ KryoTupleSerializer serializer = new KryoTupleSerializer(baseConf(),
context);
+ KryoTupleDeserializer deserializer = new
KryoTupleDeserializer(baseConf(), context);
+
+ TupleImpl original = tuple(new Values("hello", 42),
MessageId.makeRootId(5L, 11L));
+ byte[] raw = serializer.serialize(original);
+ assertFalse(Utils.ZstdUtils.isZstd(raw), "precondition: serializer
produced a raw, uncompressed tuple");
+
+ try (MockedStatic<Utils.ZstdUtils> mocked =
mockStatic(Utils.ZstdUtils.class)) {
+ // Force entry into the decompress branch, then make decompression
report a tuple-deserialization failure.
+ mocked.when(() -> Utils.ZstdUtils.isZstd(raw)).thenReturn(true);
+ mocked.when(() -> Utils.ZstdUtils.decompress(eq(raw), anyInt()))
+ .thenThrow(new
RuntimeException(KryoTupleDeserializer.FAILED_TO_DESERIALIZE_TUPLE));
+
+ TupleImpl result = deserializer.deserialize(raw);
+ assertSameTuple(original, result); // fallback re-parsed the raw
bytes and recovered the tuple
+ }
+ }
+
+ @Test
+ public void testDecompressFailureFallbackRethrowsWhenRawBytesAlsoInvalid()
{
+ // the raw bytes are not a valid kryo tuple either: the fallback parse
+ // must surface a RuntimeException rather than silently returning a
bogus tuple.
+ enableComponentLevelCompression(SOURCE_COMPONENT);
+ KryoTupleDeserializer deserializer = new
KryoTupleDeserializer(baseConf(), context);
+
+ byte[] raw = {1, 2, 3, 4, 5, 6, 7, 8};
+ try (MockedStatic<Utils.ZstdUtils> mocked =
mockStatic(Utils.ZstdUtils.class)) {
+ mocked.when(() -> Utils.ZstdUtils.isZstd(raw)).thenReturn(true);
+ mocked.when(() -> Utils.ZstdUtils.decompress(eq(raw), anyInt()))
+ .thenThrow(new
RuntimeException(KryoTupleDeserializer.FAILED_TO_DESERIALIZE_TUPLE));
+
+ assertThrows(RuntimeException.class, () ->
deserializer.deserialize(raw));
+ }
+ }
+
+ // corner cases
+
+ @Test
+ public void testRoundTripEmptyValues() {
+ Map<String, Object> conf = baseConf();
+ KryoTupleSerializer serializer = new KryoTupleSerializer(conf,
context);
+ KryoTupleDeserializer deserializer = new KryoTupleDeserializer(conf,
context);
+
+ TupleImpl original = tuple(new Values(), MessageId.makeUnanchored());
+ TupleImpl result =
deserializer.deserialize(serializer.serialize(original));
+ assertSameTuple(original, result);
+ assertTrue(result.getValues().isEmpty());
+ }
+
+ @Test
+ public void testRoundTripNullValue() {
+ Map<String, Object> conf = baseConf();
+ KryoTupleSerializer serializer = new KryoTupleSerializer(conf,
context);
+ KryoTupleDeserializer deserializer = new KryoTupleDeserializer(conf,
context);
+
+ TupleImpl original = tuple(new Values("a", null, "b"),
MessageId.makeUnanchored());
+ assertSameTuple(original,
deserializer.deserialize(serializer.serialize(original)));
+ }
+
+ // negative tests
+
+ @Test
+ public void testDeserializeGarbageBytesThrows() {
+ KryoTupleDeserializer deserializer = new
KryoTupleDeserializer(baseConf(), context);
+ // Not a zstd frame (length < 4 keeps isZstd false) and not a valid
kryo tuple.
+ assertThrows(RuntimeException.class, () ->
deserializer.deserialize(new byte[]{1, 2, 3}));
+ }
+
+ @Test
+ public void testDeserializeEmptyBytesThrows() {
+ KryoTupleDeserializer deserializer = new
KryoTupleDeserializer(baseConf(), context);
+ assertThrows(RuntimeException.class, () ->
deserializer.deserialize(new byte[0]));
+ }
+
+ @Test
+ public void testDeserializeZstdMagicButInvalidFrameThrows() {
+ enableComponentLevelCompression(SOURCE_COMPONENT);
+ KryoTupleDeserializer deserializer = new
KryoTupleDeserializer(baseConf(), context);
+ // First 4 bytes are the little-endian zstd magic 0xFD2FB528 so
isZstd() is true, but the rest is
+ // not a valid zstd frame. decompress() throws, the false-positive
fallback re-parses the raw bytes,
+ // which are also not a valid kryo tuple -> RuntimeException.
+ byte[] fakeZstd = new byte[]{(byte) 0x28, (byte) 0xB5, (byte) 0x2F,
(byte) 0xFD, 0x00, 0x01, 0x02, 0x03};
+ assertTrue(Utils.ZstdUtils.isZstd(fakeZstd));
+ assertThrows(RuntimeException.class, () ->
deserializer.deserialize(fakeZstd));
+ }
+
+ @Test
+ public void testDeserializeCompressedExceedingMaxDecompressedBytesThrows()
{
+ // A genuinely compressed tuple, but the deserializer is configured
with a tiny decompression cap.
+ // decompress() throws ("threshold exceeded"), the fallback re-parses
the still-compressed raw bytes,
+ // which are not a valid kryo tuple -> RuntimeException.
+ enableComponentLevelCompression(SOURCE_COMPONENT);
+ KryoTupleSerializer serializer = new
KryoTupleSerializer(compressionEnabledConf(0), context);
+ Map<String, Object> deserConf = baseConf();
+
deserConf.put(Config.TOPOLOGY_TUPLE_COMPRESSION_MAX_DECOMPRESSED_BYTES, 1);
+ KryoTupleDeserializer deserializer = new
KryoTupleDeserializer(deserConf, context);
+
+ byte[] bytes = serializer.serialize(tuple(new Values(bigString(8192)),
MessageId.makeUnanchored()));
+ assertTrue(Utils.ZstdUtils.isZstd(bytes));
+ assertThrows(RuntimeException.class, () ->
deserializer.deserialize(bytes));
+ }
+
+ @Test
+ public void testSerializeUnregisteredTypeWithoutJavaFallbackThrows() {
+ Map<String, Object> conf = baseConf();
+ conf.put(Config.TOPOLOGY_FALL_BACK_ON_JAVA_SERIALIZATION, false);
+ KryoTupleSerializer serializer = new KryoTupleSerializer(conf,
context);
+
+ TupleImpl original = tuple(new Values(new UnregisteredType()),
MessageId.makeUnanchored());
+ assertThrows(RuntimeException.class, () ->
serializer.serialize(original));
+ }
+
+ private static class UnregisteredType {
+ @SuppressWarnings("unused")
+ private final int field = 1;
+ }
+
+ private TupleImpl tuple(List<Object> values, MessageId id) {
+ return new TupleImpl(context, values, SOURCE_COMPONENT,
SOURCE_TASK_ID, Utils.DEFAULT_STREAM_ID, id);
+ }
+
+ private void assertSameTuple(TupleImpl expected, TupleImpl actual) {
+ assertEquals(expected.getValues(), actual.getValues());
+ assertEquals(expected.getSourceTask(), actual.getSourceTask());
+ assertEquals(expected.getSourceComponent(),
actual.getSourceComponent());
+ assertEquals(expected.getSourceStreamId(), actual.getSourceStreamId());
+ assertEquals(expected.getMessageId(), actual.getMessageId());
+ }
+
+ private String bigString(int size) {
+ StringBuilder sb = new StringBuilder(size);
+ for (int i = 0; i < size; i++) {
+ sb.append((char) ('a' + (i % 26)));
+ }
+ return sb.toString();
+ }
+}
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 d29f41d50..6086f5095 100644
--- a/storm-client/test/jvm/org/apache/storm/utils/UtilsTest.java
+++ b/storm-client/test/jvm/org/apache/storm/utils/UtilsTest.java
@@ -566,6 +566,56 @@ public class UtilsTest {
}
}
+ @Test
+ void compress_offsetLength_compressesOnlyTheSlice() {
+ byte[] prefix = "PREFIX-".getBytes(StandardCharsets.UTF_8);
+ byte[] payload = "Hello, ZSTD!".getBytes(StandardCharsets.UTF_8);
+ byte[] suffix = "-SUFFIX".getBytes(StandardCharsets.UTF_8);
+
+ byte[] framed = new byte[prefix.length + payload.length +
suffix.length];
+ System.arraycopy(prefix, 0, framed, 0, prefix.length);
+ System.arraycopy(payload, 0, framed, prefix.length, payload.length);
+ System.arraycopy(suffix, 0, framed, prefix.length + payload.length,
suffix.length);
+
+ byte[] compressedSlice = Utils.ZstdUtils.compress(framed,
prefix.length, payload.length, 3);
+ byte[] decompressed = Utils.ZstdUtils.decompress(compressedSlice, 1024
* 1024);
+ assertArrayEquals(payload, decompressed,
+ "Compressing a slice then decompressing must recover only that
slice");
+
+ // The slice path must match the full-array path when the slice spans
the whole array.
+ byte[] compressedWhole = Utils.ZstdUtils.compress(payload, 3);
+ byte[] compressedFullSlice = Utils.ZstdUtils.compress(payload, 0,
payload.length, 3);
+ assertArrayEquals(compressedWhole, compressedFullSlice,
+ "Whole-array slice must produce the same output as the
convenience overload");
+ }
+
+ @Test
+ void compress_offsetLength_zeroLengthOrNull() {
+ byte[] empty = Utils.ZstdUtils.compress(new byte[]{1, 2, 3}, 1, 0, 3);
+ assertNotNull(empty);
+ assertEquals(0, empty.length);
+
+ byte[] fromNull = Utils.ZstdUtils.compress(null, 0, 0, 3);
+ assertNull(fromNull);
+
+ byte[] emptyArray = new byte[0];
+ byte[] fromEmpty = Utils.ZstdUtils.compress(emptyArray, 0, 0, 3);
+ assertEquals(emptyArray, fromEmpty);
+ }
+
+ @Test
+ void compress_offsetLength_outOfBounds_throws() {
+ byte[] data = {1, 2, 3, 4};
+ assertThrows(IndexOutOfBoundsException.class,
+ () -> Utils.ZstdUtils.compress(data, -1, 2, 3));
+ assertThrows(IndexOutOfBoundsException.class,
+ () -> Utils.ZstdUtils.compress(data, 0, -1, 3));
+ assertThrows(IndexOutOfBoundsException.class,
+ () -> Utils.ZstdUtils.compress(data, 3, 2, 3));
+ assertThrows(IndexOutOfBoundsException.class,
+ () -> Utils.ZstdUtils.compress(data, 5, 1, 3));
+ }
+
@Test
void compress_highEntropData_doesNotThrow() {
// Random-ish bytes — Zstd may not shrink them, but must not fail