This is an automated email from the ASF dual-hosted git repository.
rzo1 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/storm.git
The following commit(s) were added to refs/heads/master by this push:
new 7cfc562ea Drop unexpected Pacemaker frames instead of terminating the
daemon; mark Pacemaker as deprecated (#9084)
7cfc562ea is described below
commit 7cfc562ea6cc6e0f5717a56c02e003ff852168ba
Author: Richard Zowalla <[email protected]>
AuthorDate: Tue Sep 15 11:41:15 2026 +0200
Drop unexpected Pacemaker frames instead of terminating the daemon; mark
Pacemaker as deprecated (#9084)
* Drop unexpected Pacemaker frames instead of terminating the daemon
The Pacemaker server decodes a CONTROL_MESSAGE frame into a ControlMessage.
With no SASL handler installed (pacemaker.auth.method NONE), or once the
SASL
handler has forwarded it upstream, the object reached
PacemakerServer.received(),
which cast it to HBMessage. The resulting ClassCastException reached
StormServerHandler.exceptionCaught, which treats anything but an
IOException as
fatal and exits the JVM. A malformed request (undecodable thrift payload, a
control frame without payload or with an unknown code, a request without
data)
took the same path.
The server-side ThriftDecoder now accepts only the control message a
Pacemaker
client sends, SASL_TOKEN_MESSAGE_REQUEST, which starts the DIGEST handshake.
Any other control frame, and control frames whose payload is missing, too
short
or carries an unknown code, is discarded and the connection closed, in the
style of the worker MessageDecoder. SASL_MESSAGE_TOKEN and HBMessage frames
are
decoded as before, so DIGEST and KERBEROS handshakes are unchanged. On the
client side a malformed control frame is reported as an IOException, so that
PacemakerClientHandler reconnects as it did before.
PacemakerServer.received() checks the message type before using it: anything
that is not an HBMessage is logged with its type and remote address and the
connection is closed.
The Pacemaker pipeline uses a new PacemakerServerHandler, a
StormServerHandler
whose exceptionCaught closes the failing connection for any Exception and
keeps serving the other clients. Errors are still handed to
StormServerHandler. StormServerHandler itself, and with it the worker
messaging pipeline, is unchanged.
PacemakerServer gets a package-private close() used by the new tests.
* Mark Pacemaker as deprecated
Pacemaker was built to take worker heartbeat writes off ZooKeeper. Since
STORM-2693 workers heartbeat to their supervisor, which reports them to
Nimbus, so the daemon is only kept for backward compatibility; the metrics
documentation already describes it as deprecated.
Mark the Pacemaker server and client classes, the pacemaker state storage
and the pacemaker.* configuration keys as @Deprecated, and add a notice to
docs/Pacemaker.md and to the storm pacemaker command help.
* Harden Pacemaker SASL token decoding; address review feedback
Bound the declared payload length in SaslMessageToken.read and drop
malformed SASL_MESSAGE_TOKEN frames in the Pacemaker ThriftDecoder
(close the connection on the server, surface an IOException on the
client), mirroring the existing control-frame handling so a malformed
SASL frame can no longer take a large allocation path in the decoder.
Also: add regression tests for the SASL token path, reword the
Pacemaker deprecation note in docs, and document PacemakerServer.close()
as test-only.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
---------
Co-authored-by: Richard Zowalla <[email protected]>
Co-authored-by: Rui Abreu <[email protected]>
Co-authored-by: Claude Opus 4.8 <[email protected]>
---
bin/storm.py | 4 +
docs/Pacemaker.md | 7 +
storm-client/src/jvm/org/apache/storm/Config.java | 20 ++
.../storm/cluster/PaceMakerStateStorage.java | 8 +
.../cluster/PaceMakerStateStorageFactory.java | 8 +
.../storm/cluster/StormClusterStateImpl.java | 1 +
.../storm/messaging/netty/SaslMessageToken.java | 5 +-
.../apache/storm/pacemaker/PacemakerClient.java | 8 +
.../storm/pacemaker/PacemakerClientHandler.java | 8 +
.../storm/pacemaker/PacemakerClientPool.java | 8 +
.../pacemaker/PacemakerConnectionException.java | 8 +
.../storm/pacemaker/codec/ThriftDecoder.java | 80 ++++++-
.../storm/pacemaker/codec/ThriftEncoder.java | 8 +
.../pacemaker/codec/ThriftNettyClientCodec.java | 8 +
.../storm/PaceMakerStateStorageFactoryTest.java | 1 +
.../storm/pacemaker/codec/ThriftDecoderTest.java | 194 +++++++++++++++
.../main/java/org/apache/storm/DaemonConfig.java | 8 +
.../storm/pacemaker/IServerMessageHandler.java | 8 +
.../java/org/apache/storm/pacemaker/Pacemaker.java | 8 +
.../apache/storm/pacemaker/PacemakerServer.java | 24 ++
.../pacemaker/codec/PacemakerServerHandler.java | 50 ++++
.../pacemaker/codec/ThriftNettyServerCodec.java | 13 +-
.../test/java/org/apache/storm/PacemakerTest.java | 1 +
.../storm/pacemaker/PacemakerServerTest.java | 261 +++++++++++++++++++++
24 files changed, 743 insertions(+), 6 deletions(-)
diff --git a/bin/storm.py b/bin/storm.py
index 55c211df8..f8c7ab3f1 100755
--- a/bin/storm.py
+++ b/bin/storm.py
@@ -925,6 +925,10 @@ def initialize_pacemaker_subcommand(subparsers):
Launches the Pacemaker daemon. This command should be run under
supervision with a tool like daemontools or monit.
+ Deprecated: Pacemaker is only kept for backward compatibility and will be
+ removed in a future release. By default workers heartbeat to their
+ supervisor, which reports them to Nimbus; no Pacemaker is needed.
+
See Setting up a Storm cluster for more information.
(https://storm.apache.org/documentation/Setting-up-a-Storm-cluster)
"""
diff --git a/docs/Pacemaker.md b/docs/Pacemaker.md
index 75fa74855..4e496a504 100644
--- a/docs/Pacemaker.md
+++ b/docs/Pacemaker.md
@@ -4,6 +4,13 @@ layout: documentation
documentation: true
---
+> **Deprecated:** Pacemaker is deprecated and only kept for backward
compatibility; it will be removed in a future release.
+> Since Storm 2.0
([STORM-2693](https://issues.apache.org/jira/browse/STORM-2693)) workers no
longer heartbeat directly into
+> ZooKeeper: each worker writes its heartbeats to local disk and its
supervisor reports them to Nimbus over Thrift (see
+> [Daemon Fault Tolerance](Daemon-Fault-Tolerance.html)). This removes the
ZooKeeper write load Pacemaker was built to avoid.
+> New clusters should keep the default `storm.cluster.state.store:
"org.apache.storm.cluster.ZKStateStorageFactory"` and
+> not run Pacemaker; existing deployments should plan to migrate away from
Pacemaker back to the default
+> ZooKeeper-based state store.
### Introduction
Pacemaker is a storm daemon designed to process heartbeats from workers. As
Storm is scaled up, ZooKeeper begins to become a bottleneck due to high volumes
of writes from workers doing heartbeats. Lots of writes to disk and too much
traffic across the network is generated as ZooKeeper tries to maintain
consistency.
diff --git a/storm-client/src/jvm/org/apache/storm/Config.java
b/storm-client/src/jvm/org/apache/storm/Config.java
index 53f8eefdf..519b8d943 100644
--- a/storm-client/src/jvm/org/apache/storm/Config.java
+++ b/storm-client/src/jvm/org/apache/storm/Config.java
@@ -1119,36 +1119,56 @@ public class Config extends HashMap<String, Object> {
public static final String TOPOLOGY_MAX_REPLICATION_WAIT_TIME_SEC =
"topology.max.replication.wait.time.sec";
/**
* The list of servers that Pacemaker is running on.
+ *
+ * @deprecated Pacemaker is deprecated and only kept for backward
compatibility; it will be removed in a future release.
+ * Use the default heartbeat path (workers heartbeat to their
supervisor, which reports them to Nimbus) instead.
*/
@IsStringList
+ @Deprecated
public static final String PACEMAKER_SERVERS = "pacemaker.servers";
/**
* The port Pacemaker should run on. Clients should connect to this port
to submit or read heartbeats.
+ *
+ * @deprecated Pacemaker is deprecated and only kept for backward
compatibility; it will be removed in a future release.
+ * Use the default heartbeat path (workers heartbeat to their
supervisor, which reports them to Nimbus) instead.
*/
@IsNumber
@IsPositiveNumber
+ @Deprecated
public static final String PACEMAKER_PORT = "pacemaker.port";
/**
* The maximum number of threads that should be used by the Pacemaker
client.
* When Pacemaker gets loaded it will spawn new threads, up to
* this many total, to handle the load.
+ *
+ * @deprecated Pacemaker is deprecated and only kept for backward
compatibility; it will be removed in a future release.
+ * Use the default heartbeat path (workers heartbeat to their
supervisor, which reports them to Nimbus) instead.
*/
@IsNumber
@IsPositiveNumber
+ @Deprecated
public static final String PACEMAKER_CLIENT_MAX_THREADS =
"pacemaker.client.max.threads";
/**
* This should be one of "DIGEST", "KERBEROS", or "NONE" Determines the
mode of authentication the pacemaker server and client use. The
* client must either match the server, or be NONE. In the case of NONE,
no authentication is performed for the client, and if the
* server is running with DIGEST or KERBEROS, the client can only write to
the server (no reads). This is intended to provide a
* primitive form of access-control.
+ *
+ * @deprecated Pacemaker is deprecated and only kept for backward
compatibility; it will be removed in a future release.
+ * Use the default heartbeat path (workers heartbeat to their
supervisor, which reports them to Nimbus) instead.
*/
@CustomValidator(validatorClass =
ConfigValidation.PacemakerAuthTypeValidator.class)
+ @Deprecated
public static final String PACEMAKER_AUTH_METHOD = "pacemaker.auth.method";
/**
* Pacemaker Thrift Max Message Size (bytes).
+ *
+ * @deprecated Pacemaker is deprecated and only kept for backward
compatibility; it will be removed in a future release.
+ * Use the default heartbeat path (workers heartbeat to their
supervisor, which reports them to Nimbus) instead.
*/
@IsInteger
@IsPositiveNumber
+ @Deprecated
public static final String PACEMAKER_THRIFT_MESSAGE_SIZE_MAX =
"pacemaker.thrift.message.size.max";
/**
* Max no.of seconds group mapping service will cache user groups
diff --git
a/storm-client/src/jvm/org/apache/storm/cluster/PaceMakerStateStorage.java
b/storm-client/src/jvm/org/apache/storm/cluster/PaceMakerStateStorage.java
index e746e5ec6..effaeea11 100644
--- a/storm-client/src/jvm/org/apache/storm/cluster/PaceMakerStateStorage.java
+++ b/storm-client/src/jvm/org/apache/storm/cluster/PaceMakerStateStorage.java
@@ -37,6 +37,14 @@ import org.apache.storm.utils.WrappedHBExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+/**
+ * State storage that keeps worker heartbeats in Pacemaker and everything else
in ZooKeeper.
+ *
+ * @deprecated Pacemaker is deprecated and only kept for backward
compatibility; it will be removed in a future release.
+ * Use the default heartbeat path instead: workers heartbeat to their
supervisor, which reports them to Nimbus over
+ * Thrift, with the default ZooKeeper-based cluster state store ({@code
org.apache.storm.cluster.ZKStateStorageFactory}).
+ */
+@Deprecated
public class PaceMakerStateStorage implements IStateStorage {
private static final int maxRetries = 10;
diff --git
a/storm-client/src/jvm/org/apache/storm/cluster/PaceMakerStateStorageFactory.java
b/storm-client/src/jvm/org/apache/storm/cluster/PaceMakerStateStorageFactory.java
index 596aefab6..6b8e3bf32 100644
---
a/storm-client/src/jvm/org/apache/storm/cluster/PaceMakerStateStorageFactory.java
+++
b/storm-client/src/jvm/org/apache/storm/cluster/PaceMakerStateStorageFactory.java
@@ -22,6 +22,14 @@ import java.util.Map;
import org.apache.storm.pacemaker.PacemakerClientPool;
import org.apache.storm.utils.Utils;
+/**
+ * Factory for {@link PaceMakerStateStorage}.
+ *
+ * @deprecated Pacemaker is deprecated and only kept for backward
compatibility; it will be removed in a future release.
+ * Use the default heartbeat path instead: workers heartbeat to their
supervisor, which reports them to Nimbus over
+ * Thrift, with the default ZooKeeper-based cluster state store ({@code
org.apache.storm.cluster.ZKStateStorageFactory}).
+ */
+@Deprecated
public class PaceMakerStateStorageFactory implements StateStorageFactory {
@Override
public IStateStorage mkStore(Map<String, Object> config, Map<String,
Object> authConf, ClusterStateContext context) {
diff --git
a/storm-client/src/jvm/org/apache/storm/cluster/StormClusterStateImpl.java
b/storm-client/src/jvm/org/apache/storm/cluster/StormClusterStateImpl.java
index 3044b37ca..8be795c2b 100644
--- a/storm-client/src/jvm/org/apache/storm/cluster/StormClusterStateImpl.java
+++ b/storm-client/src/jvm/org/apache/storm/cluster/StormClusterStateImpl.java
@@ -230,6 +230,7 @@ public class StormClusterStateImpl implements
IStormClusterState {
}
@Override
+ @SuppressWarnings("deprecation")
public boolean isPacemakerStateStore() {
if (stateStorage == null) {
return false;
diff --git
a/storm-client/src/jvm/org/apache/storm/messaging/netty/SaslMessageToken.java
b/storm-client/src/jvm/org/apache/storm/messaging/netty/SaslMessageToken.java
index 368ffba02..7e8ecf20e 100644
---
a/storm-client/src/jvm/org/apache/storm/messaging/netty/SaslMessageToken.java
+++
b/storm-client/src/jvm/org/apache/storm/messaging/netty/SaslMessageToken.java
@@ -47,9 +47,12 @@ public class SaslMessageToken implements INettySerializable {
public static SaslMessageToken read(byte[] serial) {
ByteBuf smBuffer = Unpooled.wrappedBuffer(serial);
try {
+ if (smBuffer.readableBytes() < 6) {
+ return null;
+ }
short identifier = smBuffer.readShort();
int payloadLen = smBuffer.readInt();
- if (identifier != IDENTIFIER) {
+ if (identifier != IDENTIFIER || payloadLen < 0 || payloadLen >
smBuffer.readableBytes()) {
return null;
}
byte[] token = new byte[payloadLen];
diff --git
a/storm-client/src/jvm/org/apache/storm/pacemaker/PacemakerClient.java
b/storm-client/src/jvm/org/apache/storm/pacemaker/PacemakerClient.java
index 4d8e6b528..b4a4b66b3 100644
--- a/storm-client/src/jvm/org/apache/storm/pacemaker/PacemakerClient.java
+++ b/storm-client/src/jvm/org/apache/storm/pacemaker/PacemakerClient.java
@@ -39,6 +39,14 @@ import
org.apache.storm.utils.StormBoundedExponentialBackoffRetry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+/**
+ * Netty client that sends heartbeat requests to a single Pacemaker server.
+ *
+ * @deprecated Pacemaker is deprecated and only kept for backward
compatibility; it will be removed in a future release.
+ * Use the default heartbeat path instead: workers heartbeat to their
supervisor, which reports them to Nimbus over
+ * Thrift, with the default ZooKeeper-based cluster state store ({@code
org.apache.storm.cluster.ZKStateStorageFactory}).
+ */
+@Deprecated
public class PacemakerClient implements ISaslClient {
private static final Logger LOG =
LoggerFactory.getLogger(PacemakerClient.class);
diff --git
a/storm-client/src/jvm/org/apache/storm/pacemaker/PacemakerClientHandler.java
b/storm-client/src/jvm/org/apache/storm/pacemaker/PacemakerClientHandler.java
index 31fabc1a1..c3e183cd8 100644
---
a/storm-client/src/jvm/org/apache/storm/pacemaker/PacemakerClientHandler.java
+++
b/storm-client/src/jvm/org/apache/storm/pacemaker/PacemakerClientHandler.java
@@ -21,6 +21,14 @@ import
org.apache.storm.shade.io.netty.channel.ChannelInboundHandlerAdapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+/**
+ * Inbound handler of the Pacemaker client pipeline.
+ *
+ * @deprecated Pacemaker is deprecated and only kept for backward
compatibility; it will be removed in a future release.
+ * Use the default heartbeat path instead: workers heartbeat to their
supervisor, which reports them to Nimbus over
+ * Thrift, with the default ZooKeeper-based cluster state store ({@code
org.apache.storm.cluster.ZKStateStorageFactory}).
+ */
+@Deprecated
public class PacemakerClientHandler extends ChannelInboundHandlerAdapter {
private static final Logger LOG =
LoggerFactory.getLogger(PacemakerClientHandler.class);
diff --git
a/storm-client/src/jvm/org/apache/storm/pacemaker/PacemakerClientPool.java
b/storm-client/src/jvm/org/apache/storm/pacemaker/PacemakerClientPool.java
index e17c77b4c..43c6c1f7c 100644
--- a/storm-client/src/jvm/org/apache/storm/pacemaker/PacemakerClientPool.java
+++ b/storm-client/src/jvm/org/apache/storm/pacemaker/PacemakerClientPool.java
@@ -23,6 +23,14 @@ import org.apache.storm.generated.HBMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+/**
+ * Pool of clients for the configured Pacemaker servers.
+ *
+ * @deprecated Pacemaker is deprecated and only kept for backward
compatibility; it will be removed in a future release.
+ * Use the default heartbeat path instead: workers heartbeat to their
supervisor, which reports them to Nimbus over
+ * Thrift, with the default ZooKeeper-based cluster state store ({@code
org.apache.storm.cluster.ZKStateStorageFactory}).
+ */
+@Deprecated
public class PacemakerClientPool {
private static final Logger LOG =
LoggerFactory.getLogger(PacemakerClientPool.class);
diff --git
a/storm-client/src/jvm/org/apache/storm/pacemaker/PacemakerConnectionException.java
b/storm-client/src/jvm/org/apache/storm/pacemaker/PacemakerConnectionException.java
index aedcbcf7b..3390cc5a8 100644
---
a/storm-client/src/jvm/org/apache/storm/pacemaker/PacemakerConnectionException.java
+++
b/storm-client/src/jvm/org/apache/storm/pacemaker/PacemakerConnectionException.java
@@ -12,6 +12,14 @@
package org.apache.storm.pacemaker;
+/**
+ * Thrown when no connection to a Pacemaker server is available.
+ *
+ * @deprecated Pacemaker is deprecated and only kept for backward
compatibility; it will be removed in a future release.
+ * Use the default heartbeat path instead: workers heartbeat to their
supervisor, which reports them to Nimbus over
+ * Thrift, with the default ZooKeeper-based cluster state store ({@code
org.apache.storm.cluster.ZKStateStorageFactory}).
+ */
+@Deprecated
public class PacemakerConnectionException extends Exception {
public PacemakerConnectionException(String err) {
super(err);
diff --git
a/storm-client/src/jvm/org/apache/storm/pacemaker/codec/ThriftDecoder.java
b/storm-client/src/jvm/org/apache/storm/pacemaker/codec/ThriftDecoder.java
index ce104ed1e..b66420e5c 100644
--- a/storm-client/src/jvm/org/apache/storm/pacemaker/codec/ThriftDecoder.java
+++ b/storm-client/src/jvm/org/apache/storm/pacemaker/codec/ThriftDecoder.java
@@ -22,9 +22,20 @@ import org.apache.storm.shade.io.netty.buffer.ByteBuf;
import org.apache.storm.shade.io.netty.channel.ChannelHandlerContext;
import org.apache.storm.shade.io.netty.handler.codec.ByteToMessageDecoder;
import org.apache.storm.utils.Utils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+/**
+ * Decodes length-prefixed thrift {@link HBMessage} frames of the Pacemaker
protocol.
+ *
+ * @deprecated Pacemaker is deprecated and only kept for backward
compatibility; it will be removed in a future release.
+ * Use the default heartbeat path instead: workers heartbeat to their
supervisor, which reports them to Nimbus over
+ * Thrift, with the default ZooKeeper-based cluster state store ({@code
org.apache.storm.cluster.ZKStateStorageFactory}).
+ */
+@Deprecated
public class ThriftDecoder extends ByteToMessageDecoder {
+ private static final Logger LOG =
LoggerFactory.getLogger(ThriftDecoder.class);
private static final int INTEGER_SIZE = 4;
/**
@@ -32,11 +43,28 @@ public class ThriftDecoder extends ByteToMessageDecoder {
*/
private final int maxLength;
+ /**
+ * Whether this decoder sits in a Pacemaker server pipeline. A server only
accepts the control message a client
+ * sends to start the SASL handshake; any other control frame is dropped
and the connection closed.
+ */
+ private final boolean serverSide;
+
/**
* Instantiate a ThriftDecoder that accepts serialized messages of at most
maxLength bytes.
*/
public ThriftDecoder(final int maxLengthBytes) {
+ this(maxLengthBytes, false);
+ }
+
+ /**
+ * Instantiate a ThriftDecoder that accepts serialized messages of at most
maxLength bytes.
+ *
+ * @param maxLengthBytes the maximum length of a serialized thrift message
+ * @param serverSide true if the decoder is used by a Pacemaker server,
which restricts the control messages it accepts
+ */
+ public ThriftDecoder(final int maxLengthBytes, final boolean serverSide) {
maxLength = maxLengthBytes;
+ this.serverSide = serverSide;
}
@Override
@@ -68,13 +96,61 @@ public class ThriftDecoder extends ByteToMessageDecoder {
HBMessage m = (HBMessage) Utils.thriftDeserialize(HBMessage.class,
serialized);
if (m.get_type() == HBServerMessageType.CONTROL_MESSAGE) {
- ControlMessage cm =
ControlMessage.read(m.get_data().get_message_blob());
+ ControlMessage cm = readControlMessage(m);
+ if (cm == null) {
+ if (!serverSide) {
+ // Let the client handler see the failure so that it
reconnects.
+ throw new IOException("Received a malformed control
message");
+ }
+ dropAndClose(channelHandlerContext, buf, "a malformed control
frame");
+ return;
+ }
+ if (serverSide && cm != ControlMessage.SASL_TOKEN_MESSAGE_REQUEST)
{
+ dropAndClose(channelHandlerContext, buf, "an unexpected
control frame " + cm);
+ return;
+ }
out.add(cm);
} else if (m.get_type() == HBServerMessageType.SASL_MESSAGE_TOKEN) {
- SaslMessageToken sm =
SaslMessageToken.read(m.get_data().get_message_blob());
+ SaslMessageToken sm = readSaslMessageToken(m);
+ if (sm == null) {
+ if (!serverSide) {
+ // Let the client handler see the failure so that it
reconnects.
+ throw new IOException("Received a malformed SASL token
message");
+ }
+ dropAndClose(channelHandlerContext, buf, "a malformed SASL
token frame");
+ return;
+ }
out.add(sm);
} else {
out.add(m);
}
}
+
+ private static ControlMessage readControlMessage(HBMessage m) {
+ if (m.get_data() == null || !m.get_data().is_set_message_blob()) {
+ return null;
+ }
+ byte[] blob = m.get_data().get_message_blob();
+ if (blob == null || blob.length < 2) {
+ return null;
+ }
+ return ControlMessage.read(blob);
+ }
+
+ private static SaslMessageToken readSaslMessageToken(HBMessage m) {
+ if (m.get_data() == null || !m.get_data().is_set_message_blob()) {
+ return null;
+ }
+ byte[] blob = m.get_data().get_message_blob();
+ if (blob == null) {
+ return null;
+ }
+ return SaslMessageToken.read(blob);
+ }
+
+ private static void dropAndClose(ChannelHandlerContext ctx, ByteBuf buf,
String what) {
+ LOG.warn("Channel {} sent {}; closing the connection", ctx.channel(),
what);
+ buf.skipBytes(buf.readableBytes());
+ ctx.close();
+ }
}
diff --git
a/storm-client/src/jvm/org/apache/storm/pacemaker/codec/ThriftEncoder.java
b/storm-client/src/jvm/org/apache/storm/pacemaker/codec/ThriftEncoder.java
index 659f05166..d029888f1 100644
--- a/storm-client/src/jvm/org/apache/storm/pacemaker/codec/ThriftEncoder.java
+++ b/storm-client/src/jvm/org/apache/storm/pacemaker/codec/ThriftEncoder.java
@@ -28,6 +28,14 @@ import org.apache.storm.utils.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+/**
+ * Encodes messages as length-prefixed thrift {@link HBMessage} frames of the
Pacemaker protocol.
+ *
+ * @deprecated Pacemaker is deprecated and only kept for backward
compatibility; it will be removed in a future release.
+ * Use the default heartbeat path instead: workers heartbeat to their
supervisor, which reports them to Nimbus over
+ * Thrift, with the default ZooKeeper-based cluster state store ({@code
org.apache.storm.cluster.ZKStateStorageFactory}).
+ */
+@Deprecated
public class ThriftEncoder extends MessageToMessageEncoder<Object> {
private static final Logger LOG = LoggerFactory
diff --git
a/storm-client/src/jvm/org/apache/storm/pacemaker/codec/ThriftNettyClientCodec.java
b/storm-client/src/jvm/org/apache/storm/pacemaker/codec/ThriftNettyClientCodec.java
index b20883437..55f94d5da 100644
---
a/storm-client/src/jvm/org/apache/storm/pacemaker/codec/ThriftNettyClientCodec.java
+++
b/storm-client/src/jvm/org/apache/storm/pacemaker/codec/ThriftNettyClientCodec.java
@@ -25,6 +25,14 @@ import
org.apache.storm.shade.io.netty.channel.ChannelPipeline;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+/**
+ * Builds the Pacemaker client pipeline.
+ *
+ * @deprecated Pacemaker is deprecated and only kept for backward
compatibility; it will be removed in a future release.
+ * Use the default heartbeat path instead: workers heartbeat to their
supervisor, which reports them to Nimbus over
+ * Thrift, with the default ZooKeeper-based cluster state store ({@code
org.apache.storm.cluster.ZKStateStorageFactory}).
+ */
+@Deprecated
public class ThriftNettyClientCodec extends ChannelInitializer<Channel> {
public static final String SASL_HANDLER = "sasl-handler";
diff --git
a/storm-client/test/jvm/org/apache/storm/PaceMakerStateStorageFactoryTest.java
b/storm-client/test/jvm/org/apache/storm/PaceMakerStateStorageFactoryTest.java
index 8b482e823..65590d41d 100644
---
a/storm-client/test/jvm/org/apache/storm/PaceMakerStateStorageFactoryTest.java
+++
b/storm-client/test/jvm/org/apache/storm/PaceMakerStateStorageFactoryTest.java
@@ -48,6 +48,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@ExtendWith(MockitoExtension.class)
+@SuppressWarnings("deprecation")
public class PaceMakerStateStorageFactoryTest {
@Captor
diff --git
a/storm-client/test/jvm/org/apache/storm/pacemaker/codec/ThriftDecoderTest.java
b/storm-client/test/jvm/org/apache/storm/pacemaker/codec/ThriftDecoderTest.java
new file mode 100644
index 000000000..3461037a4
--- /dev/null
+++
b/storm-client/test/jvm/org/apache/storm/pacemaker/codec/ThriftDecoderTest.java
@@ -0,0 +1,194 @@
+/**
+ * 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 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.pacemaker.codec;
+
+import org.apache.storm.generated.HBMessage;
+import org.apache.storm.generated.HBMessageData;
+import org.apache.storm.generated.HBServerMessageType;
+import org.apache.storm.messaging.netty.ControlMessage;
+import org.apache.storm.messaging.netty.SaslMessageToken;
+import org.apache.storm.shade.io.netty.buffer.ByteBuf;
+import org.apache.storm.shade.io.netty.buffer.Unpooled;
+import org.apache.storm.shade.io.netty.channel.embedded.EmbeddedChannel;
+import org.apache.storm.shade.io.netty.handler.codec.DecoderException;
+import org.apache.storm.utils.Utils;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@SuppressWarnings("deprecation")
+public class ThriftDecoderTest {
+
+ private static final int MAX_LENGTH = 1024 * 1024;
+
+ static ByteBuf frame(HBMessage message) {
+ byte[] serialized = Utils.thriftSerialize(message);
+ ByteBuf buf = Unpooled.buffer();
+ buf.writeInt(serialized.length);
+ buf.writeBytes(serialized);
+ return buf;
+ }
+
+ static ByteBuf controlFrame(ControlMessage controlMessage) {
+ ByteBuf blob = Unpooled.buffer();
+ controlMessage.write(blob);
+ byte[] bytes = new byte[blob.readableBytes()];
+ blob.readBytes(bytes);
+ return frame(new HBMessage(HBServerMessageType.CONTROL_MESSAGE,
HBMessageData.message_blob(bytes)));
+ }
+
+ private static EmbeddedChannel serverChannel() {
+ return new EmbeddedChannel(new ThriftDecoder(MAX_LENGTH, true));
+ }
+
+ static ByteBuf saslTokenFrame(short identifier, int declaredPayloadLen,
byte[] payload) {
+ ByteBuf blob = Unpooled.buffer();
+ blob.writeShort(identifier);
+ blob.writeInt(declaredPayloadLen);
+ if (payload != null) {
+ blob.writeBytes(payload);
+ }
+ byte[] bytes = new byte[blob.readableBytes()];
+ blob.readBytes(bytes);
+ return frame(new HBMessage(HBServerMessageType.SASL_MESSAGE_TOKEN,
HBMessageData.message_blob(bytes)));
+ }
+
+ @Test
+ public void serverDropsSaslTokenWithOversizedPayloadLength() {
+ EmbeddedChannel channel = serverChannel();
+
+ // A tiny frame that claims a ~2GB payload must not be allocated; it
is dropped and the connection closed.
+ channel.writeInbound(saslTokenFrame(SaslMessageToken.IDENTIFIER,
Integer.MAX_VALUE, null));
+
+ assertNull(channel.readInbound());
+ assertFalse(channel.isActive());
+ }
+
+ @Test
+ public void clientReportsSaslTokenWithOversizedPayloadLength() {
+ EmbeddedChannel channel = new EmbeddedChannel(new
ThriftDecoder(MAX_LENGTH));
+
+ assertThrows(DecoderException.class, () -> channel.writeInbound(
+ saslTokenFrame(SaslMessageToken.IDENTIFIER, Integer.MAX_VALUE,
null)));
+ assertNull(channel.readInbound());
+ }
+
+ @Test
+ public void serverDropsUnexpectedControlFrames() {
+ for (ControlMessage controlMessage : ControlMessage.values()) {
+ if (controlMessage == ControlMessage.SASL_TOKEN_MESSAGE_REQUEST) {
+ continue;
+ }
+ EmbeddedChannel channel = serverChannel();
+
+ channel.writeInbound(controlFrame(controlMessage));
+
+ assertNull(channel.readInbound(), controlMessage.name());
+ assertFalse(channel.isActive(), controlMessage.name());
+ }
+ }
+
+ @Test
+ public void serverAcceptsSaslTokenMessageRequest() {
+ EmbeddedChannel channel = serverChannel();
+
+
channel.writeInbound(controlFrame(ControlMessage.SASL_TOKEN_MESSAGE_REQUEST));
+
+ assertSame(ControlMessage.SASL_TOKEN_MESSAGE_REQUEST,
channel.readInbound());
+ assertTrue(channel.isActive());
+ }
+
+ @Test
+ public void serverAcceptsSaslMessageToken() {
+ EmbeddedChannel channel = serverChannel();
+ byte[] token = { 1, 2, 3 };
+ ByteBuf blob = Unpooled.buffer();
+ new SaslMessageToken(token).write(blob);
+ byte[] bytes = new byte[blob.readableBytes()];
+ blob.readBytes(bytes);
+
+ channel.writeInbound(frame(new
HBMessage(HBServerMessageType.SASL_MESSAGE_TOKEN,
HBMessageData.message_blob(bytes))));
+
+ SaslMessageToken decoded = channel.readInbound();
+ assertArrayEquals(token, decoded.getSaslToken());
+ assertTrue(channel.isActive());
+ }
+
+ @Test
+ public void serverPassesHeartbeatMessages() {
+ EmbeddedChannel channel = serverChannel();
+ HBMessage message = new HBMessage(HBServerMessageType.CREATE_PATH,
HBMessageData.path("/path"));
+
+ channel.writeInbound(frame(message));
+
+ assertEquals(message, channel.readInbound());
+ assertTrue(channel.isActive());
+ }
+
+ @Test
+ public void serverDropsControlFrameWithUnknownCode() {
+ EmbeddedChannel channel = serverChannel();
+
+ channel.writeInbound(frame(new
HBMessage(HBServerMessageType.CONTROL_MESSAGE,
+
HBMessageData.message_blob(new byte[]{ 0, 1 }))));
+
+ assertNull(channel.readInbound());
+ assertFalse(channel.isActive());
+ }
+
+ @Test
+ public void serverDropsControlFrameWithoutPayload() {
+ EmbeddedChannel channel = serverChannel();
+
+ channel.writeInbound(frame(new
HBMessage(HBServerMessageType.CONTROL_MESSAGE, null)));
+
+ assertNull(channel.readInbound());
+ assertFalse(channel.isActive());
+ }
+
+ @Test
+ public void serverDropsControlFrameWithShortPayload() {
+ EmbeddedChannel channel = serverChannel();
+
+ channel.writeInbound(frame(new
HBMessage(HBServerMessageType.CONTROL_MESSAGE,
+
HBMessageData.message_blob(new byte[]{ 1 }))));
+
+ assertNull(channel.readInbound());
+ assertFalse(channel.isActive());
+ }
+
+ @Test
+ public void clientDecodesSaslCompleteRequest() {
+ EmbeddedChannel channel = new EmbeddedChannel(new
ThriftDecoder(MAX_LENGTH));
+
+
channel.writeInbound(controlFrame(ControlMessage.SASL_COMPLETE_REQUEST));
+
+ assertSame(ControlMessage.SASL_COMPLETE_REQUEST,
channel.readInbound());
+ assertTrue(channel.isActive());
+ }
+
+ @Test
+ public void clientReportsMalformedControlFrame() {
+ EmbeddedChannel channel = new EmbeddedChannel(new
ThriftDecoder(MAX_LENGTH));
+
+ assertThrows(DecoderException.class, () -> channel.writeInbound(
+ frame(new HBMessage(HBServerMessageType.CONTROL_MESSAGE, null))));
+ assertNull(channel.readInbound());
+ }
+}
diff --git a/storm-server/src/main/java/org/apache/storm/DaemonConfig.java
b/storm-server/src/main/java/org/apache/storm/DaemonConfig.java
index 4805f5195..e535baf1b 100644
--- a/storm-server/src/main/java/org/apache/storm/DaemonConfig.java
+++ b/storm-server/src/main/java/org/apache/storm/DaemonConfig.java
@@ -682,15 +682,23 @@ public class DaemonConfig implements Validated {
/**
* The maximum number of threads that should be used by the Pacemaker.
When Pacemaker gets loaded it will spawn new threads, up to this
* many total, to handle the load.
+ *
+ * @deprecated Pacemaker is deprecated and only kept for backward
compatibility; it will be removed in a future release.
+ * Use the default heartbeat path (workers heartbeat to their
supervisor, which reports them to Nimbus) instead.
*/
@IsNumber
@IsPositiveNumber
+ @Deprecated
public static final String PACEMAKER_MAX_THREADS = "pacemaker.max.threads";
/**
* This parameter is used by the storm-deploy project to configure the jvm
options for the pacemaker daemon.
+ *
+ * @deprecated Pacemaker is deprecated and only kept for backward
compatibility; it will be removed in a future release.
+ * Use the default heartbeat path (workers heartbeat to their
supervisor, which reports them to Nimbus) instead.
*/
@IsStringOrStringList
+ @Deprecated
public static final String PACEMAKER_CHILDOPTS = "pacemaker.childopts";
diff --git
a/storm-server/src/main/java/org/apache/storm/pacemaker/IServerMessageHandler.java
b/storm-server/src/main/java/org/apache/storm/pacemaker/IServerMessageHandler.java
index 73451e9cf..dac6d4442 100644
---
a/storm-server/src/main/java/org/apache/storm/pacemaker/IServerMessageHandler.java
+++
b/storm-server/src/main/java/org/apache/storm/pacemaker/IServerMessageHandler.java
@@ -14,6 +14,14 @@ package org.apache.storm.pacemaker;
import org.apache.storm.generated.HBMessage;
+/**
+ * Handles heartbeat requests received by a Pacemaker server.
+ *
+ * @deprecated Pacemaker is deprecated and only kept for backward
compatibility; it will be removed in a future release.
+ * Use the default heartbeat path instead: workers heartbeat to their
supervisor, which reports them to Nimbus over
+ * Thrift, with the default ZooKeeper-based cluster state store ({@code
org.apache.storm.cluster.ZKStateStorageFactory}).
+ */
+@Deprecated
public interface IServerMessageHandler {
HBMessage handleMessage(HBMessage m, boolean authenticated);
diff --git
a/storm-server/src/main/java/org/apache/storm/pacemaker/Pacemaker.java
b/storm-server/src/main/java/org/apache/storm/pacemaker/Pacemaker.java
index 51441512c..a7809c407 100644
--- a/storm-server/src/main/java/org/apache/storm/pacemaker/Pacemaker.java
+++ b/storm-server/src/main/java/org/apache/storm/pacemaker/Pacemaker.java
@@ -33,6 +33,14 @@ import org.apache.storm.utils.VersionInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+/**
+ * The Pacemaker daemon, an in-memory store for worker heartbeats.
+ *
+ * @deprecated Pacemaker is deprecated and only kept for backward
compatibility; it will be removed in a future release.
+ * Use the default heartbeat path instead: workers heartbeat to their
supervisor, which reports them to Nimbus over
+ * Thrift, with the default ZooKeeper-based cluster state store ({@code
org.apache.storm.cluster.ZKStateStorageFactory}).
+ */
+@Deprecated
public class Pacemaker implements IServerMessageHandler {
private static final Logger LOG = LoggerFactory.getLogger(Pacemaker.class);
diff --git
a/storm-server/src/main/java/org/apache/storm/pacemaker/PacemakerServer.java
b/storm-server/src/main/java/org/apache/storm/pacemaker/PacemakerServer.java
index 19e6cbe69..123f77aac 100644
--- a/storm-server/src/main/java/org/apache/storm/pacemaker/PacemakerServer.java
+++ b/storm-server/src/main/java/org/apache/storm/pacemaker/PacemakerServer.java
@@ -38,6 +38,14 @@ import
org.apache.storm.shade.io.netty.util.concurrent.GlobalEventExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+/**
+ * Netty server of the Pacemaker daemon.
+ *
+ * @deprecated Pacemaker is deprecated and only kept for backward
compatibility; it will be removed in a future release.
+ * Use the default heartbeat path instead: workers heartbeat to their
supervisor, which reports them to Nimbus over
+ * Thrift, with the default ZooKeeper-based cluster state store ({@code
org.apache.storm.cluster.ZKStateStorageFactory}).
+ */
+@Deprecated
class PacemakerServer implements ISaslServer {
private static final int FIVE_MB_IN_BYTES = 5 * 1024 * 1024;
@@ -133,6 +141,12 @@ class PacemakerServer implements ISaslServer {
@Override
public void received(Object mesg, String remote, Channel channel) throws
InterruptedException {
+ if (!(mesg instanceof HBMessage)) {
+ LOG.warn("Dropping unexpected message of type {} from {}; closing
the connection",
+ mesg == null ? null : mesg.getClass().getName(), remote);
+ channel.close();
+ return;
+ }
cleanPipeline(channel);
boolean authenticated = (authMethod ==
ThriftNettyServerCodec.AuthMethod.NONE) ||
authenticatedChannels.contains(channel);
@@ -148,6 +162,16 @@ class PacemakerServer implements ISaslServer {
}
}
+ /**
+ * Close all channels and stop the event loops of this server. The
Pacemaker daemon itself runs until the JVM exits,
+ * so this exists for tests to shut a server down deterministically.
+ */
+ void close() {
+ allChannels.close().awaitUninterruptibly();
+ bossEventLoopGroup.shutdownGracefully().awaitUninterruptibly();
+ workerEventLoopGroup.shutdownGracefully().awaitUninterruptibly();
+ }
+
@Override
public String name() {
return topologyName;
diff --git
a/storm-server/src/main/java/org/apache/storm/pacemaker/codec/PacemakerServerHandler.java
b/storm-server/src/main/java/org/apache/storm/pacemaker/codec/PacemakerServerHandler.java
new file mode 100644
index 000000000..7bd8b1a82
--- /dev/null
+++
b/storm-server/src/main/java/org/apache/storm/pacemaker/codec/PacemakerServerHandler.java
@@ -0,0 +1,50 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership. The
ASF licenses this file to you under the Apache License, Version
+ * 2.0 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions
+ * and limitations under the License.
+ */
+
+package org.apache.storm.pacemaker.codec;
+
+import org.apache.storm.messaging.netty.IServer;
+import org.apache.storm.messaging.netty.StormServerHandler;
+import org.apache.storm.shade.io.netty.channel.ChannelHandlerContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Pacemaker server handler. A failure while handling a request only affects
the connection it arrived on: the
+ * connection is closed and the Pacemaker server keeps serving its other
clients. Errors are still handled by
+ * {@link StormServerHandler}.
+ *
+ * @deprecated Pacemaker is deprecated and only kept for backward
compatibility; it will be removed in a future release.
+ * Use the default heartbeat path instead: workers heartbeat to their
supervisor, which reports them to Nimbus over
+ * Thrift, with the default ZooKeeper-based cluster state store ({@code
org.apache.storm.cluster.ZKStateStorageFactory}).
+ */
+@Deprecated
+public class PacemakerServerHandler extends StormServerHandler {
+ private static final Logger LOG =
LoggerFactory.getLogger(PacemakerServerHandler.class);
+
+ public PacemakerServerHandler(IServer server) {
+ super(server);
+ }
+
+ @Override
+ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
+ if (!(cause instanceof Exception)) {
+ super.exceptionCaught(ctx, cause);
+ return;
+ }
+ try {
+ LOG.warn("Closing connection {} after failing to handle its
request", ctx.channel(), cause);
+ } finally {
+ ctx.close();
+ }
+ }
+}
diff --git
a/storm-server/src/main/java/org/apache/storm/pacemaker/codec/ThriftNettyServerCodec.java
b/storm-server/src/main/java/org/apache/storm/pacemaker/codec/ThriftNettyServerCodec.java
index d15f369d9..2b93372f6 100644
---
a/storm-server/src/main/java/org/apache/storm/pacemaker/codec/ThriftNettyServerCodec.java
+++
b/storm-server/src/main/java/org/apache/storm/pacemaker/codec/ThriftNettyServerCodec.java
@@ -21,7 +21,6 @@ import org.apache.storm.messaging.netty.ISaslServer;
import org.apache.storm.messaging.netty.IServer;
import org.apache.storm.messaging.netty.KerberosSaslServerHandler;
import org.apache.storm.messaging.netty.SaslStormServerHandler;
-import org.apache.storm.messaging.netty.StormServerHandler;
import org.apache.storm.security.auth.ClientAuthUtils;
import org.apache.storm.shade.io.netty.channel.Channel;
import org.apache.storm.shade.io.netty.channel.ChannelInitializer;
@@ -29,6 +28,14 @@ import
org.apache.storm.shade.io.netty.channel.ChannelPipeline;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+/**
+ * Builds the Pacemaker server pipeline.
+ *
+ * @deprecated Pacemaker is deprecated and only kept for backward
compatibility; it will be removed in a future release.
+ * Use the default heartbeat path instead: workers heartbeat to their
supervisor, which reports them to Nimbus over
+ * Thrift, with the default ZooKeeper-based cluster state store ({@code
org.apache.storm.cluster.ZKStateStorageFactory}).
+ */
+@Deprecated
public class ThriftNettyServerCodec extends ChannelInitializer<Channel> {
public static final String SASL_HANDLER = "sasl-handler";
@@ -54,7 +61,7 @@ public class ThriftNettyServerCodec extends
ChannelInitializer<Channel> {
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("encoder", new ThriftEncoder());
- pipeline.addLast("decoder", new
ThriftDecoder(thriftMessageMaxSizeBytes));
+ pipeline.addLast("decoder", new
ThriftDecoder(thriftMessageMaxSizeBytes, true));
if (authMethod == AuthMethod.DIGEST) {
try {
LOG.debug("Adding SaslStormServerHandler to pacemaker server
pipeline.");
@@ -78,7 +85,7 @@ public class ThriftNettyServerCodec extends
ChannelInitializer<Channel> {
LOG.debug("Not authenticating any clients. AuthMethod is NONE");
}
- pipeline.addLast("handler", new StormServerHandler(server));
+ pipeline.addLast("handler", new PacemakerServerHandler(server));
}
public enum AuthMethod {
diff --git a/storm-server/src/test/java/org/apache/storm/PacemakerTest.java
b/storm-server/src/test/java/org/apache/storm/PacemakerTest.java
index e8ea2a441..9fd5505f8 100644
--- a/storm-server/src/test/java/org/apache/storm/PacemakerTest.java
+++ b/storm-server/src/test/java/org/apache/storm/PacemakerTest.java
@@ -32,6 +32,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
+@SuppressWarnings("deprecation")
public class PacemakerTest {
private HBMessage hbMessage;
diff --git
a/storm-server/src/test/java/org/apache/storm/pacemaker/PacemakerServerTest.java
b/storm-server/src/test/java/org/apache/storm/pacemaker/PacemakerServerTest.java
new file mode 100644
index 000000000..1718d64ad
--- /dev/null
+++
b/storm-server/src/test/java/org/apache/storm/pacemaker/PacemakerServerTest.java
@@ -0,0 +1,261 @@
+/**
+ * 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.pacemaker;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import org.apache.storm.Config;
+import org.apache.storm.DaemonConfig;
+import org.apache.storm.generated.HBMessage;
+import org.apache.storm.generated.HBMessageData;
+import org.apache.storm.generated.HBServerMessageType;
+import org.apache.storm.messaging.netty.ControlMessage;
+import org.apache.storm.messaging.netty.ISaslClient;
+import org.apache.storm.messaging.netty.ISaslServer;
+import org.apache.storm.messaging.netty.SaslMessageToken;
+import org.apache.storm.messaging.netty.SaslStormClientHandler;
+import org.apache.storm.metric.StormMetricsRegistry;
+import org.apache.storm.pacemaker.codec.ThriftDecoder;
+import org.apache.storm.pacemaker.codec.ThriftEncoder;
+import org.apache.storm.pacemaker.codec.ThriftNettyServerCodec;
+import org.apache.storm.pacemaker.codec.ThriftNettyServerCodec.AuthMethod;
+import org.apache.storm.shade.io.netty.buffer.ByteBuf;
+import org.apache.storm.shade.io.netty.buffer.Unpooled;
+import org.apache.storm.shade.io.netty.channel.Channel;
+import org.apache.storm.shade.io.netty.channel.embedded.EmbeddedChannel;
+import org.apache.storm.utils.Utils;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@SuppressWarnings("deprecation")
+public class PacemakerServerTest {
+
+ private static final int MAX_LENGTH = 1024 * 1024;
+
+ private static PacemakerServer server;
+
+ private static Map<String, Object> config() {
+ Map<String, Object> conf = new HashMap<>();
+ conf.put(Config.PACEMAKER_PORT, 0);
+ conf.put(Config.PACEMAKER_AUTH_METHOD, "NONE");
+ conf.put(DaemonConfig.PACEMAKER_MAX_THREADS, 1);
+ conf.put(Config.PACEMAKER_THRIFT_MESSAGE_SIZE_MAX, MAX_LENGTH);
+ return conf;
+ }
+
+ private static ByteBuf frame(byte[] serialized) {
+ ByteBuf buf = Unpooled.buffer();
+ buf.writeInt(serialized.length);
+ buf.writeBytes(serialized);
+ return buf;
+ }
+
+ private static ByteBuf frame(HBMessage message) {
+ return frame(Utils.thriftSerialize(message));
+ }
+
+ private static ByteBuf controlFrame(ControlMessage controlMessage) {
+ ByteBuf buf = Unpooled.buffer();
+ controlMessage.write(buf);
+ byte[] blob = new byte[buf.readableBytes()];
+ buf.readBytes(blob);
+ return frame(new HBMessage(HBServerMessageType.CONTROL_MESSAGE,
HBMessageData.message_blob(blob)));
+ }
+
+ private static ByteBuf saslTokenFrame(short identifier, int
declaredPayloadLen, byte[] payload) {
+ ByteBuf buf = Unpooled.buffer();
+ buf.writeShort(identifier);
+ buf.writeInt(declaredPayloadLen);
+ if (payload != null) {
+ buf.writeBytes(payload);
+ }
+ byte[] blob = new byte[buf.readableBytes()];
+ buf.readBytes(blob);
+ return frame(new HBMessage(HBServerMessageType.SASL_MESSAGE_TOKEN,
HBMessageData.message_blob(blob)));
+ }
+
+ private static HBMessage readResponse(EmbeddedChannel serverChannel) {
+ EmbeddedChannel decoder = new EmbeddedChannel(new
ThriftDecoder(MAX_LENGTH));
+ Object out;
+ while ((out = serverChannel.readOutbound()) != null) {
+ decoder.writeInbound(out);
+ }
+ return decoder.readInbound();
+ }
+
+ private EmbeddedChannel pipeline() {
+ return new EmbeddedChannel(new ThriftNettyServerCodec(server,
config(), AuthMethod.NONE, MAX_LENGTH));
+ }
+
+ @BeforeAll
+ public static void setUp() {
+ server = new PacemakerServer(new Pacemaker(new ConcurrentHashMap<>(),
new StormMetricsRegistry()), config());
+ }
+
+ @AfterAll
+ public static void tearDown() {
+ server.close();
+ }
+
+ @Test
+ public void heartbeatRequestIsAnswered() {
+ EmbeddedChannel channel = pipeline();
+ HBMessage request = new HBMessage(HBServerMessageType.CREATE_PATH,
HBMessageData.path("/path"));
+ request.set_message_id(7);
+
+ channel.writeInbound(frame(request));
+
+ HBMessage response = readResponse(channel);
+ assertEquals(HBServerMessageType.CREATE_PATH_RESPONSE,
response.get_type());
+ assertEquals(7, response.get_message_id());
+ assertTrue(channel.isActive());
+ }
+
+ @Test
+ public void controlFrameClosesOnlyThatConnection() {
+ EmbeddedChannel other = pipeline();
+ EmbeddedChannel channel = pipeline();
+
+ channel.writeInbound(controlFrame(ControlMessage.CLOSE_MESSAGE));
+
+ assertFalse(channel.isActive());
+ assertNull(channel.readOutbound());
+
+ other.writeInbound(frame(new
HBMessage(HBServerMessageType.CREATE_PATH, HBMessageData.path("/path"))));
+ assertEquals(HBServerMessageType.CREATE_PATH_RESPONSE,
readResponse(other).get_type());
+ assertTrue(other.isActive());
+ }
+
+ @Test
+ public void oversizedSaslTokenClosesOnlyThatConnection() {
+ EmbeddedChannel other = pipeline();
+ EmbeddedChannel channel = pipeline();
+
+ // A SASL token frame that declares a ~2GB payload previously OOM'd
the decoder and terminated the daemon.
+ channel.writeInbound(saslTokenFrame(SaslMessageToken.IDENTIFIER,
Integer.MAX_VALUE, null));
+
+ assertFalse(channel.isActive());
+ assertNull(channel.readOutbound());
+
+ other.writeInbound(frame(new
HBMessage(HBServerMessageType.CREATE_PATH, HBMessageData.path("/path"))));
+ assertEquals(HBServerMessageType.CREATE_PATH_RESPONSE,
readResponse(other).get_type());
+ assertTrue(other.isActive());
+ }
+
+ @Test
+ public void saslFrameWithoutAuthenticationConfiguredClosesConnection() {
+ // Passes the decoder, but no SASL handler is installed when the auth
method is NONE.
+ EmbeddedChannel channel = pipeline();
+
+
channel.writeInbound(controlFrame(ControlMessage.SASL_TOKEN_MESSAGE_REQUEST));
+
+ assertFalse(channel.isActive());
+ assertNull(channel.readOutbound());
+ }
+
+ @Test
+ public void receivedDropsUnexpectedMessageType() throws Exception {
+ EmbeddedChannel channel = new EmbeddedChannel();
+
+ server.received(ControlMessage.CLOSE_MESSAGE, "remote", channel);
+
+ assertFalse(channel.isActive());
+ assertNull(channel.readOutbound());
+ }
+
+ @Test
+ public void undecodableFrameClosesConnection() {
+ EmbeddedChannel channel = pipeline();
+
+ channel.writeInbound(frame(new byte[]{ 0x7f, 0x7f, 0x7f, 0x7f }));
+
+ assertFalse(channel.isActive());
+ }
+
+ @Test
+ public void requestFailureClosesConnection() {
+ EmbeddedChannel channel = pipeline();
+
+ channel.writeInbound(frame(new
HBMessage(HBServerMessageType.SEND_PULSE, null)));
+
+ assertFalse(channel.isActive());
+ assertNull(channel.readOutbound());
+ }
+
+ @Test
+ public void digestHandshakeAuthenticatesChannel() throws Exception {
+ ISaslServer saslServer = mock(ISaslServer.class);
+ when(saslServer.name()).thenReturn("pacemaker_server");
+ when(saslServer.secretKey()).thenReturn("secret");
+ ISaslClient saslClient = mock(ISaslClient.class);
+ when(saslClient.name()).thenReturn("pacemaker_server");
+ when(saslClient.secretKey()).thenReturn("secret");
+
+ EmbeddedChannel serverChannel = new EmbeddedChannel(
+ new ThriftNettyServerCodec(saslServer, config(),
AuthMethod.DIGEST, MAX_LENGTH));
+ EmbeddedChannel clientChannel = new EmbeddedChannel(
+ new ThriftEncoder(), new ThriftDecoder(MAX_LENGTH), new
SaslStormClientHandler(saslClient));
+
+ exchange(clientChannel, serverChannel);
+
+ verify(saslServer).authenticated(serverChannel);
+ verify(saslClient, atLeastOnce()).channelReady(any(Channel.class));
+
assertNull(serverChannel.pipeline().get(ThriftNettyServerCodec.SASL_HANDLER));
+ assertTrue(serverChannel.isActive());
+ assertTrue(clientChannel.isActive());
+ verify(saslServer, never()).received(any(), anyString(),
any(Channel.class));
+
+ HBMessage request = new HBMessage(HBServerMessageType.GET_PULSE,
HBMessageData.path("/path"));
+ serverChannel.writeInbound(frame(request));
+ verify(saslServer).received(request,
serverChannel.remoteAddress().toString(), serverChannel);
+
+ // Control frames other than the handshake request are still refused
once authenticated.
+ serverChannel.writeInbound(controlFrame(ControlMessage.EOB_MESSAGE));
+ assertFalse(serverChannel.isActive());
+ }
+
+ private static void exchange(EmbeddedChannel client, EmbeddedChannel
server) {
+ boolean progress = true;
+ while (progress) {
+ progress = forward(client, server) | forward(server, client);
+ }
+ }
+
+ private static boolean forward(EmbeddedChannel from, EmbeddedChannel to) {
+ List<Object> frames = new ArrayList<>();
+ Object out;
+ while ((out = from.readOutbound()) != null) {
+ frames.add(out);
+ }
+ for (Object frame : frames) {
+ to.writeInbound(frame);
+ }
+ return !frames.isEmpty();
+ }
+}