This is an automated email from the ASF dual-hosted git repository.

GGraziadei pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/storm.git


The following commit(s) were added to refs/heads/master by this push:
     new 9dc65e94a Decouple the control plane from the data plane on receive 
queues (#8892)
9dc65e94a is described below

commit 9dc65e94aa9438caf6a576bed1f68d8f0760eb57
Author: Gianluca Graziadei <[email protected]>
AuthorDate: Wed Jul 15 20:04:03 2026 +0200

    Decouple the control plane from the data plane on receive queues (#8892)
    
    * decouple the control plane from the data plane on receive queues
    
    * address review feedback on receive-queue control lane
    
    - simplify Constants.isControlStreamId: drop the static-init bitmask fast
      path in favor of startsWith("__") + set lookup (same behavior, less to
      maintain)
    - document that __tick tuples may be dropped under saturation when the
      lane is enabled (Config javadoc + docs/Performance.md), so bolts using
      ticks for windowing/expiry tolerate an occasional miss
    - log a rate-limited WARN in JCQueue.tryPublishControl when a control
      tuple is dropped, so a stalled executor is visible in the logs and not
      only via the control_dropped_messages metric
    - note on the controlQueue field that close() does not drain the lane,
      matching recvQueue behavior
    - fix import order in JCQueueTest
---
 conf/defaults.yaml                                 |   2 +
 docs/Performance.md                                |  24 +++
 storm-client/src/jvm/org/apache/storm/Config.java  |  24 +++
 .../src/jvm/org/apache/storm/Constants.java        |  17 ++
 .../apache/storm/daemon/worker/WorkerState.java    |  15 +-
 .../jvm/org/apache/storm/executor/Executor.java    |  49 +++---
 .../apache/storm/executor/ExecutorTransfer.java    |   8 +
 .../src/jvm/org/apache/storm/utils/JCQueue.java    |  84 +++++++++-
 .../jvm/org/apache/storm/utils/JCQueueMetrics.java |  19 ++-
 .../test/jvm/org/apache/storm/ConstantsTest.java   |  61 +++++++
 .../executor/ExecutorTransferControlLaneTest.java  | 186 +++++++++++++++++++++
 .../jvm/org/apache/storm/utils/JCQueueTest.java    | 172 +++++++++++++++++++
 12 files changed, 629 insertions(+), 32 deletions(-)

diff --git a/conf/defaults.yaml b/conf/defaults.yaml
index d2b1c16eb..9682cf8bc 100644
--- a/conf/defaults.yaml
+++ b/conf/defaults.yaml
@@ -343,6 +343,8 @@ topology.transfer.batch.size: 1       # can be no larger 
than half of `topology.
 topology.executor.receive.buffer.size: 32768  # size of recv queue for spouts 
& bolts. Will be internally rounded up to next power of 2 (if not already a 
power of 2)
 topology.producer.batch.size: 1               # can be no larger than half of 
`topology.executor.receive.buffer.size`
 topology.producer.batch.dynamic: false        # when true, the producer batch 
size adapts between 1 and topology.producer.batch.size to reduce latency under 
light load. No effect unless topology.producer.batch.size > 1.
+topology.executor.receive.control.queue.enable: false  # when true, low-volume 
control tuples (flush/tick/metrics tick/feedback) are delivered via a dedicated 
control lane of the recv queue, drained before the data queue and exempt from 
backpressure.
+topology.executor.receive.control.buffer.size: 1024    # size of the control 
lane of the recv queue. Only used when 
topology.executor.receive.control.queue.enable is true. Will be internally 
rounded up to next power of 2 (if not already a power of 2)
 
 topology.batch.flush.interval.millis: 1  # Flush tuples are disabled if this 
is set to 0 or if (topology.producer.batch.size=1 and 
topology.transfer.batch.size=1).
 topology.spout.recvq.skips: 3  # Check recvQ once every N invocations of 
Spout's nextTuple() [when ACKs disabled]
diff --git a/docs/Performance.md b/docs/Performance.md
index 5ca0f4ad6..5774a8876 100644
--- a/docs/Performance.md
+++ b/docs/Performance.md
@@ -30,6 +30,30 @@ Very large message queues are also not desirable to deal 
with slow consumers. Be
 are large and often full, the messages will end up waiting longer in these 
queues at each step of the processing, leading to poor latency being
 reported on the Storm UI. Large queues also imply higher memory consumption 
especially if the queues are typically full.
 
+#### Control lane for system control tuples
+
+An executor's receive queue carries two very different kinds of traffic: the 
data tuples flowing between components, and low-volume time-driven
+system tuples (flush tuples, tick tuples, metrics ticks, upstream feedback) 
that keep the topology responsive. Because the queue is FIFO, a control
+tuple enqueued behind a full data backlog waits for the entire backlog to 
drain first, so data-plane saturation directly degrades control-plane
+latency.
+
+- `topology.executor.receive.control.queue.enable` (default `false`) : When 
enabled, each executor's receive queue gets a small dedicated *control
+lane* that is drained before the data queue on every consume pass. The 
time-driven system streams (`__flush`, `__tick`, `__metrics_tick`,
+`__feedback_tick`, `__feedback`) are routed to it at every ingress point 
(timer-generated tuples, intra-worker transfer, and inter-worker receive),
+insulating them from data-plane buffering, backpressure and overflow so that 
their delivery latency stays bounded even while the data plane
+saturates. Writes to the control lane are un-batched and never block: if the 
lane is full the tuple is dropped and counted (see the
+`receive-queue-control_dropped_messages` metric), which is safe because these 
signals are periodic and the next one arrives within the signal's
+period. The ack streams and the `__metrics` payload stream are 
volume-proportional to the data plane and stay on the data path, so acking,
+`max.spout.pending` throttling and at-least-once semantics are unaffected. 
Note that enabling this delivers control tuples ahead of co-enqueued data
+tuples; the whitelisted streams are wall-clock signals with no ordering 
contract against data, so this is safe.
+Note also that `__tick` is one of these control streams, so the drop-when-full 
behavior applies to user-facing tick tuples too: with the lane
+enabled a `__tick` tuple may occasionally be dropped under sustained 
saturation instead of blocking until delivered. Bolts that rely on tick tuples
+for windowing or expiry logic must tolerate an occasional missed tick — it is 
not only internal signals that can be dropped.
+
+- `topology.executor.receive.control.buffer.size` (default `1024`) : The size 
of the control lane. Control traffic is low-volume, so the default is
+ample; like the other queue sizes it is internally rounded up to the next 
power of 2. The control lane is excluded from the queue load reported to
+load-aware groupings.
+
 
 ## 2. Batch Size
 Producers can either write a batch of messages to the consumer's queue or 
write each message individually. This batch size can be configured.
diff --git a/storm-client/src/jvm/org/apache/storm/Config.java 
b/storm-client/src/jvm/org/apache/storm/Config.java
index 36b3bb78a..f9c09b897 100644
--- a/storm-client/src/jvm/org/apache/storm/Config.java
+++ b/storm-client/src/jvm/org/apache/storm/Config.java
@@ -766,6 +766,30 @@ public class Config extends HashMap<String, Object> {
     @IsPositiveNumber
     @IsInteger
     public static final String TOPOLOGY_EXECUTOR_RECEIVE_BUFFER_SIZE = 
"topology.executor.receive.buffer.size";
+    /**
+     * When enabled, each executor's receive queue gets a small, dedicated 
control lane that is drained before the
+     * data queue. Low-volume time-driven system tuples (specified here {@link 
Constants#SYSTEM_CONTROL_STREAM_IDS}) are routed to
+     * it, insulating them from data-plane buffering, backpressure and 
overflow so their delivery latency stays
+     * bounded while the data plane saturates. Control tuples are published to 
the lane without batching and without
+     * participating in backpressure; if the lane is full they are dropped 
(and counted), which is safe because these
+     * signals are periodic and the next one arrives within the signal's 
period. Ack and metrics payload streams are
+     * unaffected: they stay on the data path.
+     *
+     * <p>Note that {@code __tick} tuples travel on the control lane. When 
this is enabled a {@code __tick} tuple may
+     * occasionally be dropped under sustained saturation rather than blocking 
until delivered as it does with the lane
+     * off. Bolts that rely on tick tuples for windowing or expiry logic must 
therefore tolerate an occasional missed
+     * tick; it is not only internal signals that may be dropped. Default 
value: false.
+     */
+    @IsBoolean
+    public static final String TOPOLOGY_EXECUTOR_RECEIVE_CONTROL_QUEUE_ENABLE 
= "topology.executor.receive.control.queue.enable";
+    /**
+     * The size of the control lane of the receive queue for each executor. 
Only used when {@link
+     * #TOPOLOGY_EXECUTOR_RECEIVE_CONTROL_QUEUE_ENABLE} is set. Control 
traffic is low-volume, so a small buffer
+     * suffices; will be internally rounded up to the next power of 2. Default 
value: 1024.
+     */
+    @IsPositiveNumber
+    @IsInteger
+    public static final String TOPOLOGY_EXECUTOR_RECEIVE_CONTROL_BUFFER_SIZE = 
"topology.executor.receive.control.buffer.size";
     /**
      * The size of the transfer queue for each worker.
      */
diff --git a/storm-client/src/jvm/org/apache/storm/Constants.java 
b/storm-client/src/jvm/org/apache/storm/Constants.java
index 683b42402..72abcb74a 100644
--- a/storm-client/src/jvm/org/apache/storm/Constants.java
+++ b/storm-client/src/jvm/org/apache/storm/Constants.java
@@ -14,6 +14,7 @@ package org.apache.storm;
 
 import java.util.Arrays;
 import java.util.List;
+import java.util.Set;
 import org.apache.storm.coordination.CoordinatedBolt;
 
 
@@ -31,6 +32,22 @@ public class Constants {
     public static final String FEEDBACK_STREAM_ID = "__feedback";
     public static final String FEEDBACK_TICK_STREAM_ID = "__feedback_tick";
 
+    /**
+     * System streams carrying low-volume, time-driven control signals that 
may be routed to the executor receive
+     * queue's control lane (see {@code 
Config.TOPOLOGY_EXECUTOR_RECEIVE_CONTROL_QUEUE_ENABLE}). Tuples on these
+     * streams are wall-clock signals with no ordering contract against data 
tuples, so delivering them ahead of
+     * co-enqueued data tuples is safe, and dropping one when the control lane 
is full is self-healing because the
+     * next one arrives within the signal's period.
+     */
+    public static final Set<String> SYSTEM_CONTROL_STREAM_IDS =
+            Set.of(SYSTEM_TICK_STREAM_ID, SYSTEM_FLUSH_STREAM_ID, 
METRICS_TICK_STREAM_ID, FEEDBACK_STREAM_ID, FEEDBACK_TICK_STREAM_ID);
+
+    public static boolean isControlStreamId(String streamId) {
+        // All control stream ids start with "__"; the prefix check cheaply 
rejects the common data-stream
+        // case before falling through to the set lookup.
+        return streamId != null && streamId.startsWith("__") && 
SYSTEM_CONTROL_STREAM_IDS.contains(streamId);
+    }
+
     public static final Object TOPOLOGY = "topology";
     public static final String SYSTEM_TOPOLOGY = "system-topology";
     public static final String STORM_CONF = "storm-conf";
diff --git 
a/storm-client/src/jvm/org/apache/storm/daemon/worker/WorkerState.java 
b/storm-client/src/jvm/org/apache/storm/daemon/worker/WorkerState.java
index 496c503e9..59aceb8d6 100644
--- a/storm-client/src/jvm/org/apache/storm/daemon/worker/WorkerState.java
+++ b/storm-client/src/jvm/org/apache/storm/daemon/worker/WorkerState.java
@@ -578,6 +578,14 @@ public class WorkerState {
                 continue;
             }
 
+            // 0- route control tuples to the control lane: un-batched, ahead 
of any data backlog, exempt from
+            //    backpressure and overflow. If the lane is full the tuple is 
dropped and counted; control signals
+            //    are periodic, so the next one arrives within its period.
+            if (queue.isControlLaneEnabled() && 
Constants.isControlStreamId(tuple.getTuple().getSourceStreamId())) {
+                queue.tryPublishControl(tuple);
+                continue;
+            }
+
             // 1- try adding to main queue if its overflow is not empty
             if (queue.isEmptyOverflow()) {
                 if (queue.tryPublish(tuple)) {
@@ -739,6 +747,11 @@ public class WorkerState {
         Integer recvBatchSize = 
ObjectReader.getInt(topologyConf.get(Config.TOPOLOGY_PRODUCER_BATCH_SIZE));
         boolean dynamicBatch = 
ObjectReader.getBoolean(topologyConf.get(Config.TOPOLOGY_PRODUCER_BATCH_DYNAMIC),
 false);
         Integer overflowLimit = 
ObjectReader.getInt(topologyConf.get(Config.TOPOLOGY_EXECUTOR_OVERFLOW_LIMIT));
+        boolean controlQueueEnable =
+            
ObjectReader.getBoolean(topologyConf.get(Config.TOPOLOGY_EXECUTOR_RECEIVE_CONTROL_QUEUE_ENABLE),
 false);
+        int controlQueueSize = controlQueueEnable
+            ? 
ObjectReader.getInt(topologyConf.get(Config.TOPOLOGY_EXECUTOR_RECEIVE_CONTROL_BUFFER_SIZE),
 1024)
+            : 0;
 
         if (recvBatchSize > recvQueueSize / 2) {
             throw new 
IllegalArgumentException(Config.TOPOLOGY_PRODUCER_BATCH_SIZE + ":" + 
recvBatchSize
@@ -760,7 +773,7 @@ public class WorkerState {
             }
             receiveQueueMap.put(executor, new JCQueue("receive-queue" + 
executor.toString(), "receive-queue",
                                                       recvQueueSize, 
overflowLimit, recvBatchSize, backPressureWaitStrategy,
-                this.getTopologyId(), compId, taskIds, this.getPort(), 
metricRegistry, dynamicBatch));
+                this.getTopologyId(), compId, taskIds, this.getPort(), 
metricRegistry, dynamicBatch, controlQueueSize));
 
         }
         return receiveQueueMap;
diff --git a/storm-client/src/jvm/org/apache/storm/executor/Executor.java 
b/storm-client/src/jvm/org/apache/storm/executor/Executor.java
index 59f0894fb..d488a6263 100644
--- a/storm-client/src/jvm/org/apache/storm/executor/Executor.java
+++ b/storm-client/src/jvm/org/apache/storm/executor/Executor.java
@@ -577,17 +577,24 @@ public abstract class Executor implements Callable, 
JCQueue.Consumer {
                         new TupleImpl(workerTopologyContext, new 
Values(interval), Constants.SYSTEM_COMPONENT_ID,
                                 (int) Constants.SYSTEM_TASK_ID, 
Constants.METRICS_TICK_STREAM_ID);
                 AddressedTuple metricsTickTuple = new 
AddressedTuple(AddressedTuple.BROADCAST_DEST, tuple);
-                try {
-                    receiveQueue.publish(metricsTickTuple);
-                    receiveQueue.flush();  // avoid buffering
-                } catch (InterruptedException e) {
-                    LOG.warn("Thread interrupted when publishing metrics. 
Setting interrupt flag.");
-                    Thread.currentThread().interrupt();
-                    return;
-                }
+                publishTimerTuple(metricsTickTuple, "metrics tick tuple");
             }
         );
     }
+    
+    private void publishTimerTuple(AddressedTuple timerTuple, String 
logContext) {
+        if (receiveQueue.isControlLaneEnabled()) {
+            receiveQueue.tryPublishControl(timerTuple);
+            return;
+        }
+        try {
+            receiveQueue.publish(timerTuple);
+            receiveQueue.flush();  // avoid buffering
+        } catch (InterruptedException e) {
+            LOG.warn("Thread interrupted when publishing {}. Setting interrupt 
flag.", logContext);
+            Thread.currentThread().interrupt();
+        }
+    }
 
     /**
      * Collects the task ids of every upstream (source component) task. These 
are the recipients of
@@ -618,14 +625,7 @@ public abstract class Executor implements Callable, 
JCQueue.Consumer {
                         new TupleImpl(workerTopologyContext, new 
Values(interval), Constants.SYSTEM_COMPONENT_ID,
                                 (int) Constants.SYSTEM_TASK_ID, 
Constants.FEEDBACK_TICK_STREAM_ID);
                 AddressedTuple feedbackTickTuple = new 
AddressedTuple(AddressedTuple.BROADCAST_DEST, tuple);
-                try {
-                    receiveQueue.publish(feedbackTickTuple);
-                    receiveQueue.flush();  // avoid buffering
-                } catch (InterruptedException e) {
-                    LOG.warn("Thread interrupted when publishing upstream 
feedback tick. Setting interrupt flag.");
-                    Thread.currentThread().interrupt();
-                    return;
-                }
+                publishTimerTuple(feedbackTickTuple, "upstream feedback tick 
tuple");
             }
         );
     }
@@ -663,14 +663,7 @@ public abstract class Executor implements Callable, 
JCQueue.Consumer {
                                                         (int) 
Constants.SYSTEM_TASK_ID,
                                                         
Constants.SYSTEM_TICK_STREAM_ID);
                         AddressedTuple tickTuple = new 
AddressedTuple(AddressedTuple.BROADCAST_DEST, tuple);
-                        try {
-                            receiveQueue.publish(tickTuple);
-                            receiveQueue.flush(); // avoid buffering
-                        } catch (InterruptedException e) {
-                            LOG.warn("Thread interrupted when emitting tick 
tuple. Setting interrupt flag.");
-                            Thread.currentThread().interrupt();
-                            return;
-                        }
+                        publishTimerTuple(tickTuple, "tick tuple");
                     }
                 );
             }
@@ -683,13 +676,15 @@ public abstract class Executor implements Callable, 
JCQueue.Consumer {
         }
     }
 
-    // Called by flush-tuple-timer thread
+    // Called by flush-tuple-timer thread. Publishes to the control lane when 
enabled (insulating the flush signal
+    // from the data backlog); otherwise falls back to an un-batched write to 
the recvQueue as before.
     public boolean publishFlushTuple() {
-        if (receiveQueue.tryPublishDirect(flushTuple)) {
+        if (receiveQueue.tryPublishControl(flushTuple)) {
             LOG.debug("Published Flush tuple to: {} ", getComponentId());
             return true;
         } else {
-            LOG.debug("RecvQ is currently full, will retry publishing Flush 
Tuple later to : {}", getComponentId());
+            LOG.debug("Target queue (control lane or RecvQ) is currently full, 
will retry publishing Flush Tuple later to : {}",
+                getComponentId());
             return false;
         }
     }
diff --git 
a/storm-client/src/jvm/org/apache/storm/executor/ExecutorTransfer.java 
b/storm-client/src/jvm/org/apache/storm/executor/ExecutorTransfer.java
index ffd25c494..7052dce4a 100644
--- a/storm-client/src/jvm/org/apache/storm/executor/ExecutorTransfer.java
+++ b/storm-client/src/jvm/org/apache/storm/executor/ExecutorTransfer.java
@@ -17,6 +17,7 @@ import java.util.Map;
 import java.util.Queue;
 import java.util.concurrent.atomic.AtomicReferenceArray;
 import org.apache.storm.Config;
+import org.apache.storm.Constants;
 import org.apache.storm.daemon.worker.WorkerState;
 import org.apache.storm.serialization.KryoTupleSerializer;
 import org.apache.storm.task.WorkerTopologyContext;
@@ -99,6 +100,13 @@ public class ExecutorTransfer {
      */
     public boolean tryTransferLocal(AddressedTuple tuple, JCQueue localQueue, 
Queue<AddressedTuple> pendingEmits) {
         workerData.checkSerialize(threadLocalSerializer.get(), tuple);
+        if (localQueue.isControlLaneEnabled() && 
Constants.isControlStreamId(tuple.getTuple().getSourceStreamId())) {
+            // control tuples bypass batching, pendingEmits ordering and 
backpressure. If the control lane is full
+            // the tuple is dropped and counted; control signals are periodic, 
so the next one arrives within its
+            // period. Report the tuple as handled either way so it is never 
queued behind the data backlog.
+            localQueue.tryPublishControl(tuple);
+            return true;
+        }
         if (pendingEmits != null) {
             if (pendingEmits.isEmpty() && localQueue.tryPublish(tuple)) {
                 queuesToFlush.set(tuple.dest - indexingBase, localQueue);
diff --git a/storm-client/src/jvm/org/apache/storm/utils/JCQueue.java 
b/storm-client/src/jvm/org/apache/storm/utils/JCQueue.java
index 0c26158fa..5c4c69583 100644
--- a/storm-client/src/jvm/org/apache/storm/utils/JCQueue.java
+++ b/storm-client/src/jvm/org/apache/storm/utils/JCQueue.java
@@ -22,6 +22,8 @@ import java.io.Closeable;
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
 import org.apache.storm.metrics2.StormMetricRegistry;
 import org.apache.storm.policy.IWaitStrategy;
 import org.apache.storm.shade.org.jctools.queues.MessagePassingQueue;
@@ -38,6 +40,9 @@ public class JCQueue implements Closeable {
     private final MpscArrayQueue<Object> recvQueue;
     // only holds msgs from other workers (via WorkerTransfer), when recvQueue 
is full
     private final MpscUnboundedArrayQueue<Object> overflowQ;
+    // dedicated lane for low-volume control tuples (flush/tick/metrics 
tick/feedback), drained before recvQueue.
+    // Like recvQueue, this lane is not drained on close(): any tuples still 
buffered at shutdown are discarded.
+    private final MpscArrayQueue<Object> controlQueue;
     private final int overflowLimit; // ensures... overflowCount <= 
overflowLimit. if set to 0, disables overflow limiting.
     private final int producerBatchSz;
     private final boolean dynamicBatch;
@@ -48,6 +53,10 @@ public class JCQueue implements Closeable {
     private final IWaitStrategy backPressureWaitStrategy;
     private final String queueName;
 
+    // Throttle for the control-lane-full WARN so a stalled executor cannot 
flood the logs; log at most once per interval.
+    private static final long CONTROL_DROP_LOG_INTERVAL_NANOS = 
TimeUnit.SECONDS.toNanos(30);
+    private final AtomicLong lastControlDropLogNanos = new 
AtomicLong(Long.MIN_VALUE);
+
     public JCQueue(String queueName, String metricNamePrefix, int size, int 
overflowLimit, int producerBatchSz,
                    IWaitStrategy backPressureWaitStrategy, String topologyId, 
String componentId, List<Integer> taskIds,
                    int port, StormMetricRegistry metricRegistry) {
@@ -58,15 +67,23 @@ public class JCQueue implements Closeable {
     public JCQueue(String queueName, String metricNamePrefix, int size, int 
overflowLimit, int producerBatchSz,
                    IWaitStrategy backPressureWaitStrategy, String topologyId, 
String componentId, List<Integer> taskIds,
                    int port, StormMetricRegistry metricRegistry, boolean 
dynamicBatch) {
+        this(queueName, metricNamePrefix, size, overflowLimit, 
producerBatchSz, backPressureWaitStrategy, topologyId, componentId,
+                taskIds, port, metricRegistry, dynamicBatch, 0);
+    }
+
+    public JCQueue(String queueName, String metricNamePrefix, int size, int 
overflowLimit, int producerBatchSz,
+                   IWaitStrategy backPressureWaitStrategy, String topologyId, 
String componentId, List<Integer> taskIds,
+                   int port, StormMetricRegistry metricRegistry, boolean 
dynamicBatch, int controlQueueSize) {
         this.queueName = queueName;
         this.dynamicBatch = dynamicBatch;
         this.overflowLimit = overflowLimit;
         this.recvQueue = new MpscArrayQueue<>(size);
         this.overflowQ = new MpscUnboundedArrayQueue<>(size);
+        this.controlQueue = (controlQueueSize > 0) ? new 
MpscArrayQueue<>(controlQueueSize) : null;
 
         for (Integer taskId : taskIds) {
             this.jcqMetrics.add(new JCQueueMetrics(metricNamePrefix, 
topologyId, componentId, taskId, port,
-                    metricRegistry, recvQueue, overflowQ));
+                    metricRegistry, recvQueue, overflowQ, controlQueue));
         }
 
         //The batch size can be no larger than half the full recvQueue size, 
to avoid contention issues.
@@ -105,9 +122,14 @@ public class JCQueue implements Closeable {
     }
 
     public int size() {
-        return recvQueue.size() + overflowQ.size();
+        int controlCount = (controlQueue == null) ? 0 : controlQueue.size();
+        return controlCount + recvQueue.size() + overflowQ.size();
     }
 
+    /**
+     * Load of the data plane only. The control lane and overflow lane are 
deliberately excluded so that load-aware groupings
+     * (e.g. LoadAwareShuffleGrouping) are unaffected by pending control 
tuples.
+     */
     public double getQueueLoad() {
         return ((double) recvQueue.size()) / recvQueue.capacity();
     }
@@ -116,6 +138,19 @@ public class JCQueue implements Closeable {
      * Non blocking. Returns immediately if Q is empty. Returns number of 
elements consumed from Q.
      */
     private int consumeImpl(Consumer consumer, ExitCondition exitCond) throws 
InterruptedException {
+        int controlDrainCount = 0;
+        if (controlQueue != null) {
+            // drain the control lane first: it is small and bounded, so it 
cannot starve the data plane
+            while (exitCond.keepRunning()) {
+                Object tuple = controlQueue.poll();
+                if (tuple == null) {
+                    break;
+                }
+                consumer.accept(tuple);
+                ++controlDrainCount;
+            }
+        }
+
         int drainCount = 0;
         while (exitCond.keepRunning()) {
             Object tuple = recvQueue.poll();
@@ -133,7 +168,7 @@ public class JCQueue implements Closeable {
             ++overflowDrainCount;
             consumer.accept(tuple);
         }
-        int total = drainCount + overflowDrainCount;
+        int total = controlDrainCount + drainCount + overflowDrainCount;
         if (total > 0) {
             consumer.flush();
         }
@@ -216,6 +251,49 @@ public class JCQueue implements Closeable {
         return tryPublishInternal(obj);
     }
 
+    /**
+     * Whether this queue has a dedicated control lane for low-volume control 
tuples.
+     */
+    public boolean isControlLaneEnabled() {
+        return controlQueue != null;
+    }
+
+    /**
+     * Non-blocking, un-batched write to the control lane, exempt from 
backpressure. Falls back to an un-batched
+     * write to the recvQueue (same as {@link #tryPublishDirect(Object)}) when 
the control lane is disabled.
+     * Returns false if the target queue is full; a failed control publish is 
dropped by the caller and counted
+     * via the control drop metric, which is safe because control signals are 
periodic and self-healing.
+     */
+    public boolean tryPublishControl(Object obj) {
+        if (controlQueue == null) {
+            return tryPublishInternal(obj);
+        }
+        if (controlQueue.offer(obj)) {
+            return true;
+        }
+        for (JCQueueMetrics jcQueueMetric : jcqMetrics) {
+            jcQueueMetric.notifyControlMsgDrop();
+        }
+        maybeWarnControlDrop();
+        return false;
+    }
+
+    // A full control lane means the consuming executor thread is stalled; 
surface it in the logs (rate-limited) and
+    // not only via the control_dropped_messages metric, which an operator may 
not be charting.
+    private void maybeWarnControlDrop() {
+        // Reads the clock on every invocation, but this is not a hot path: it 
runs only when a control tuple is
+        // dropped, and control tuples reach tryPublishControl only via the 
isControlStreamId guard (timer-driven
+        // tick/flush/metrics-tick/feedback signals). So drops are bounded by 
those signal periods (order of a few
+        // per second per queue even under a full stall), not by the data 
rate. A time-based throttle also has to
+        // read the clock to know the interval elapsed; on Linux nanoTime() is 
a vDSO read (no syscall/context switch).
+        long now = System.nanoTime();
+        long last = lastControlDropLogNanos.get();
+        if (now - last >= CONTROL_DROP_LOG_INTERVAL_NANOS && 
lastControlDropLogNanos.compareAndSet(last, now)) {
+            LOG.warn("Control lane full on queue '{}'; dropping control tuple. 
The consuming executor thread is likely "
+                    + "stalled. See the receive-queue-control_dropped_messages 
metric.", queueName);
+        }
+    }
+
     /**
      * Un-batched write to overflowQ. Should only be called by WorkerTransfer 
returns false if overflowLimit has reached
      */
diff --git a/storm-client/src/jvm/org/apache/storm/utils/JCQueueMetrics.java 
b/storm-client/src/jvm/org/apache/storm/utils/JCQueueMetrics.java
index 6a6a56f4f..771e13a1b 100644
--- a/storm-client/src/jvm/org/apache/storm/utils/JCQueueMetrics.java
+++ b/storm-client/src/jvm/org/apache/storm/utils/JCQueueMetrics.java
@@ -31,10 +31,15 @@ public class JCQueueMetrics implements Closeable {
     private final RateTracker arrivalsTracker = new RateTracker(10000, 10);
     private final RateTracker insertFailuresTracker = new RateTracker(10000, 
10);
     private final AtomicLong droppedMessages = new AtomicLong(0);
+    private final AtomicLong droppedControlMessages = new AtomicLong(0);
 
+    /**
+     * Creates and registers the queue gauges. {@code controlQ} may be null 
when the queue has no control lane;
+     * the control-lane gauges are then not registered.
+     */
     public JCQueueMetrics(String metricNamePrefix, String topologyId, String 
componentId, int taskId, int port,
                           StormMetricRegistry metricRegistry, 
MpscArrayQueue<Object> receiveQ,
-                          MpscUnboundedArrayQueue<Object> overflowQ) {
+                          MpscUnboundedArrayQueue<Object> overflowQ, 
MpscArrayQueue<Object> controlQ) {
 
         Gauge<Integer> cap = new Gauge<Integer>() {
             @Override
@@ -103,6 +108,14 @@ public class JCQueueMetrics implements Closeable {
         metricRegistry.gauge(metricNamePrefix + "-insert_failures", 
insertFailures, topologyId, componentId, taskId, port);
         metricRegistry.gauge(metricNamePrefix + "-dropped_messages", dropped, 
topologyId, componentId, taskId, port);
         metricRegistry.gauge(metricNamePrefix + "-overflow", overflow, 
topologyId, componentId, taskId, port);
+
+        if (controlQ != null) {
+            Gauge<Integer> controlPop = controlQ::size;
+            Gauge<Long> controlDropped = droppedControlMessages::get;
+
+            metricRegistry.gauge(metricNamePrefix + "-control_population", 
controlPop, topologyId, componentId, taskId, port);
+            metricRegistry.gauge(metricNamePrefix + 
"-control_dropped_messages", controlDropped, topologyId, componentId, taskId, 
port);
+        }
     }
 
     public void notifyArrivals(long counts) {
@@ -117,6 +130,10 @@ public class JCQueueMetrics implements Closeable {
         droppedMessages.incrementAndGet();
     }
 
+    public void notifyControlMsgDrop() {
+        droppedControlMessages.incrementAndGet();
+    }
+
     @Override
     public void close() {
         arrivalsTracker.close();
diff --git a/storm-client/test/jvm/org/apache/storm/ConstantsTest.java 
b/storm-client/test/jvm/org/apache/storm/ConstantsTest.java
new file mode 100644
index 000000000..cdf930591
--- /dev/null
+++ b/storm-client/test/jvm/org/apache/storm/ConstantsTest.java
@@ -0,0 +1,61 @@
+/**
+ * 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;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+public class ConstantsTest {
+
+    @Test
+    public void testEveryWhitelistedStreamIsControl() {
+        for (String id : Constants.SYSTEM_CONTROL_STREAM_IDS) {
+            // fresh instance: the fast-reject gates must not depend on 
identity or a cached hashCode
+            assertTrue(Constants.isControlStreamId(new 
String(id.toCharArray())), id);
+        }
+    }
+
+    @Test
+    public void testHighVolumeSystemStreamsAreNotControl() {
+        // volume-proportional to the data plane: must stay on the data path 
(see SYSTEM_CONTROL_STREAM_IDS javadoc)
+        String[] dataPlaneSystemStreams = {
+            "__ack_init", "__ack_ack", "__ack_fail", "__ack_reset_timeout",
+            "__metrics", "__system", "__eventlog", "__heartbeat"
+        };
+        for (String id : dataPlaneSystemStreams) {
+            assertFalse(Constants.isControlStreamId(id), id);
+        }
+    }
+
+    @Test
+    public void testUserStreamsAreNotControl() {
+        // includes ids whose length collides with a whitelisted id, to 
exercise the prefix/char gates
+        String[] userStreams = { "default", "s1", "stream", "__x", "_tick", 
"___tick", "abcdef", "abcdefghij" };
+        for (String id : userStreams) {
+            assertFalse(Constants.isControlStreamId(id), id);
+        }
+    }
+
+    @Test
+    public void testDegenerateStreamIdsAreNotControl() {
+        assertFalse(Constants.isControlStreamId(null));
+        assertFalse(Constants.isControlStreamId(""));
+        assertFalse(Constants.isControlStreamId("__"));
+        StringBuilder longId = new StringBuilder("__");
+        for (int i = 0; i < 100; i++) {
+            longId.append('t');
+        }
+        assertFalse(Constants.isControlStreamId(longId.toString()));
+    }
+}
diff --git 
a/storm-client/test/jvm/org/apache/storm/executor/ExecutorTransferControlLaneTest.java
 
b/storm-client/test/jvm/org/apache/storm/executor/ExecutorTransferControlLaneTest.java
new file mode 100644
index 000000000..4b072be0f
--- /dev/null
+++ 
b/storm-client/test/jvm/org/apache/storm/executor/ExecutorTransferControlLaneTest.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.executor;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import org.apache.storm.Constants;
+import org.apache.storm.daemon.worker.WorkerState;
+import org.apache.storm.generated.StormTopology;
+import org.apache.storm.metrics2.StormMetricRegistry;
+import org.apache.storm.policy.WaitStrategyPark;
+import org.apache.storm.task.GeneralTopologyContext;
+import org.apache.storm.task.WorkerTopologyContext;
+import org.apache.storm.testing.TestWordCounter;
+import org.apache.storm.testing.TestWordSpout;
+import org.apache.storm.topology.TopologyBuilder;
+import org.apache.storm.tuple.AddressedTuple;
+import org.apache.storm.tuple.Fields;
+import org.apache.storm.tuple.TupleImpl;
+import org.apache.storm.tuple.Values;
+import org.apache.storm.utils.JCQueue;
+import org.apache.storm.utils.Utils;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Verifies the control-lane routing in {@link 
ExecutorTransfer#tryTransferLocal}: whitelisted control streams
+ * (see {@link Constants#SYSTEM_CONTROL_STREAM_IDS}) must reach the 
destination's control lane regardless of
+ * pendingEmits ordering and data-queue saturation, while data tuples and the 
lane-disabled path must keep the
+ * original semantics.
+ */
+public class ExecutorTransferControlLaneTest {
+
+    private static final int DEST_TASK_ID = 1;
+
+    private Map<String, Object> topoConf;
+    private WorkerState workerState;
+    private GeneralTopologyContext generalTopologyContext;
+
+    @BeforeEach
+    public void setup() {
+        topoConf = Utils.readStormConfig();
+
+        TopologyBuilder builder = new TopologyBuilder();
+        builder.setSpout("1", new TestWordSpout(true), 1);
+        builder.setBolt("2", new TestWordCounter(), 1).fieldsGrouping("1", new 
Fields("word"));
+        StormTopology stormTopology = builder.createTopology();
+
+        WorkerTopologyContext workerTopologyContext = 
mock(WorkerTopologyContext.class);
+        when(workerTopologyContext.getRawTopology()).thenReturn(stormTopology);
+
+        workerState = mock(WorkerState.class);
+        
when(workerState.getWorkerTopologyContext()).thenReturn(workerTopologyContext);
+        generalTopologyContext = mock(GeneralTopologyContext.class);
+    }
+
+    private ExecutorTransfer mkExecutorTransfer(JCQueue localQueue) {
+        Map<Integer, JCQueue> receiveQMap = new HashMap<>();
+        receiveQMap.put(DEST_TASK_ID, localQueue);
+        when(workerState.getLocalReceiveQueues()).thenReturn(receiveQMap);
+
+        ExecutorTransfer executorTransfer = new ExecutorTransfer(workerState, 
topoConf);
+        executorTransfer.initLocalRecvQueues();
+        return executorTransfer;
+    }
+
+    private JCQueue mkQueue(String name, int size, int controlQueueSize) {
+        return new JCQueue(name, name, size, 0, 1, new WaitStrategyPark(100), 
"test", "test",
+            Collections.singletonList(DEST_TASK_ID), 6701, new 
StormMetricRegistry(), false, controlQueueSize);
+    }
+
+    private AddressedTuple mkTuple(String streamId) {
+        TupleImpl tuple = new TupleImpl(generalTopologyContext, new 
Values("v"), Constants.SYSTEM_COMPONENT_ID,
+            (int) Constants.SYSTEM_TASK_ID, streamId);
+        return new AddressedTuple(DEST_TASK_ID, tuple);
+    }
+
+    private static List<Object> drain(JCQueue queue) {
+        List<Object> drained = new ArrayList<>();
+        queue.consume(new JCQueue.Consumer() {
+            @Override
+            public void accept(Object event) {
+                drained.add(event);
+            }
+
+            @Override
+            public void flush() {
+            }
+        });
+        return drained;
+    }
+
+    @Test
+    public void testControlTupleBypassesPendingEmits() {
+        JCQueue queue = mkQueue("bypassPending", 16, 4);
+        ExecutorTransfer executorTransfer = mkExecutorTransfer(queue);
+
+        Queue<AddressedTuple> pendingEmits = new ArrayDeque<>();
+        pendingEmits.add(mkTuple("default")); // non-empty: the data path 
would reject and append here
+
+        AddressedTuple controlTuple = 
mkTuple(Constants.SYSTEM_FLUSH_STREAM_ID);
+        assertTrue(executorTransfer.tryTransferLocal(controlTuple, queue, 
pendingEmits),
+            "control tuple must be reported as handled");
+        assertEquals(1, pendingEmits.size(), "control tuple must not be queued 
behind pendingEmits");
+        assertEquals(Collections.singletonList(controlTuple), drain(queue));
+    }
+
+    @Test
+    public void testControlTupleDeliveredWhenDataQueueFull() {
+        JCQueue queue = mkQueue("fullDataQueue", 16, 4);
+        ExecutorTransfer executorTransfer = mkExecutorTransfer(queue);
+
+        while (queue.tryPublishDirect("DATA")) { // saturate the data plane
+        }
+
+        AddressedTuple controlTuple = mkTuple(Constants.SYSTEM_TICK_STREAM_ID);
+        assertTrue(executorTransfer.tryTransferLocal(controlTuple, queue, 
null));
+        assertEquals(controlTuple, drain(queue).get(0), "control tuple must be 
drained ahead of the data backlog");
+    }
+
+    @Test
+    public void testControlTupleDroppedOnFullLaneStillReportedHandled() {
+        JCQueue queue = mkQueue("fullLane", 16, 2);
+        ExecutorTransfer executorTransfer = mkExecutorTransfer(queue);
+
+        int accepted = 0;
+        while (accepted < 64 && queue.tryPublishControl("CTRL")) { // saturate 
the control lane
+            accepted++;
+        }
+        assertTrue(accepted > 0 && accepted < 64);
+
+        Queue<AddressedTuple> pendingEmits = new ArrayDeque<>();
+        
assertTrue(executorTransfer.tryTransferLocal(mkTuple(Constants.SYSTEM_FLUSH_STREAM_ID),
 queue, pendingEmits),
+            "a dropped control tuple is self-healing and must be reported as 
handled");
+        assertTrue(pendingEmits.isEmpty(), "a dropped control tuple must not 
fall back to pendingEmits");
+        assertEquals(accepted, drain(queue).size());
+    }
+
+    @Test
+    public void testDataTupleKeepsPendingEmitsSemantics() {
+        JCQueue queue = mkQueue("dataPath", 16, 4);
+        ExecutorTransfer executorTransfer = mkExecutorTransfer(queue);
+
+        Queue<AddressedTuple> pendingEmits = new ArrayDeque<>();
+        pendingEmits.add(mkTuple("default"));
+
+        AddressedTuple dataTuple = mkTuple("default");
+        assertFalse(executorTransfer.tryTransferLocal(dataTuple, queue, 
pendingEmits),
+            "data tuples must keep the original ordering semantics");
+        assertEquals(2, pendingEmits.size());
+        assertTrue(drain(queue).isEmpty());
+    }
+
+    @Test
+    public void testControlTupleFollowsDataPathWhenLaneDisabled() {
+        JCQueue queue = mkQueue("laneDisabled", 16, 0);
+        ExecutorTransfer executorTransfer = mkExecutorTransfer(queue);
+
+        Queue<AddressedTuple> pendingEmits = new ArrayDeque<>();
+        pendingEmits.add(mkTuple("default"));
+
+        
assertFalse(executorTransfer.tryTransferLocal(mkTuple(Constants.SYSTEM_FLUSH_STREAM_ID),
 queue, pendingEmits),
+            "with the lane disabled, control tuples must keep the original 
data-path semantics");
+        assertEquals(2, pendingEmits.size());
+    }
+}
diff --git a/storm-client/test/jvm/org/apache/storm/utils/JCQueueTest.java 
b/storm-client/test/jvm/org/apache/storm/utils/JCQueueTest.java
index e6bf54e19..f2b563473 100644
--- a/storm-client/test/jvm/org/apache/storm/utils/JCQueueTest.java
+++ b/storm-client/test/jvm/org/apache/storm/utils/JCQueueTest.java
@@ -15,12 +15,18 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import com.codahale.metrics.Gauge;
 import java.lang.ref.WeakReference;
 import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
+import java.util.Map;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 import org.apache.storm.metrics2.StormMetricRegistry;
 import org.apache.storm.policy.IWaitStrategy;
@@ -233,6 +239,154 @@ public class JCQueueTest {
         assertEquals(3, inserter.batchSize());
     }
 
+    @Test
+    public void testControlTuplesDrainedBeforeData() {
+        JCQueue queue = createQueueWithControlLane("controlFirst", 16, 4);
+        assertTrue(queue.isControlLaneEnabled());
+
+        for (int i = 0; i < 8; i++) {
+            assertTrue(queue.tryPublishDirect(i));
+        }
+        // published AFTER the data backlog, must still be consumed first
+        assertTrue(queue.tryPublishControl("CTRL"));
+
+        List<Object> drained = new ArrayList<>();
+        queue.consume(collectingConsumer(drained));
+
+        assertEquals(9, drained.size());
+        assertEquals("CTRL", drained.get(0), "control tuple must be drained 
ahead of the data backlog");
+    }
+
+    @Test
+    public void testControlLaneDropsOnFullWithoutBlocking() {
+        JCQueue queue = createQueueWithControlLane("controlDrop", 16, 2);
+
+        int accepted = 0;
+        while (accepted < 64 && queue.tryPublishControl("CTRL" + accepted)) {
+            accepted++;
+        }
+        assertTrue(accepted > 0 && accepted < 64, "control lane must be 
bounded");
+
+        // a full control lane drops (returns false) instead of blocking, and 
leaves the data path unaffected
+        assertFalse(queue.tryPublishControl("EXTRA"));
+        assertTrue(queue.tryPublishDirect("DATA"));
+        assertEquals(accepted + 1, queue.size());
+    }
+
+    @Test
+    public void testControlPublishFallsBackToRecvQueueWhenLaneDisabled() {
+        JCQueue queue = createQueue("controlDisabled", 16);
+        assertFalse(queue.isControlLaneEnabled());
+
+        assertTrue(queue.tryPublishControl("CTRL"));
+        assertEquals(1, queue.size());
+
+        List<Object> drained = new ArrayList<>();
+        queue.consume(collectingConsumer(drained));
+        assertEquals(Collections.singletonList("CTRL"), drained);
+    }
+
+    @Test
+    public void testControlTuplesDrainedBeforeOverflow() {
+        JCQueue queue = createQueueWithControlLane("controlBeforeOverflow", 
16, 4);
+
+        assertTrue(queue.tryPublishToOverflow("OVERFLOW"));
+        assertTrue(queue.tryPublishDirect("DATA"));
+        assertTrue(queue.tryPublishControl("CTRL"));
+
+        List<Object> drained = new ArrayList<>();
+        int consumed = queue.consume(collectingConsumer(drained));
+
+        assertEquals(3, consumed, "consume() return value must include control 
tuples");
+        assertEquals(Arrays.asList("CTRL", "DATA", "OVERFLOW"), drained,
+            "drain order must be control lane, then recvQueue, then 
overflowQ");
+    }
+
+    @Test
+    public void testControlLaneIsFifo() {
+        JCQueue queue = createQueueWithControlLane("controlFifo", 16, 8);
+        for (int i = 0; i < 5; i++) {
+            assertTrue(queue.tryPublishControl("CTRL" + i));
+        }
+
+        List<Object> drained = new ArrayList<>();
+        queue.consume(collectingConsumer(drained));
+        assertEquals(Arrays.asList("CTRL0", "CTRL1", "CTRL2", "CTRL3", 
"CTRL4"), drained);
+    }
+
+    @Test
+    public void testConsumerFlushCalledAfterControlOnlyDrain() {
+        JCQueue queue = createQueueWithControlLane("controlFlush", 16, 4);
+        assertTrue(queue.tryPublishControl("CTRL"));
+
+        AtomicInteger flushCount = new AtomicInteger();
+        int consumed = queue.consume(new JCQueue.Consumer() {
+            @Override
+            public void accept(Object event) {
+            }
+
+            @Override
+            public void flush() {
+                flushCount.incrementAndGet();
+            }
+        });
+
+        assertEquals(1, consumed);
+        assertEquals(1, flushCount.get(),
+            "a drain that consumed only control tuples must still flush the 
consumer (e.g. deliver a flush tuple's effect)");
+    }
+
+    @Test
+    public void testExitConditionStopsControlDrain() {
+        JCQueue queue = createQueueWithControlLane("controlExitCond", 16, 4);
+        assertTrue(queue.tryPublishControl("CTRL"));
+
+        List<Object> drained = new ArrayList<>();
+        int consumed = queue.consume(collectingConsumer(drained), () -> false);
+
+        assertEquals(0, consumed, "exit condition must be honored before 
draining the control lane");
+        assertTrue(drained.isEmpty());
+        assertEquals(1, queue.size(), "unconsumed control tuple must remain 
queued");
+    }
+
+    @Test
+    public void testControlDropIsCountedInMetrics() {
+        StormMetricRegistry registry = new StormMetricRegistry();
+        JCQueue queue = new JCQueue("controlDropMetric", "controlDropMetric", 
16, 0, 1, waitStrategy,
+            "test", "test", Collections.singletonList(1000), 1000, registry, 
false, 2);
+
+        int accepted = 0;
+        while (accepted < 64 && queue.tryPublishControl("CTRL")) {
+            accepted++;
+        }
+        assertTrue(accepted > 0 && accepted < 64);
+        assertFalse(queue.tryPublishControl("EXTRA")); // second counted drop 
(first happened when the fill loop stopped)
+
+        assertEquals((long) accepted, gaugeValue(registry, 
"control_population"),
+            "control_population gauge must report the lane occupancy");
+        assertEquals(2L, gaugeValue(registry, "control_dropped_messages"),
+            "every rejected control publish must be counted as a dropped 
control message");
+    }
+
+    private long gaugeValue(StormMetricRegistry registry, String nameSuffix) {
+        for (Map.Entry<String, Gauge> entry : 
registry.getRegistry().getGauges().entrySet()) {
+            if (entry.getKey().contains(nameSuffix)) {
+                return ((Number) entry.getValue().getValue()).longValue();
+            }
+        }
+        throw new AssertionError("gauge not registered: " + nameSuffix);
+    }
+
+    @Test
+    public void testQueueLoadExcludesControlLane() {
+        JCQueue queue = createQueueWithControlLane("controlLoad", 16, 4);
+
+        assertTrue(queue.tryPublishControl("CTRL"));
+
+        assertEquals(0.0, queue.getQueueLoad(), 0.0, "control lane must not 
contribute to the data-plane load");
+        assertEquals(1, queue.size(), "control lane must contribute to 
size()");
+    }
+
     @Test
     public void testQueueIsCollectedAfterLongLivedProducerPublishes() {
         Assertions.assertTimeoutPreemptively(Duration.ofSeconds(30), () -> {
@@ -325,6 +479,24 @@ public class JCQueueTest {
                 new StormMetricRegistry(), dynamicBatch);
     }
 
+    private JCQueue createQueueWithControlLane(String name, int queueSize, int 
controlQueueSize) {
+        return new JCQueue(name, name, queueSize, 0, 1, waitStrategy, "test", 
"test", Collections.singletonList(1000), 1000,
+                new StormMetricRegistry(), false, controlQueueSize);
+    }
+
+    private static JCQueue.Consumer collectingConsumer(List<Object> sink) {
+        return new JCQueue.Consumer() {
+            @Override
+            public void accept(Object event) {
+                sink.add(event);
+            }
+
+            @Override
+            public void flush() {
+            }
+        };
+    }
+
     private static class IncProducer implements Runnable {
 
         private final JCQueue queue;

Reply via email to