From 6d2b96c79e75fb230ec0fd9f34d7910d7644593f Mon Sep 17 00:00:00 2001 From: Domenico Francesco Bruscino Date: Tue, 22 Jun 2021 09:52:48 +0200 Subject: [PATCH 1/2] ARTEMIS-3275 Lock CORE client communication during failover retries --- .../client/impl/ClientSessionFactoryImpl.java | 38 ++++++++----- .../core/client/impl/ClientSessionImpl.java | 54 +++++++++++-------- .../client/impl/ClientSessionInternal.java | 2 + .../artemis/core/protocol/core/Channel.java | 7 +++ .../core/impl/ActiveMQSessionContext.java | 2 +- .../core/protocol/core/impl/ChannelImpl.java | 5 ++ .../cluster/failover/FailoverTest.java | 41 ++++++++++++++ .../cluster/util/BackupSyncDelay.java | 5 ++ 8 files changed, 118 insertions(+), 36 deletions(-) diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/client/impl/ClientSessionFactoryImpl.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/client/impl/ClientSessionFactoryImpl.java index 40fb156777..92236f4aa4 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/client/impl/ClientSessionFactoryImpl.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/client/impl/ClientSessionFactoryImpl.java @@ -632,10 +632,19 @@ public class ClientSessionFactoryImpl implements ClientSessionFactoryInternal, C connector = null; - boolean allSessionReconnected; + HashSet sessionsToFailover; + synchronized (sessions) { + sessionsToFailover = new HashSet<>(sessions); + } + + for (ClientSessionInternal session : sessionsToFailover) { + session.preHandleFailover(connection); + } + + boolean allSessionReconnected = false; int failedReconnectSessionsCounter = 0; do { - allSessionReconnected = reconnectSessions(oldConnection, reconnectAttempts, me); + allSessionReconnected = reconnectSessions(sessionsToFailover, oldConnection, reconnectAttempts, me); if (oldConnection != null) { oldConnection.destroy(); } @@ -644,10 +653,19 @@ public class ClientSessionFactoryImpl implements ClientSessionFactoryInternal, C failedReconnectSessionsCounter++; oldConnection = connection; connection = null; + + // Wait for retry when the connection is established but not all session are reconnected. + if ((reconnectAttempts == -1 || failedReconnectSessionsCounter < reconnectAttempts) && oldConnection != null) { + waitForRetry(retryInterval); + } } } while ((reconnectAttempts == -1 || failedReconnectSessionsCounter < reconnectAttempts) && !allSessionReconnected); + for (ClientSessionInternal session : sessionsToFailover) { + session.postHandleFailover(connection, allSessionReconnected); + } + if (oldConnection != null) { oldConnection.destroy(); } @@ -764,18 +782,10 @@ public class ClientSessionFactoryImpl implements ClientSessionFactoryInternal, C /* * Re-attach sessions all pre-existing sessions to the new remoting connection */ - private boolean reconnectSessions(final RemotingConnection oldConnection, - final int reconnectAttempts, - final ActiveMQException cause) { - HashSet sessionsToFailover; - synchronized (sessions) { - sessionsToFailover = new HashSet<>(sessions); - } - - for (ClientSessionInternal session : sessionsToFailover) { - session.preHandleFailover(connection); - } - + private boolean reconnectSessions(final Set sessionsToFailover, + final RemotingConnection oldConnection, + final int reconnectAttempts, + final ActiveMQException cause) { getConnectionWithRetry(reconnectAttempts, oldConnection); if (connection == null) { diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/client/impl/ClientSessionImpl.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/client/impl/ClientSessionImpl.java index d3a66a5682..33e5a77f6c 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/client/impl/ClientSessionImpl.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/client/impl/ClientSessionImpl.java @@ -141,6 +141,8 @@ public final class ClientSessionImpl implements ClientSessionInternal, FailureLi private volatile boolean mayAttemptToFailover = true; + private volatile boolean resetCreditManager = false; + /** * Current XID. this will be used in case of failover */ @@ -1387,8 +1389,6 @@ public final class ClientSessionImpl implements ClientSessionInternal, FailureLi return true; } - boolean resetCreditManager = false; - try { // TODO remove this and encapsulate it @@ -1463,30 +1463,42 @@ public final class ClientSessionImpl implements ClientSessionInternal, FailureLi } catch (Throwable t) { ActiveMQClientLogger.LOGGER.failedToHandleFailover(t); suc = false; - } finally { - sessionContext.releaseCommunications(); - } - - if (resetCreditManager) { - synchronized (producerCreditManager) { - producerCreditManager.reset(); - } - - // Also need to send more credits for consumers, otherwise the system could hand with the server - // not having any credits to send } } - HashMap metaDataToSend; - - synchronized (metadata) { - metaDataToSend = new HashMap<>(metadata); - } - - sessionContext.resetMetadata(metaDataToSend); - return suc; + } + @Override + public void postHandleFailover(RemotingConnection connection, boolean successful) { + sessionContext.releaseCommunications(); + + if (successful) { + synchronized (this) { + if (closed) { + return; + } + + if (resetCreditManager) { + synchronized (producerCreditManager) { + producerCreditManager.reset(); + } + + resetCreditManager = false; + + // Also need to send more credits for consumers, otherwise the system could hand with the server + // not having any credits to send + } + } + + HashMap metaDataToSend; + + synchronized (metadata) { + metaDataToSend = new HashMap<>(metadata); + } + + sessionContext.resetMetadata(metaDataToSend); + } } @Override diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/client/impl/ClientSessionInternal.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/client/impl/ClientSessionInternal.java index a3700b2e50..173087d9cc 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/client/impl/ClientSessionInternal.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/client/impl/ClientSessionInternal.java @@ -68,6 +68,8 @@ public interface ClientSessionInternal extends ClientSession { boolean handleFailover(RemotingConnection backupConnection, ActiveMQException cause); + void postHandleFailover(RemotingConnection connection, boolean successful); + RemotingConnection getConnection(); void cleanUp(boolean failingOver) throws ActiveMQException; diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/Channel.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/Channel.java index 372cad4db3..12817294c6 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/Channel.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/Channel.java @@ -184,6 +184,13 @@ public interface Channel { */ int getLastConfirmedCommandID(); + /** + * queries if this channel is locked. This method is designed for use in monitoring of the system state, not for synchronization control. + * + * @return true it the channel is locked and false otherwise + */ + boolean isLocked(); + /** * locks the channel. *

diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/ActiveMQSessionContext.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/ActiveMQSessionContext.java index 4e122a9c87..811ebef669 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/ActiveMQSessionContext.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/ActiveMQSessionContext.java @@ -139,7 +139,7 @@ public class ActiveMQSessionContext extends SessionContext { private String name; private boolean killed; - protected Channel getSessionChannel() { + public Channel getSessionChannel() { return sessionChannel; } diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/ChannelImpl.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/ChannelImpl.java index 4ecd2c63de..18bb08cc55 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/ChannelImpl.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/ChannelImpl.java @@ -689,6 +689,11 @@ public final class ChannelImpl implements Channel { } } + @Override + public boolean isLocked() { + return failingOver; + } + @Override public void lock() { if (logger.isTraceEnabled()) { diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/cluster/failover/FailoverTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/cluster/failover/FailoverTest.java index 3dcf9a9d22..43756f1b58 100644 --- a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/cluster/failover/FailoverTest.java +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/cluster/failover/FailoverTest.java @@ -27,6 +27,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import org.apache.activemq.artemis.api.core.ActiveMQDuplicateIdException; import org.apache.activemq.artemis.api.core.ActiveMQException; @@ -50,8 +51,10 @@ import org.apache.activemq.artemis.api.core.client.ServerLocator; import org.apache.activemq.artemis.api.core.client.SessionFailureListener; import org.apache.activemq.artemis.core.client.impl.ClientSessionFactoryImpl; import org.apache.activemq.artemis.core.client.impl.ClientSessionFactoryInternal; +import org.apache.activemq.artemis.core.client.impl.ClientSessionInternal; import org.apache.activemq.artemis.core.protocol.core.Channel; import org.apache.activemq.artemis.core.protocol.core.Packet; +import org.apache.activemq.artemis.core.protocol.core.impl.ActiveMQSessionContext; import org.apache.activemq.artemis.core.protocol.core.impl.ChannelImpl; import org.apache.activemq.artemis.core.protocol.core.impl.PacketImpl; import org.apache.activemq.artemis.core.protocol.core.impl.RemotingConnectionImpl; @@ -1971,6 +1974,44 @@ public class FailoverTest extends FailoverTestBase { Assert.assertNotNull(message); } + @Test(timeout = 120000) + public void testChannelStateDuringFailover() throws Exception { + locator.setBlockOnNonDurableSend(true).setBlockOnDurableSend(true).setBlockOnAcknowledge(true).setReconnectAttempts(300).setRetryInterval(100); + + sf = createSessionFactoryAndWaitForTopology(locator, 2); + + final AtomicBoolean channelLockedDuringFailover = new AtomicBoolean(false); + + ClientSession session = createSession(sf, true, true, 0); + + backupServer.addInterceptor( + new Interceptor() { + private int index = 0; + + @Override + public boolean intercept(Packet packet, RemotingConnection connection) throws ActiveMQException { + if (index < 1 && packet.getType() == PacketImpl.CREATESESSION) { + sf.getConnection().addCloseListener(() -> { + index++; + ActiveMQSessionContext sessionContext = (ActiveMQSessionContext)((ClientSessionInternal)session).getSessionContext(); + channelLockedDuringFailover.set(sessionContext.getSessionChannel().isLocked()); + }); + + Channel sessionChannel = ((RemotingConnectionImpl)connection).getChannel(ChannelImpl.CHANNEL_ID.SESSION.id, -1); + sessionChannel.send(new ActiveMQExceptionMessage(new ActiveMQInternalErrorException())); + return false; + } + return true; + } + }); + + session.start(); + + crash(session); + + Assert.assertTrue(channelLockedDuringFailover.get()); + } + @Test(timeout = 120000) public void testForceBlockingReturn() throws Exception { locator.setBlockOnNonDurableSend(true).setBlockOnDurableSend(true).setBlockOnAcknowledge(true).setReconnectAttempts(300).setRetryInterval(100); diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/cluster/util/BackupSyncDelay.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/cluster/util/BackupSyncDelay.java index b0af71b367..287d730efc 100644 --- a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/cluster/util/BackupSyncDelay.java +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/cluster/util/BackupSyncDelay.java @@ -300,6 +300,11 @@ public class BackupSyncDelay implements Interceptor { throw new UnsupportedOperationException(); } + @Override + public boolean isLocked() { + return false; + } + @Override public void lock() { throw new UnsupportedOperationException(); From 3555dd7d25492f0adea7795fc2cbf4047d520981 Mon Sep 17 00:00:00 2001 From: Domenico Francesco Bruscino Date: Fri, 6 Aug 2021 00:49:11 +0200 Subject: [PATCH 2/2] ARTEMIS-3365 Add broker balancers --- .../cli/commands/check/CheckAbstract.java | 6 +- .../cli/commands/check/CheckContext.java | 6 +- .../artemis/cli/commands/check/NodeCheck.java | 27 +- .../cli/commands/check/QueueCheck.java | 2 +- .../api/core/ActiveMQExceptionType.java | 6 + .../api/core/ActiveMQRedirectedException.java | 34 ++ .../artemis/api/core/DisconnectReason.java | 73 ++++ .../api/core/TransportConfiguration.java | 2 +- .../api/core/client/ClientSession.java | 8 + .../api/core/client/ClientSessionFactory.java | 26 ++ .../management/ActiveMQManagementProxy.java | 73 ++-- .../management/BrokerBalancerControl.java | 31 ++ .../core/management/ObjectNameBuilder.java | 9 + .../api/core/management/ResourceNames.java | 2 + .../client/ActiveMQClientMessageBundle.java | 4 + .../client/impl/ClientSessionFactoryImpl.java | 210 +++++---- .../core/client/impl/ClientSessionImpl.java | 4 + .../protocol/core/CoreRemotingConnection.java | 10 + .../impl/ActiveMQClientProtocolManager.java | 63 ++- .../core/protocol/core/impl/ChannelImpl.java | 4 + .../protocol/core/impl/PacketDecoder.java | 18 + .../core/protocol/core/impl/PacketImpl.java | 7 + .../core/impl/RemotingConnectionImpl.java | 23 +- .../ClusterTopologyChangeMessage_V3.java | 29 +- .../ClusterTopologyChangeMessage_V4.java | 91 ++++ .../impl/wireformat/CreateSessionMessage.java | 37 +- .../wireformat/CreateSessionMessage_V2.java | 103 +++++ .../impl/wireformat/DisconnectMessage_V3.java | 137 ++++++ .../impl/netty/TransportConstants.java | 5 + .../spi/core/protocol/RemotingConnection.java | 8 + .../core/remoting/ClientProtocolManager.java | 3 +- .../artemis/spi/core/remoting/Connection.java | 8 + .../remoting/TopologyResponseHandler.java | 3 +- .../resources/activemq-version.properties | 2 +- .../management/OperationAnnotationTest.java | 3 +- .../jms/client/ActiveMQConnection.java | 30 +- .../amqp/broker/ProtonProtocolManager.java | 10 +- .../client/ProtonClientProtocolManager.java | 2 +- .../connect/AMQPBrokerConnectionManager.java | 3 +- .../amqp/proton/AMQPConnectionContext.java | 4 +- .../amqp/proton/AMQPRedirectContext.java | 37 ++ .../amqp/proton/AMQPRedirectHandler.java | 64 +++ .../amqp/sasl/AnonymousServerSASLFactory.java | 3 +- .../amqp/sasl/ExternalServerSASLFactory.java | 3 +- .../amqp/sasl/GSSAPIServerSASLFactory.java | 3 +- .../amqp/sasl/PlainServerSASLFactory.java | 3 +- .../protocol/amqp/sasl/ServerSASLFactory.java | 3 +- .../sasl/scram/SCRAMServerSASLFactory.java | 3 +- .../client/HornetQClientProtocolManager.java | 9 +- .../protocol/mqtt/MQTTProtocolHandler.java | 16 +- .../protocol/mqtt/MQTTProtocolManager.java | 10 +- .../protocol/mqtt/MQTTRedirectContext.java | 37 ++ .../protocol/mqtt/MQTTRedirectHandler.java | 55 +++ .../protocol/openwire/OpenWireConnection.java | 6 + .../openwire/OpenWireProtocolManager.java | 10 +- .../openwire/OpenWireRedirectContext.java | 37 ++ .../openwire/OpenWireRedirectHandler.java | 58 +++ .../protocol/stomp/StompProtocolManager.java | 8 +- .../artemis/ra/inflow/ActiveMQActivation.java | 3 +- .../artemis/core/config/Configuration.java | 13 + .../BrokerBalancerConfiguration.java | 95 +++++ .../config/balancing/PolicyConfiguration.java | 45 ++ .../config/balancing/PoolConfiguration.java | 123 ++++++ .../core/config/impl/ConfigurationImpl.java | 20 + .../artemis/core/config/impl/Validators.java | 11 + .../impl/FileConfigurationParser.java | 102 ++++- .../impl/BrokerBalancerControlImpl.java | 160 +++++++ .../management/impl/view/ConsumerView.java | 2 +- .../management/impl/view/ProducerView.java | 2 +- .../core/protocol/ProtocolHandler.java | 5 + .../core/impl/ActiveMQPacketHandler.java | 14 +- .../core/impl/ActiveMQRedirectContext.java | 28 ++ .../core/impl/ActiveMQRedirectHandler.java | 52 +++ .../core/impl/CoreProtocolManager.java | 25 +- .../remoting/impl/netty/NettyAcceptor.java | 7 +- .../impl/netty/NettySNIHostnameHandler.java | 42 ++ .../impl/netty/NettyServerConnection.java | 22 +- .../core/server/ActiveMQMessageBundle.java | 11 + .../artemis/core/server/ActiveMQServer.java | 3 + .../core/server/ActiveMQServerLogger.java | 14 + .../core/server/balancing/BrokerBalancer.java | 193 +++++++++ .../balancing/BrokerBalancerManager.java | 191 +++++++++ .../server/balancing/RedirectContext.java | 57 +++ .../server/balancing/RedirectHandler.java | 74 ++++ .../balancing/policies/AbstractPolicy.java | 52 +++ .../policies/ConsistentHashPolicy.java | 75 ++++ .../policies/DefaultPolicyFactory.java | 49 +++ .../policies/FirstElementPolicy.java | 43 ++ .../policies/LeastConnectionsPolicy.java | 133 ++++++ .../server/balancing/policies/Policy.java | 36 ++ .../balancing/policies/PolicyFactory.java | 24 ++ .../policies/PolicyFactoryResolver.java | 74 ++++ .../balancing/policies/RoundRobinPolicy.java | 47 +++ .../server/balancing/pools/AbstractPool.java | 243 +++++++++++ .../server/balancing/pools/ClusterPool.java | 69 +++ .../pools/DiscoveryGroupService.java | 87 ++++ .../server/balancing/pools/DiscoveryPool.java | 70 +++ .../balancing/pools/DiscoveryService.java | 88 ++++ .../core/server/balancing/pools/Pool.java | 65 +++ .../server/balancing/pools/StaticPool.java | 44 ++ .../balancing/targets/AbstractTarget.java | 112 +++++ .../targets/AbstractTargetFactory.java | 45 ++ .../balancing/targets/ActiveMQTarget.java | 132 ++++++ .../targets/ActiveMQTargetFactory.java | 32 ++ .../server/balancing/targets/LocalTarget.java | 69 +++ .../core/server/balancing/targets/Target.java | 59 +++ .../balancing/targets/TargetFactory.java | 32 ++ .../server/balancing/targets/TargetKey.java | 53 +++ .../balancing/targets/TargetKeyResolver.java | 108 +++++ .../balancing/targets/TargetListener.java | 24 ++ .../balancing/targets/TargetMonitor.java | 129 ++++++ .../server/balancing/targets/TargetProbe.java | 32 ++ .../core/server/impl/ActiveMQServerImpl.java | 16 + .../server/management/ManagementService.java | 9 + .../impl/ManagementServiceImpl.java | 26 +- .../protocol/AbstractProtocolManager.java | 3 +- .../spi/core/protocol/ProtocolManager.java | 5 +- .../schema/artemis-configuration.xsd | 175 ++++++++ .../config/impl/FileConfigurationTest.java | 37 ++ .../policies/ConsistentHashPolicyTest.java | 55 +++ .../policies/FirstElementPolicyTest.java | 47 +++ .../policies/LeastConnectionsPolicyTest.java | 95 +++++ .../balancing/policies/PolicyTestBase.java | 52 +++ .../policies/RoundRobinPolicyTest.java | 58 +++ .../balancing/pools/DiscoveryPoolTest.java | 174 ++++++++ .../balancing/pools/MockDiscoveryService.java | 87 ++++ .../server/balancing/pools/PoolTestBase.java | 190 +++++++++ .../balancing/pools/StaticPoolTest.java | 39 ++ .../server/balancing/targets/MockTarget.java | 156 +++++++ .../balancing/targets/MockTargetFactory.java | 105 +++++ .../balancing/targets/MockTargetProbe.java | 61 +++ .../targets/TargetKeyResolverTest.java | 110 +++++ .../group/impl/ClusteredResetMockTest.java | 21 + .../ConfigurationTest-full-config.xml | 32 ++ .../ConfigurationTest-xinclude-config.xml | 32 ++ docs/user-manual/en/SUMMARY.md | 1 + docs/user-manual/en/broker-balancers.md | 191 +++++++++ .../en/images/broker_balancer_workflow.png | Bin 0 -> 96089 bytes .../management_api_redirect_sequence.png | Bin 0 -> 12611 bytes .../en/images/native_redirect_sequence.png | Bin 0 -> 11169 bytes .../broker-balancer/evenly-redirect/pom.xml | 207 +++++++++ .../broker-balancer/evenly-redirect/readme.md | 8 + .../jms/example/EvenlyRedirectExample.java | 106 +++++ .../resources/activemq/server0/broker.xml | 135 ++++++ .../resources/activemq/server1/broker.xml | 113 +++++ .../resources/activemq/server2/broker.xml | 113 +++++ examples/features/broker-balancer/pom.xml | 56 +++ .../symmetric-redirect/pom.xml | 164 +++++++ .../symmetric-redirect/readme.md | 9 + .../jms/example/SymmetricRedirectExample.java | 107 +++++ .../resources/activemq/server0/broker.xml | 150 +++++++ .../resources/activemq/server1/broker.xml | 150 +++++++ pom.xml | 2 +- .../balancing/BalancingTestBase.java | 246 +++++++++++ .../balancing/MQTTRedirectTest.java | 125 ++++++ .../integration/balancing/RedirectTest.java | 399 ++++++++++++++++++ .../integration/balancing/TargetKeyTest.java | 184 ++++++++ .../cluster/failover/FailoverTest.java | 8 +- .../SessionMetadataAddExceptionTest.java | 4 +- .../management/ActiveMQServerControlTest.java | 6 +- .../management/BrokerBalancerControlTest.java | 186 ++++++++ .../management/ManagementControlHelper.java | 7 + tests/security-resources/build.sh | 18 +- .../security-resources/client-ca-keystore.p12 | Bin 2589 -> 2589 bytes .../client-ca-truststore.jceks | Bin 950 -> 950 bytes .../client-ca-truststore.jks | Bin 950 -> 950 bytes .../client-ca-truststore.p12 | Bin 1186 -> 1186 bytes tests/security-resources/client-ca.pem | 54 +-- .../security-resources/client-keystore.jceks | Bin 4124 -> 4124 bytes tests/security-resources/client-keystore.jks | Bin 4144 -> 4143 bytes tests/security-resources/client-keystore.p12 | Bin 4759 -> 4759 bytes tests/security-resources/other-client-crl.pem | 16 +- .../other-client-keystore.jceks | Bin 4136 -> 4136 bytes .../other-client-keystore.jks | Bin 4156 -> 4155 bytes .../other-client-keystore.p12 | Bin 4787 -> 4787 bytes tests/security-resources/other-server-crl.pem | 16 +- .../other-server-keystore.jceks | Bin 4136 -> 4183 bytes .../other-server-keystore.jks | Bin 4155 -> 4202 bytes .../other-server-keystore.p12 | Bin 4787 -> 4835 bytes .../other-server-truststore.jceks | Bin 1053 -> 1100 bytes .../other-server-truststore.jks | Bin 1053 -> 1100 bytes .../other-server-truststore.p12 | Bin 1290 -> 1338 bytes .../security-resources/server-ca-keystore.p12 | Bin 2589 -> 2589 bytes .../server-ca-truststore.jceks | Bin 950 -> 950 bytes .../server-ca-truststore.jks | Bin 950 -> 950 bytes .../server-ca-truststore.p12 | Bin 1186 -> 1186 bytes tests/security-resources/server-ca.pem | 54 +-- .../security-resources/server-keystore.jceks | Bin 4103 -> 4150 bytes tests/security-resources/server-keystore.jks | Bin 4122 -> 4169 bytes tests/security-resources/server-keystore.p12 | Bin 4735 -> 4783 bytes .../unknown-client-keystore.jceks | Bin 4112 -> 4112 bytes .../unknown-client-keystore.jks | Bin 4132 -> 4131 bytes .../unknown-client-keystore.p12 | Bin 4767 -> 4767 bytes .../unknown-server-keystore.jceks | Bin 4112 -> 4112 bytes .../unknown-server-keystore.jks | Bin 4131 -> 4130 bytes .../unknown-server-keystore.p12 | Bin 4767 -> 4767 bytes 196 files changed, 8931 insertions(+), 320 deletions(-) create mode 100644 artemis-commons/src/main/java/org/apache/activemq/artemis/api/core/ActiveMQRedirectedException.java create mode 100644 artemis-commons/src/main/java/org/apache/activemq/artemis/api/core/DisconnectReason.java create mode 100644 artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/management/BrokerBalancerControl.java create mode 100644 artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/wireformat/ClusterTopologyChangeMessage_V4.java create mode 100644 artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/wireformat/CreateSessionMessage_V2.java create mode 100644 artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/wireformat/DisconnectMessage_V3.java create mode 100644 artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/AMQPRedirectContext.java create mode 100644 artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/AMQPRedirectHandler.java create mode 100644 artemis-protocols/artemis-mqtt-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/mqtt/MQTTRedirectContext.java create mode 100644 artemis-protocols/artemis-mqtt-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/mqtt/MQTTRedirectHandler.java create mode 100644 artemis-protocols/artemis-openwire-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/openwire/OpenWireRedirectContext.java create mode 100644 artemis-protocols/artemis-openwire-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/openwire/OpenWireRedirectHandler.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/config/balancing/BrokerBalancerConfiguration.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/config/balancing/PolicyConfiguration.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/config/balancing/PoolConfiguration.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/management/impl/BrokerBalancerControlImpl.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/ActiveMQRedirectContext.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/ActiveMQRedirectHandler.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/remoting/impl/netty/NettySNIHostnameHandler.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/BrokerBalancer.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/BrokerBalancerManager.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/RedirectContext.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/RedirectHandler.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/policies/AbstractPolicy.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/policies/ConsistentHashPolicy.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/policies/DefaultPolicyFactory.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/policies/FirstElementPolicy.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/policies/LeastConnectionsPolicy.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/policies/Policy.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/policies/PolicyFactory.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/policies/PolicyFactoryResolver.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/policies/RoundRobinPolicy.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/pools/AbstractPool.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/pools/ClusterPool.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/pools/DiscoveryGroupService.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/pools/DiscoveryPool.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/pools/DiscoveryService.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/pools/Pool.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/pools/StaticPool.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/AbstractTarget.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/AbstractTargetFactory.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/ActiveMQTarget.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/ActiveMQTargetFactory.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/LocalTarget.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/Target.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/TargetFactory.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/TargetKey.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/TargetKeyResolver.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/TargetListener.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/TargetMonitor.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/TargetProbe.java create mode 100644 artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/policies/ConsistentHashPolicyTest.java create mode 100644 artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/policies/FirstElementPolicyTest.java create mode 100644 artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/policies/LeastConnectionsPolicyTest.java create mode 100644 artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/policies/PolicyTestBase.java create mode 100644 artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/policies/RoundRobinPolicyTest.java create mode 100644 artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/pools/DiscoveryPoolTest.java create mode 100644 artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/pools/MockDiscoveryService.java create mode 100644 artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/pools/PoolTestBase.java create mode 100644 artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/pools/StaticPoolTest.java create mode 100644 artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/targets/MockTarget.java create mode 100644 artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/targets/MockTargetFactory.java create mode 100644 artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/targets/MockTargetProbe.java create mode 100644 artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/targets/TargetKeyResolverTest.java create mode 100644 docs/user-manual/en/broker-balancers.md create mode 100644 docs/user-manual/en/images/broker_balancer_workflow.png create mode 100644 docs/user-manual/en/images/management_api_redirect_sequence.png create mode 100644 docs/user-manual/en/images/native_redirect_sequence.png create mode 100644 examples/features/broker-balancer/evenly-redirect/pom.xml create mode 100644 examples/features/broker-balancer/evenly-redirect/readme.md create mode 100644 examples/features/broker-balancer/evenly-redirect/src/main/java/org/apache/activemq/artemis/jms/example/EvenlyRedirectExample.java create mode 100644 examples/features/broker-balancer/evenly-redirect/src/main/resources/activemq/server0/broker.xml create mode 100644 examples/features/broker-balancer/evenly-redirect/src/main/resources/activemq/server1/broker.xml create mode 100644 examples/features/broker-balancer/evenly-redirect/src/main/resources/activemq/server2/broker.xml create mode 100644 examples/features/broker-balancer/pom.xml create mode 100644 examples/features/broker-balancer/symmetric-redirect/pom.xml create mode 100644 examples/features/broker-balancer/symmetric-redirect/readme.md create mode 100644 examples/features/broker-balancer/symmetric-redirect/src/main/java/org/apache/activemq/artemis/jms/example/SymmetricRedirectExample.java create mode 100644 examples/features/broker-balancer/symmetric-redirect/src/main/resources/activemq/server0/broker.xml create mode 100644 examples/features/broker-balancer/symmetric-redirect/src/main/resources/activemq/server1/broker.xml create mode 100644 tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/balancing/BalancingTestBase.java create mode 100644 tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/balancing/MQTTRedirectTest.java create mode 100644 tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/balancing/RedirectTest.java create mode 100644 tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/balancing/TargetKeyTest.java create mode 100644 tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/management/BrokerBalancerControlTest.java diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/check/CheckAbstract.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/check/CheckAbstract.java index ff1057522d..3d7019b8e4 100644 --- a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/check/CheckAbstract.java +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/check/CheckAbstract.java @@ -25,7 +25,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import io.airlift.airline.Option; -import org.apache.activemq.artemis.api.core.client.ServerLocator; import org.apache.activemq.artemis.api.core.management.ActiveMQManagementProxy; import org.apache.activemq.artemis.cli.CLIException; import org.apache.activemq.artemis.cli.commands.AbstractAction; @@ -72,10 +71,7 @@ public abstract class CheckAbstract extends AbstractAction { int successTasks = 0; try (ActiveMQConnectionFactory factory = createCoreConnectionFactory(); - ServerLocator serverLocator = factory.getServerLocator(); - ActiveMQManagementProxy managementProxy = new ActiveMQManagementProxy(serverLocator, user, password)) { - - managementProxy.start(); + ActiveMQManagementProxy managementProxy = new ActiveMQManagementProxy(factory.getServerLocator(), user, password)) { StopWatch watch = new StopWatch(); CheckTask[] checkTasks = getCheckTasks(); diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/check/CheckContext.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/check/CheckContext.java index d1e8ca57ff..617b603f83 100644 --- a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/check/CheckContext.java +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/check/CheckContext.java @@ -61,7 +61,7 @@ public class CheckContext extends ActionContext { public String getNodeId() throws Exception { if (nodeId == null) { - nodeId = managementProxy.invokeOperation(String.class, "broker", "getNodeID"); + nodeId = managementProxy.getAttribute("broker", "NodeID", String.class, 0); } return nodeId; @@ -69,8 +69,8 @@ public class CheckContext extends ActionContext { public Map getTopology() throws Exception { if (topology == null) { - topology = Arrays.stream(NodeInfo.from(managementProxy.invokeOperation( - String.class, "broker", "listNetworkTopology"))). + topology = Arrays.stream(NodeInfo.from((String)managementProxy. + invokeOperation("broker", "listNetworkTopology", null, null, 0))). collect(Collectors.toMap(node -> node.getId(), node -> node)); } diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/check/NodeCheck.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/check/NodeCheck.java index 601d254016..3897a15b2b 100644 --- a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/check/NodeCheck.java +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/check/NodeCheck.java @@ -138,7 +138,7 @@ public class NodeCheck extends CheckAbstract { } private void checkNodeUp(final CheckContext context) throws Exception { - if (!context.getManagementProxy().invokeOperation(Boolean.class, "broker", "isStarted")) { + if (!context.getManagementProxy().getAttribute("broker", "Started", Boolean.class, 0)) { throw new CheckException("The node isn't started."); } } @@ -182,28 +182,31 @@ public class NodeCheck extends CheckAbstract { } private void checkNodeDiskUsage(final CheckContext context) throws Exception { - int thresholdValue; + Integer maxDiskUsage; if (diskUsage == -1) { - thresholdValue = context.getManagementProxy().invokeOperation( - int.class, "broker", "getMaxDiskUsage"); + maxDiskUsage = context.getManagementProxy(). + getAttribute("broker", "MaxDiskUsage", Integer.class, 0); } else { - thresholdValue = diskUsage; + maxDiskUsage = diskUsage; } - checkNodeUsage(context, "getDiskStoreUsage", thresholdValue); + Double diskStoreUsage = context.getManagementProxy(). + getAttribute("broker", "DiskStoreUsage", Double.class, 0); + + checkNodeResourceUsage("DiskStoreUsage", (int)(diskStoreUsage * 100), maxDiskUsage); } private void checkNodeMemoryUsage(final CheckContext context) throws Exception { - checkNodeUsage(context, "getAddressMemoryUsagePercentage", memoryUsage); + int addressMemoryUsagePercentage = context.getManagementProxy(). + getAttribute("broker", "AddressMemoryUsagePercentage", Integer.class, 0); + + checkNodeResourceUsage("MemoryUsage", addressMemoryUsagePercentage, memoryUsage); } - private void checkNodeUsage(final CheckContext context, final String name, final int thresholdValue) throws Exception { - int usageValue = context.getManagementProxy().invokeOperation(int.class, "broker", name); - + private void checkNodeResourceUsage(final String resourceName, final int usageValue, final int thresholdValue) throws Exception { if (usageValue > thresholdValue) { - throw new CheckException("The " + (name.startsWith("get") ? name.substring(3) : name) + - " " + usageValue + " is less than " + thresholdValue); + throw new CheckException("The " + resourceName + " " + usageValue + " is less than " + thresholdValue); } } } diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/check/QueueCheck.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/check/QueueCheck.java index 5bd4fd9d76..ed9a1629c0 100644 --- a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/check/QueueCheck.java +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/check/QueueCheck.java @@ -132,7 +132,7 @@ public class QueueCheck extends CheckAbstract { } private void checkQueueUp(final CheckContext context) throws Exception { - if (context.getManagementProxy().invokeOperation(Boolean.class,ResourceNames.QUEUE + getName(), "isPaused")) { + if (context.getManagementProxy().getAttribute(ResourceNames.QUEUE + getName(), "Paused", Boolean.class, 0)) { throw new CheckException("The queue is paused."); } } diff --git a/artemis-commons/src/main/java/org/apache/activemq/artemis/api/core/ActiveMQExceptionType.java b/artemis-commons/src/main/java/org/apache/activemq/artemis/api/core/ActiveMQExceptionType.java index 8bb51c3dcf..2c6e585260 100644 --- a/artemis-commons/src/main/java/org/apache/activemq/artemis/api/core/ActiveMQExceptionType.java +++ b/artemis-commons/src/main/java/org/apache/activemq/artemis/api/core/ActiveMQExceptionType.java @@ -267,6 +267,12 @@ public enum ActiveMQExceptionType { public ActiveMQException createException(String msg) { return new ActiveMQDivertDoesNotExistException(msg); } + }, + REDIRECTED(222) { + @Override + public ActiveMQException createException(String msg) { + return new ActiveMQRedirectedException(msg); + } }; private static final Map TYPE_MAP; diff --git a/artemis-commons/src/main/java/org/apache/activemq/artemis/api/core/ActiveMQRedirectedException.java b/artemis-commons/src/main/java/org/apache/activemq/artemis/api/core/ActiveMQRedirectedException.java new file mode 100644 index 0000000000..ed52b04f23 --- /dev/null +++ b/artemis-commons/src/main/java/org/apache/activemq/artemis/api/core/ActiveMQRedirectedException.java @@ -0,0 +1,34 @@ +/* + * 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.activemq.artemis.api.core; + +/** + * A client was redirected. + */ +public final class ActiveMQRedirectedException extends ActiveMQException { + + private static final long serialVersionUID = 7414966383933311627L; + + public ActiveMQRedirectedException() { + super(ActiveMQExceptionType.REDIRECTED); + } + + public ActiveMQRedirectedException(String message) { + super(ActiveMQExceptionType.REDIRECTED, message); + } +} + diff --git a/artemis-commons/src/main/java/org/apache/activemq/artemis/api/core/DisconnectReason.java b/artemis-commons/src/main/java/org/apache/activemq/artemis/api/core/DisconnectReason.java new file mode 100644 index 0000000000..ff5831e716 --- /dev/null +++ b/artemis-commons/src/main/java/org/apache/activemq/artemis/api/core/DisconnectReason.java @@ -0,0 +1,73 @@ +/* + * 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.activemq.artemis.api.core; + +public enum DisconnectReason { + REDIRECT((byte)0, false), + REDIRECT_ON_CRITICAL_ERROR((byte)1, true), + SCALE_DOWN((byte)2, false), + SCALE_DOWN_ON_CRITICAL_ERROR((byte)3, true), + SHOUT_DOWN((byte)4, false), + SHOUT_DOWN_ON_CRITICAL_ERROR((byte)5, true); + + private final byte type; + private final boolean criticalError; + + DisconnectReason(byte type, boolean criticalError) { + this.type = type; + this.criticalError = criticalError; + } + + public byte getType() { + return type; + } + + public boolean isCriticalError() { + return criticalError; + } + + public boolean isRedirect() { + return this == REDIRECT || this == REDIRECT_ON_CRITICAL_ERROR; + } + + public boolean isScaleDown() { + return this == SCALE_DOWN || this == SCALE_DOWN_ON_CRITICAL_ERROR; + } + + public boolean isShutDown() { + return this == SHOUT_DOWN || this == SHOUT_DOWN_ON_CRITICAL_ERROR; + } + + public static DisconnectReason getType(byte type) { + switch (type) { + case 0: + return REDIRECT; + case 1: + return REDIRECT_ON_CRITICAL_ERROR; + case 2: + return SCALE_DOWN; + case 3: + return SCALE_DOWN_ON_CRITICAL_ERROR; + case 4: + return SHOUT_DOWN; + case 5: + return SHOUT_DOWN_ON_CRITICAL_ERROR; + default: + return null; + } + } +} diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/TransportConfiguration.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/TransportConfiguration.java index ee285a314e..74657ccc1f 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/TransportConfiguration.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/TransportConfiguration.java @@ -255,7 +255,7 @@ public class TransportConfiguration implements Serializable { public String toString() { StringBuilder str = new StringBuilder(TransportConfiguration.class.getSimpleName()); str.append("(name=" + name + ", "); - str.append("factory=" + replaceWildcardChars(factoryClassName)); + str.append("factory=" + (factoryClassName == null ? "null" : replaceWildcardChars(factoryClassName))); str.append(") "); str.append(toStringParameters(params, extraProps)); return str.toString(); diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/client/ClientSession.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/client/ClientSession.java index ad7e702dc8..51df731bc6 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/client/ClientSession.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/client/ClientSession.java @@ -39,6 +39,14 @@ public interface ClientSession extends XAResource, AutoCloseable { */ String JMS_SESSION_IDENTIFIER_PROPERTY = "jms-session"; + /** + * Just like {@link ClientSession.AddressQuery#JMS_SESSION_IDENTIFIER_PROPERTY} this is + * used to identify the ClientID over JMS Session. + * However this is only used when the JMS Session.clientID is set (which is optional). + * With this property management tools and the server can identify the jms-client-id used over JMS + */ + String JMS_SESSION_CLIENT_ID_PROPERTY = "jms-client-id"; + /** * Information returned by a binding query * diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/client/ClientSessionFactory.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/client/ClientSessionFactory.java index 9fc1f489df..e1e18e927e 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/client/ClientSessionFactory.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/client/ClientSessionFactory.java @@ -135,6 +135,32 @@ public interface ClientSessionFactory extends AutoCloseable { boolean preAcknowledge, int ackBatchSize) throws ActiveMQException; + /** + * Creates an authenticated session. + *

+ * It is possible to pre-acknowledge messages on the server so that the client can avoid additional network trip + * to the server to acknowledge messages. While this increase performance, this does not guarantee delivery (as messages + * can be lost after being pre-acknowledged on the server). Use with caution if your application design permits it. + * + * @param username the user name + * @param password the user password + * @param xa whether the session support XA transaction semantic or not + * @param autoCommitSends true to automatically commit message sends, false to commit manually + * @param autoCommitAcks true to automatically commit message acknowledgement, false to commit manually + * @param preAcknowledge true to pre-acknowledge messages on the server, false to let the client acknowledge the messages + * @param clientID the session clientID + * @return a ClientSession + * @throws ActiveMQException if an exception occurs while creating the session + */ + ClientSession createSession(String username, + String password, + boolean xa, + boolean autoCommitSends, + boolean autoCommitAcks, + boolean preAcknowledge, + int ackBatchSize, + String clientID) throws ActiveMQException; + /** * Closes this factory and any session created by it. */ diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/management/ActiveMQManagementProxy.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/management/ActiveMQManagementProxy.java index 599ff738ae..7d77a877f9 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/management/ActiveMQManagementProxy.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/management/ActiveMQManagementProxy.java @@ -18,7 +18,6 @@ package org.apache.activemq.artemis.api.core.management; import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration; -import org.apache.activemq.artemis.api.core.ActiveMQException; import org.apache.activemq.artemis.api.core.client.ActiveMQClient; import org.apache.activemq.artemis.api.core.client.ClientMessage; import org.apache.activemq.artemis.api.core.client.ClientRequestor; @@ -28,51 +27,67 @@ import org.apache.activemq.artemis.api.core.client.ServerLocator; public class ActiveMQManagementProxy implements AutoCloseable { - private final String username; - private final String password; - private final ServerLocator locator; + private final ServerLocator serverLocator; - private ClientSessionFactory sessionFactory; - private ClientSession session; - private ClientRequestor requestor; + private final ClientSessionFactory sessionFactory; - public ActiveMQManagementProxy(final ServerLocator locator, final String username, final String password) { - this.locator = locator; - this.username = username; - this.password = password; + private final ClientSession clientSession; + + + public ActiveMQManagementProxy(final ClientSession session) { + serverLocator = null; + sessionFactory = null; + clientSession = session; } - public void start() throws Exception { + public ActiveMQManagementProxy(final ServerLocator locator, final String username, final String password) throws Exception { + serverLocator = locator; sessionFactory = locator.createSessionFactory(); - session = sessionFactory.createSession(username, password, false, true, true, false, ActiveMQClient.DEFAULT_ACK_BATCH_SIZE); - requestor = new ClientRequestor(session, ActiveMQDefaultConfiguration.getDefaultManagementAddress()); - - session.start(); + clientSession = sessionFactory.createSession(username, password, false, true, true, false, ActiveMQClient.DEFAULT_ACK_BATCH_SIZE).start(); } - public T invokeOperation(final Class type, final String resourceName, final String operationName, final Object... operationArgs) throws Exception { - ClientMessage request = session.createMessage(false); + public T getAttribute(final String resourceName, final String attributeName, final Class attributeClass, final int timeout) throws Exception { + try (ClientRequestor requestor = new ClientRequestor(clientSession, ActiveMQDefaultConfiguration.getDefaultManagementAddress())) { + ClientMessage request = clientSession.createMessage(false); - ManagementHelper.putOperationInvocation(request, resourceName, operationName, operationArgs); + ManagementHelper.putAttribute(request, resourceName, attributeName); - ClientMessage reply = requestor.request(request); + ClientMessage reply = requestor.request(request, timeout); - if (ManagementHelper.hasOperationSucceeded(reply)) { - return (T)ManagementHelper.getResult(reply, type); - } else { - throw new Exception("Failed to invoke " + resourceName + "." + operationName + ". Reason: " + ManagementHelper.getResult(reply, String.class)); + if (ManagementHelper.hasOperationSucceeded(reply)) { + return (T)ManagementHelper.getResult(reply, attributeClass); + } else { + throw new Exception("Failed to get " + resourceName + "." + attributeName + ". Reason: " + ManagementHelper.getResult(reply, String.class)); + } } } + public T invokeOperation(final String resourceName, final String operationName, final Object[] operationParams, final Class operationClass, final int timeout) throws Exception { + try (ClientRequestor requestor = new ClientRequestor(clientSession, ActiveMQDefaultConfiguration.getDefaultManagementAddress())) { + ClientMessage request = clientSession.createMessage(false); - public void stop() throws ActiveMQException { - session.stop(); + ManagementHelper.putOperationInvocation(request, resourceName, operationName, operationParams); + + ClientMessage reply = requestor.request(request, timeout); + + if (ManagementHelper.hasOperationSucceeded(reply)) { + return (T)ManagementHelper.getResult(reply, operationClass); + } else { + throw new Exception("Failed to invoke " + resourceName + "." + operationName + ". Reason: " + ManagementHelper.getResult(reply, String.class)); + } + } } @Override public void close() throws Exception { - requestor.close(); - session.close(); - sessionFactory.close(); + clientSession.close(); + + if (sessionFactory != null) { + sessionFactory.close(); + } + + if (serverLocator != null) { + serverLocator.close(); + } } } diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/management/BrokerBalancerControl.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/management/BrokerBalancerControl.java new file mode 100644 index 0000000000..983888f0be --- /dev/null +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/management/BrokerBalancerControl.java @@ -0,0 +1,31 @@ +/* + * 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.activemq.artemis.api.core.management; + +import javax.management.MBeanOperationInfo; +import javax.management.openmbean.CompositeData; + +/** + * A BrokerBalancerControl is used to manage a BrokerBalancer. + */ +public interface BrokerBalancerControl { + @Operation(desc = "Get the target associated with key", impact = MBeanOperationInfo.INFO) + CompositeData getTarget(@Parameter(desc = "a key", name = "key") String key) throws Exception; + + @Operation(desc = "Get the target associated with key as JSON", impact = MBeanOperationInfo.INFO) + String getTargetAsJSON(@Parameter(desc = "a key", name = "key") String key); +} diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/management/ObjectNameBuilder.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/management/ObjectNameBuilder.java index dacc91f554..a97ff7cefe 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/management/ObjectNameBuilder.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/management/ObjectNameBuilder.java @@ -149,6 +149,15 @@ public final class ObjectNameBuilder { return createObjectName("cluster-connection", name); } + /** + * Returns the ObjectName used by BrokerBalancerControl. + * + * @see BrokerBalancerControl + */ + public ObjectName getBrokerBalancerObjectName(final String name) throws Exception { + return createObjectName("broker-balancer", name); + } + private ObjectName createObjectName(final String type, final String name) throws Exception { return ObjectName.getInstance(String.format("%s,component=%ss,name=%s", getActiveMQServerName(), type, ObjectName.quote(name))); } diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/management/ResourceNames.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/management/ResourceNames.java index 7aaa54d517..0d45dd9ea5 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/management/ResourceNames.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/management/ResourceNames.java @@ -44,6 +44,8 @@ public final class ResourceNames { public static final String BROADCAST_GROUP = "broadcastgroup."; + public static final String BROKER_BALANCER = "brokerbalancer."; + public static final String RETROACTIVE_SUFFIX = "retro"; public static SimpleString getRetroactiveResourceQueueName(String prefix, String delimiter, SimpleString address, RoutingType routingType) { diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/client/ActiveMQClientMessageBundle.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/client/ActiveMQClientMessageBundle.java index 8468e84bb4..3bbf51be71 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/client/ActiveMQClientMessageBundle.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/client/ActiveMQClientMessageBundle.java @@ -27,6 +27,7 @@ import org.apache.activemq.artemis.api.core.ActiveMQLargeMessageException; import org.apache.activemq.artemis.api.core.ActiveMQLargeMessageInterruptedException; import org.apache.activemq.artemis.api.core.ActiveMQNotConnectedException; import org.apache.activemq.artemis.api.core.ActiveMQObjectClosedException; +import org.apache.activemq.artemis.api.core.ActiveMQRedirectedException; import org.apache.activemq.artemis.api.core.ActiveMQTransactionOutcomeUnknownException; import org.apache.activemq.artemis.api.core.ActiveMQTransactionRolledBackException; import org.apache.activemq.artemis.api.core.ActiveMQUnBlockedException; @@ -237,4 +238,7 @@ public interface ActiveMQClientMessageBundle { @Message(id = 219065, value = "Failed to handle packet.") RuntimeException failedToHandlePacket(@Cause Exception e); + + @Message(id = 219066, value = "The connection was redirected") + ActiveMQRedirectedException redirected(); } diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/client/impl/ClientSessionFactoryImpl.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/client/impl/ClientSessionFactoryImpl.java index 92236f4aa4..80a2428554 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/client/impl/ClientSessionFactoryImpl.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/client/impl/ClientSessionFactoryImpl.java @@ -37,6 +37,7 @@ import org.apache.activemq.artemis.api.core.ActiveMQBuffer; import org.apache.activemq.artemis.api.core.ActiveMQException; import org.apache.activemq.artemis.api.core.ActiveMQInterruptedException; import org.apache.activemq.artemis.api.core.ActiveMQNotConnectedException; +import org.apache.activemq.artemis.api.core.DisconnectReason; import org.apache.activemq.artemis.api.core.Interceptor; import org.apache.activemq.artemis.api.core.Pair; import org.apache.activemq.artemis.api.core.TransportConfiguration; @@ -79,11 +80,13 @@ public class ClientSessionFactoryImpl implements ClientSessionFactoryInternal, C private final ClientProtocolManager clientProtocolManager; - private TransportConfiguration connectorConfig; + private final TransportConfiguration connectorConfig; - private TransportConfiguration currentConnectorConfig; + private TransportConfiguration previousConnectorConfig; - private volatile TransportConfiguration backupConfig; + private volatile TransportConfiguration currentConnectorConfig; + + private volatile TransportConfiguration backupConnectorConfig; private ConnectorFactory connectorFactory; @@ -184,6 +187,8 @@ public class ClientSessionFactoryImpl implements ClientSessionFactoryInternal, C this.clientProtocolManager.setSessionFactory(this); + this.connectorConfig = connectorConfig.getA(); + this.currentConnectorConfig = connectorConfig.getA(); connectorFactory = instantiateConnectorFactory(connectorConfig.getA().getFactoryClassName()); @@ -231,7 +236,7 @@ public class ClientSessionFactoryImpl implements ClientSessionFactoryInternal, C connectionReadyForWrites = true; if (connectorConfig.getB() != null) { - this.backupConfig = connectorConfig.getB(); + this.backupConnectorConfig = connectorConfig.getB(); } } @@ -253,8 +258,8 @@ public class ClientSessionFactoryImpl implements ClientSessionFactoryInternal, C if (connection == null) { StringBuilder msg = new StringBuilder("Unable to connect to server using configuration ").append(currentConnectorConfig); - if (backupConfig != null) { - msg.append(" and backup configuration ").append(backupConfig); + if (backupConnectorConfig != null) { + msg.append(" and backup configuration ").append(backupConnectorConfig); } throw new ActiveMQNotConnectedException(msg.toString()); } @@ -288,7 +293,7 @@ public class ClientSessionFactoryImpl implements ClientSessionFactoryInternal, C if (logger.isDebugEnabled()) { logger.debug("Setting up backup config = " + backUp + " for live = " + live); } - backupConfig = backUp; + backupConnectorConfig = backUp; } else { if (logger.isDebugEnabled()) { logger.debug("ClientSessionFactoryImpl received backup update for live/backup pair = " + live + @@ -302,7 +307,19 @@ public class ClientSessionFactoryImpl implements ClientSessionFactoryInternal, C @Override public Object getBackupConnector() { - return backupConfig; + return backupConnectorConfig; + } + + @Override + public ClientSession createSession(final String username, + final String password, + final boolean xa, + final boolean autoCommitSends, + final boolean autoCommitAcks, + final boolean preAcknowledge, + final int ackBatchSize, + final String clientID) throws ActiveMQException { + return createSessionInternal(username, password, xa, autoCommitSends, autoCommitAcks, preAcknowledge, ackBatchSize, clientID); } @Override @@ -313,42 +330,42 @@ public class ClientSessionFactoryImpl implements ClientSessionFactoryInternal, C final boolean autoCommitAcks, final boolean preAcknowledge, final int ackBatchSize) throws ActiveMQException { - return createSessionInternal(username, password, xa, autoCommitSends, autoCommitAcks, preAcknowledge, ackBatchSize); + return createSessionInternal(username, password, xa, autoCommitSends, autoCommitAcks, preAcknowledge, ackBatchSize, null); } @Override public ClientSession createSession(final boolean autoCommitSends, final boolean autoCommitAcks, final int ackBatchSize) throws ActiveMQException { - return createSessionInternal(null, null, false, autoCommitSends, autoCommitAcks, serverLocator.isPreAcknowledge(), ackBatchSize); + return createSessionInternal(null, null, false, autoCommitSends, autoCommitAcks, serverLocator.isPreAcknowledge(), ackBatchSize, null); } @Override public ClientSession createXASession() throws ActiveMQException { - return createSessionInternal(null, null, true, false, false, serverLocator.isPreAcknowledge(), serverLocator.getAckBatchSize()); + return createSessionInternal(null, null, true, false, false, serverLocator.isPreAcknowledge(), serverLocator.getAckBatchSize(), null); } @Override public ClientSession createTransactedSession() throws ActiveMQException { - return createSessionInternal(null, null, false, false, false, serverLocator.isPreAcknowledge(), serverLocator.getAckBatchSize()); + return createSessionInternal(null, null, false, false, false, serverLocator.isPreAcknowledge(), serverLocator.getAckBatchSize(), null); } @Override public ClientSession createSession() throws ActiveMQException { - return createSessionInternal(null, null, false, true, true, serverLocator.isPreAcknowledge(), serverLocator.getAckBatchSize()); + return createSessionInternal(null, null, false, true, true, serverLocator.isPreAcknowledge(), serverLocator.getAckBatchSize(), null); } @Override public ClientSession createSession(final boolean autoCommitSends, final boolean autoCommitAcks) throws ActiveMQException { - return createSessionInternal(null, null, false, autoCommitSends, autoCommitAcks, serverLocator.isPreAcknowledge(), serverLocator.getAckBatchSize()); + return createSessionInternal(null, null, false, autoCommitSends, autoCommitAcks, serverLocator.isPreAcknowledge(), serverLocator.getAckBatchSize(), null); } @Override public ClientSession createSession(final boolean xa, final boolean autoCommitSends, final boolean autoCommitAcks) throws ActiveMQException { - return createSessionInternal(null, null, xa, autoCommitSends, autoCommitAcks, serverLocator.isPreAcknowledge(), serverLocator.getAckBatchSize()); + return createSessionInternal(null, null, xa, autoCommitSends, autoCommitAcks, serverLocator.isPreAcknowledge(), serverLocator.getAckBatchSize(), null); } @Override @@ -356,7 +373,7 @@ public class ClientSessionFactoryImpl implements ClientSessionFactoryInternal, C final boolean autoCommitSends, final boolean autoCommitAcks, final boolean preAcknowledge) throws ActiveMQException { - return createSessionInternal(null, null, xa, autoCommitSends, autoCommitAcks, preAcknowledge, serverLocator.getAckBatchSize()); + return createSessionInternal(null, null, xa, autoCommitSends, autoCommitAcks, preAcknowledge, serverLocator.getAckBatchSize(), null); } // ClientConnectionLifeCycleListener implementation -------------------------------------------------- @@ -717,10 +734,11 @@ public class ClientSessionFactoryImpl implements ClientSessionFactoryInternal, C final boolean autoCommitSends, final boolean autoCommitAcks, final boolean preAcknowledge, - final int ackBatchSize) throws ActiveMQException { + final int ackBatchSize, + final String clientID) throws ActiveMQException { String name = UUIDGenerator.getInstance().generateStringUUID(); - SessionContext context = createSessionChannel(name, username, password, xa, autoCommitSends, autoCommitAcks, preAcknowledge); + SessionContext context = createSessionChannel(name, username, password, xa, autoCommitSends, autoCommitAcks, preAcknowledge, clientID); ClientSessionInternal session = new ClientSessionImpl(this, name, username, password, xa, autoCommitSends, autoCommitAcks, preAcknowledge, serverLocator.isBlockOnAcknowledge(), serverLocator.isAutoGroup(), ackBatchSize, serverLocator.getConsumerWindowSize(), serverLocator.getConsumerMaxRate(), serverLocator.getConfirmationWindowSize(), serverLocator.getProducerWindowSize(), serverLocator.getProducerMaxRate(), serverLocator.isBlockOnNonDurableSend(), serverLocator.isBlockOnDurableSend(), serverLocator.isCacheLargeMessagesClient(), serverLocator.getMinLargeMessageSize(), serverLocator.isCompressLargeMessage(), serverLocator.getInitialMessagePacketSize(), serverLocator.getGroupID(), context, orderedExecutorFactory.getExecutor(), orderedExecutorFactory.getExecutor(), orderedExecutorFactory.getExecutor()); @@ -1050,11 +1068,13 @@ public class ClientSessionFactoryImpl implements ClientSessionFactoryInternal, C public class CloseRunnable implements Runnable { private final RemotingConnection conn; - private final String scaleDownTargetNodeID; + private final DisconnectReason reason; + private final String targetNodeID; - public CloseRunnable(RemotingConnection conn, String scaleDownTargetNodeID) { + public CloseRunnable(RemotingConnection conn, DisconnectReason reason, String targetNodeID) { this.conn = conn; - this.scaleDownTargetNodeID = scaleDownTargetNodeID; + this.reason = reason; + this.targetNodeID = targetNodeID; } // Must be executed on new thread since cannot block the Netty thread for a long time and fail @@ -1063,10 +1083,12 @@ public class ClientSessionFactoryImpl implements ClientSessionFactoryInternal, C public void run() { try { CLOSE_RUNNABLES.add(this); - if (scaleDownTargetNodeID == null) { - conn.fail(ActiveMQClientMessageBundle.BUNDLE.disconnected()); + if (reason.isRedirect()) { + conn.fail(ActiveMQClientMessageBundle.BUNDLE.redirected()); + } else if (reason.isScaleDown()) { + conn.fail(ActiveMQClientMessageBundle.BUNDLE.disconnected(), targetNodeID); } else { - conn.fail(ActiveMQClientMessageBundle.BUNDLE.disconnected(), scaleDownTargetNodeID); + conn.fail(ActiveMQClientMessageBundle.BUNDLE.disconnected()); } } finally { CLOSE_RUNNABLES.remove(this); @@ -1146,72 +1168,39 @@ public class ClientSessionFactoryImpl implements ClientSessionFactoryInternal, C Connection transportConnection = null; try { - if (logger.isDebugEnabled()) { - logger.debug("Trying to connect with connectorFactory = " + connectorFactory + - ", connectorConfig=" + currentConnectorConfig); + //Try to connect with the current connector configuration + transportConnection = createTransportConnection("current", currentConnectorConfig); + if (transportConnection != null) { + return transportConnection; } - Connector liveConnector = createConnector(connectorFactory, currentConnectorConfig); - - if ((transportConnection = openTransportConnection(liveConnector)) != null) { - // if we can't connect the connect method will return null, hence we have to try the backup - connector = liveConnector; - return transportConnection; - } else if (backupConfig != null) { - if (logger.isDebugEnabled()) { - logger.debug("Trying backup config = " + backupConfig); - } - - ConnectorFactory backupConnectorFactory = instantiateConnectorFactory(backupConfig.getFactoryClassName()); - - Connector backupConnector = createConnector(backupConnectorFactory, backupConfig); - - transportConnection = openTransportConnection(backupConnector); + if (backupConnectorConfig != null) { + //Try to connect with the backup connector configuration + transportConnection = createTransportConnection("backup", backupConnectorConfig); if (transportConnection != null) { - /*looks like the backup is now live, let's use that*/ - - if (logger.isDebugEnabled()) { - logger.debug("Connected to the backup at " + backupConfig); - } - - // Switching backup as live - connector = backupConnector; - connectorConfig = currentConnectorConfig; - currentConnectorConfig = backupConfig; - connectorFactory = backupConnectorFactory; return transportConnection; } } - if (logger.isDebugEnabled()) { - logger.debug("Backup is not active, trying original connection configuration now."); + if (previousConnectorConfig != null && !currentConnectorConfig.equals(previousConnectorConfig)) { + //Try to connect with the previous connector configuration + transportConnection = createTransportConnection("previous", previousConnectorConfig); + if (transportConnection != null) { + return transportConnection; + } } - - if (currentConnectorConfig.equals(connectorConfig) || connectorConfig == null) { - - // There was no changes on current and original connectors, just return null here and let the retry happen at the first portion of this method on the next retry - return null; + if (!currentConnectorConfig.equals(connectorConfig)) { + //Try to connect with the initial connector configuration + transportConnection = createTransportConnection("initial", connectorConfig); + if (transportConnection != null) { + return transportConnection; + } } - ConnectorFactory lastConnectorFactory = instantiateConnectorFactory(connectorConfig.getFactoryClassName()); - - Connector lastConnector = createConnector(lastConnectorFactory, connectorConfig); - - transportConnection = openTransportConnection(lastConnector); - - if (transportConnection != null) { - logger.debug("Returning into original connector"); - connector = lastConnector; - TransportConfiguration temp = currentConnectorConfig; - currentConnectorConfig = connectorConfig; - connectorConfig = temp; - return transportConnection; - } else { - logger.debug("no connection been made, returning null"); - return null; - } + logger.debug("no connection been made, returning null"); + return null; } catch (Exception cause) { // Sanity catch for badly behaved remoting plugins @@ -1236,6 +1225,33 @@ public class ClientSessionFactoryImpl implements ClientSessionFactoryInternal, C } + private Connection createTransportConnection(String name, TransportConfiguration transportConnectorConfig) { + ConnectorFactory transportConnectorFactory = instantiateConnectorFactory( + transportConnectorConfig.getFactoryClassName()); + + if (logger.isDebugEnabled()) { + logger.debug("Trying to connect with connectorFactory=" + transportConnectorFactory + + " and " + name + "ConnectorConfig: " + transportConnectorConfig); + } + + Connector transportConnector = createConnector(transportConnectorFactory, transportConnectorConfig); + + Connection transportConnection = openTransportConnection(transportConnector); + + if (transportConnection != null) { + if (logger.isDebugEnabled()) { + logger.debug("Connected with the " + name + "ConnectorConfig=" + transportConnectorConfig); + } + + connector = transportConnector; + connectorFactory = transportConnectorFactory; + previousConnectorConfig = currentConnectorConfig; + currentConnectorConfig = transportConnectorConfig; + } + + return transportConnection; + } + private class DelegatingBufferHandler implements BufferHandler { @Override @@ -1413,9 +1429,10 @@ public class ClientSessionFactoryImpl implements ClientSessionFactoryInternal, C final boolean xa, final boolean autoCommitSends, final boolean autoCommitAcks, - final boolean preAcknowledge) throws ActiveMQException { + final boolean preAcknowledge, + final String clientID) throws ActiveMQException { synchronized (createSessionLock) { - return clientProtocolManager.createSessionContext(name, username, password, xa, autoCommitSends, autoCommitAcks, preAcknowledge, serverLocator.getMinLargeMessageSize(), serverLocator.getConfirmationWindowSize()); + return clientProtocolManager.createSessionContext(name, username, password, xa, autoCommitSends, autoCommitAcks, preAcknowledge, serverLocator.getMinLargeMessageSize(), serverLocator.getConfirmationWindowSize(), clientID); } } @@ -1427,20 +1444,39 @@ public class ClientSessionFactoryImpl implements ClientSessionFactoryInternal, C class SessionFactoryTopologyHandler implements TopologyResponseHandler { @Override - public void nodeDisconnected(RemotingConnection conn, String nodeID, String scaleDownTargetNodeID) { + public void nodeDisconnected(RemotingConnection conn, String nodeID, DisconnectReason reason, String targetNodeID, TransportConfiguration tagetConnector) { if (logger.isTraceEnabled()) { logger.trace("Disconnect being called on client:" + - " server locator = " + - serverLocator + - " notifying node " + - nodeID + - " as down", new Exception("trace")); + " server locator = " + + serverLocator + + " notifying node " + + nodeID + + " as down with reason " + + reason, new Exception("trace")); } serverLocator.notifyNodeDown(System.currentTimeMillis(), nodeID); - closeExecutor.execute(new CloseRunnable(conn, scaleDownTargetNodeID)); + if (reason.isRedirect()) { + if (serverLocator.isHA()) { + TopologyMemberImpl topologyMember = serverLocator.getTopology().getMember(nodeID); + + if (topologyMember != null) { + if (topologyMember.getConnector().getB() != null) { + backupConnectorConfig = topologyMember.getConnector().getB(); + } else if (logger.isDebugEnabled()) { + logger.debug("The topology member " + nodeID + " with connector " + tagetConnector + " has no backup"); + } + } else if (logger.isDebugEnabled()) { + logger.debug("The topology member " + nodeID + " with connector " + tagetConnector + " not found"); + } + } + + currentConnectorConfig = tagetConnector; + } + + closeExecutor.execute(new CloseRunnable(conn, reason, targetNodeID)); } diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/client/impl/ClientSessionImpl.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/client/impl/ClientSessionImpl.java index 33e5a77f6c..5d831daf79 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/client/impl/ClientSessionImpl.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/client/impl/ClientSessionImpl.java @@ -33,6 +33,7 @@ import org.apache.activemq.artemis.api.core.ActiveMQBuffer; import org.apache.activemq.artemis.api.core.ActiveMQBuffers; import org.apache.activemq.artemis.api.core.ActiveMQException; import org.apache.activemq.artemis.api.core.ActiveMQExceptionType; +import org.apache.activemq.artemis.api.core.ActiveMQRedirectedException; import org.apache.activemq.artemis.api.core.Message; import org.apache.activemq.artemis.api.core.QueueAttributes; import org.apache.activemq.artemis.api.core.QueueConfiguration; @@ -1460,6 +1461,9 @@ public final class ClientSessionImpl implements ClientSessionInternal, FailureLi sessionContext.returnBlocking(cause); } + } catch (ActiveMQRedirectedException e) { + logger.info("failedToHandleFailover.ActiveMQRedirectedException"); + suc = false; } catch (Throwable t) { ActiveMQClientLogger.LOGGER.failedToHandleFailover(t); suc = false; diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/CoreRemotingConnection.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/CoreRemotingConnection.java index 76f87cf106..61a01dc9fb 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/CoreRemotingConnection.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/CoreRemotingConnection.java @@ -51,6 +51,16 @@ public interface CoreRemotingConnection extends RemotingConnection { return version >= PacketImpl.ARTEMIS_2_7_0_VERSION; } + default boolean isVersionSupportClientID() { + int version = getChannelVersion(); + return version >= PacketImpl.ARTEMIS_2_18_0_VERSION; + } + + default boolean isVersionSupportRedirect() { + int version = getChannelVersion(); + return version >= PacketImpl.ARTEMIS_2_18_0_VERSION; + } + /** * Sets the client protocol used on the communication. This will determine if the client has * support for certain packet types diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/ActiveMQClientProtocolManager.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/ActiveMQClientProtocolManager.java index b1d6cc8d59..84ae30b196 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/ActiveMQClientProtocolManager.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/ActiveMQClientProtocolManager.java @@ -27,6 +27,7 @@ import org.apache.activemq.artemis.api.core.ActiveMQBuffer; import org.apache.activemq.artemis.api.core.ActiveMQException; import org.apache.activemq.artemis.api.core.ActiveMQExceptionType; import org.apache.activemq.artemis.api.core.ActiveMQInterruptedException; +import org.apache.activemq.artemis.api.core.DisconnectReason; import org.apache.activemq.artemis.api.core.Interceptor; import org.apache.activemq.artemis.api.core.Pair; import org.apache.activemq.artemis.api.core.SimpleString; @@ -45,10 +46,13 @@ import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.CheckFailo import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.ClusterTopologyChangeMessage; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.ClusterTopologyChangeMessage_V2; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.ClusterTopologyChangeMessage_V3; +import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.ClusterTopologyChangeMessage_V4; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.CreateSessionMessage; +import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.CreateSessionMessage_V2; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.CreateSessionResponseMessage; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.DisconnectMessage; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.DisconnectMessage_V2; +import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.DisconnectMessage_V3; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.Ping; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.SubscribeClusterTopologyUpdatesMessageV2; import org.apache.activemq.artemis.core.remoting.impl.netty.ActiveMQFrameDecoder2; @@ -243,10 +247,11 @@ public class ActiveMQClientProtocolManager implements ClientProtocolManager { boolean autoCommitAcks, boolean preAcknowledge, int minLargeMessageSize, - int confirmationWindowSize) throws ActiveMQException { + int confirmationWindowSize, + String clientID) throws ActiveMQException { for (Version clientVersion : VersionLoader.getClientVersions()) { try { - return createSessionContext(clientVersion, name, username, password, xa, autoCommitSends, autoCommitAcks, preAcknowledge, minLargeMessageSize, confirmationWindowSize); + return createSessionContext(clientVersion, name, username, password, xa, autoCommitSends, autoCommitAcks, preAcknowledge, minLargeMessageSize, confirmationWindowSize, clientID); } catch (ActiveMQException e) { if (e.getType() != ActiveMQExceptionType.INCOMPATIBLE_CLIENT_SERVER_VERSIONS) { throw e; @@ -266,7 +271,8 @@ public class ActiveMQClientProtocolManager implements ClientProtocolManager { boolean autoCommitAcks, boolean preAcknowledge, int minLargeMessageSize, - int confirmationWindowSize) throws ActiveMQException { + int confirmationWindowSize, + String clientID) throws ActiveMQException { if (!isAlive()) throw ActiveMQClientMessageBundle.BUNDLE.clientSessionClosed(); @@ -293,7 +299,7 @@ public class ActiveMQClientProtocolManager implements ClientProtocolManager { long sessionChannelID = connection.generateChannelID(); - Packet request = newCreateSessionPacket(clientVersion, name, username, password, xa, autoCommitSends, autoCommitAcks, preAcknowledge, minLargeMessageSize, confirmationWindowSize, sessionChannelID); + Packet request = newCreateSessionPacket(clientVersion.getIncrementingVersion(), name, username, password, xa, autoCommitSends, autoCommitAcks, preAcknowledge, minLargeMessageSize, confirmationWindowSize, sessionChannelID, clientID); try { // channel1 reference here has to go away @@ -302,7 +308,8 @@ public class ActiveMQClientProtocolManager implements ClientProtocolManager { if (!isAlive()) throw cause; - if (cause.getType() == ActiveMQExceptionType.UNBLOCKED) { + if (cause.getType() == ActiveMQExceptionType.UNBLOCKED || + cause.getType() == ActiveMQExceptionType.REDIRECTED) { // This means the thread was blocked on create session and failover unblocked it // so failover could occur @@ -339,11 +346,11 @@ public class ActiveMQClientProtocolManager implements ClientProtocolManager { } while (retry); sessionChannel.getConnection().setChannelVersion(response.getServerVersion()); - return newSessionContext(name, confirmationWindowSize, sessionChannel, response); + return newSessionContext(name, confirmationWindowSize, sessionChannel, response); } - protected Packet newCreateSessionPacket(Version clientVersion, + protected Packet newCreateSessionPacket(int clientVersion, String name, String username, String password, @@ -353,8 +360,13 @@ public class ActiveMQClientProtocolManager implements ClientProtocolManager { boolean preAcknowledge, int minLargeMessageSize, int confirmationWindowSize, - long sessionChannelID) { - return new CreateSessionMessage(name, sessionChannelID, clientVersion.getIncrementingVersion(), username, password, minLargeMessageSize, xa, autoCommitSends, autoCommitAcks, preAcknowledge, confirmationWindowSize, null); + long sessionChannelID, + String clientID) { + if (connection.isVersionSupportClientID()) { + return new CreateSessionMessage_V2(name, sessionChannelID, clientVersion, username, password, minLargeMessageSize, xa, autoCommitSends, autoCommitAcks, preAcknowledge, confirmationWindowSize, null, clientID); + } else { + return new CreateSessionMessage(name, sessionChannelID, clientVersion, username, password, minLargeMessageSize, xa, autoCommitSends, autoCommitAcks, preAcknowledge, confirmationWindowSize, null); + } } protected SessionContext newSessionContext(String name, @@ -459,19 +471,15 @@ public class ActiveMQClientProtocolManager implements ClientProtocolManager { public void handlePacket(final Packet packet) { final byte type = packet.getType(); - if (type == PacketImpl.DISCONNECT || type == PacketImpl.DISCONNECT_V2) { - final DisconnectMessage msg = (DisconnectMessage) packet; - String scaleDownTargetNodeID = null; - - SimpleString nodeID = msg.getNodeID(); - - if (packet instanceof DisconnectMessage_V2) { - final DisconnectMessage_V2 msg_v2 = (DisconnectMessage_V2) packet; - scaleDownTargetNodeID = msg_v2.getScaleDownNodeID() == null ? null : msg_v2.getScaleDownNodeID().toString(); - } - - if (topologyResponseHandler != null) - topologyResponseHandler.nodeDisconnected(conn, nodeID == null ? null : nodeID.toString(), scaleDownTargetNodeID); + if (type == PacketImpl.DISCONNECT) { + final DisconnectMessage disMessage = (DisconnectMessage) packet; + handleDisconnect(disMessage.getNodeID(), null, null, null); + } else if (type == PacketImpl.DISCONNECT_V2) { + final DisconnectMessage_V2 disMessage = (DisconnectMessage_V2) packet; + handleDisconnect(disMessage.getNodeID(), DisconnectReason.SCALE_DOWN, disMessage.getScaleDownNodeID(), null); + } else if (type == PacketImpl.DISCONNECT_V3) { + final DisconnectMessage_V3 disMessage = (DisconnectMessage_V3) packet; + handleDisconnect(disMessage.getNodeID(), disMessage.getReason(), disMessage.getTargetNodeID(), disMessage.getTargetConnector()); } else if (type == PacketImpl.CLUSTER_TOPOLOGY) { ClusterTopologyChangeMessage topMessage = (ClusterTopologyChangeMessage) packet; notifyTopologyChange(updateTransportConfiguration(topMessage)); @@ -481,11 +489,22 @@ public class ActiveMQClientProtocolManager implements ClientProtocolManager { } else if (type == PacketImpl.CLUSTER_TOPOLOGY_V3) { ClusterTopologyChangeMessage_V3 topMessage = (ClusterTopologyChangeMessage_V3) packet; notifyTopologyChange(updateTransportConfiguration(topMessage)); + } else if (type == PacketImpl.CLUSTER_TOPOLOGY_V4) { + ClusterTopologyChangeMessage_V4 topMessage = (ClusterTopologyChangeMessage_V4) packet; + notifyTopologyChange(updateTransportConfiguration(topMessage)); + connection.setChannelVersion(topMessage.getServerVersion()); } else if (type == PacketImpl.CHECK_FOR_FAILOVER_REPLY) { System.out.println("Channel0Handler.handlePacket"); } } + private void handleDisconnect(SimpleString nodeID, DisconnectReason reason, SimpleString targetNodeID, TransportConfiguration tagetConnector) { + if (topologyResponseHandler != null) { + topologyResponseHandler.nodeDisconnected(conn, nodeID == null ? null : nodeID.toString(), reason, + targetNodeID == null ? null : targetNodeID.toString(), tagetConnector); + } + } + /** * @param topMessage */ diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/ChannelImpl.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/ChannelImpl.java index 18bb08cc55..d133826d04 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/ChannelImpl.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/ChannelImpl.java @@ -184,6 +184,10 @@ public final class ChannelImpl implements Channel { return version >= 129; case PacketImpl.SESS_BINDINGQUERY_RESP_V4: return version >= 129; + case PacketImpl.CLUSTER_TOPOLOGY_V4: + case PacketImpl.CREATESESSION_V2: + case PacketImpl.DISCONNECT_V3: + return version >= PacketImpl.ARTEMIS_2_18_0_VERSION; default: return true; } diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/PacketDecoder.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/PacketDecoder.java index 9cc62b25a6..640e079cd1 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/PacketDecoder.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/PacketDecoder.java @@ -29,10 +29,12 @@ import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.CheckFailo import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.ClusterTopologyChangeMessage; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.ClusterTopologyChangeMessage_V2; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.ClusterTopologyChangeMessage_V3; +import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.ClusterTopologyChangeMessage_V4; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.CreateAddressMessage; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.CreateQueueMessage; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.CreateQueueMessage_V2; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.CreateSessionMessage; +import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.CreateSessionMessage_V2; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.CreateSessionResponseMessage; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.CreateSharedQueueMessage; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.CreateSharedQueueMessage_V2; @@ -40,6 +42,7 @@ import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.Disconnect import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.DisconnectConsumerWithKillMessage; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.DisconnectMessage; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.DisconnectMessage_V2; +import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.DisconnectMessage_V3; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.FederationDownstreamConnectMessage; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.NullResponseMessage; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.NullResponseMessage_V2; @@ -98,8 +101,10 @@ import static org.apache.activemq.artemis.core.protocol.core.impl.PacketImpl.CHE import static org.apache.activemq.artemis.core.protocol.core.impl.PacketImpl.CLUSTER_TOPOLOGY; import static org.apache.activemq.artemis.core.protocol.core.impl.PacketImpl.CLUSTER_TOPOLOGY_V2; import static org.apache.activemq.artemis.core.protocol.core.impl.PacketImpl.CLUSTER_TOPOLOGY_V3; +import static org.apache.activemq.artemis.core.protocol.core.impl.PacketImpl.CLUSTER_TOPOLOGY_V4; import static org.apache.activemq.artemis.core.protocol.core.impl.PacketImpl.CREATESESSION; import static org.apache.activemq.artemis.core.protocol.core.impl.PacketImpl.CREATESESSION_RESP; +import static org.apache.activemq.artemis.core.protocol.core.impl.PacketImpl.CREATESESSION_V2; import static org.apache.activemq.artemis.core.protocol.core.impl.PacketImpl.CREATE_ADDRESS; import static org.apache.activemq.artemis.core.protocol.core.impl.PacketImpl.CREATE_QUEUE; import static org.apache.activemq.artemis.core.protocol.core.impl.PacketImpl.CREATE_QUEUE_V2; @@ -109,6 +114,7 @@ import static org.apache.activemq.artemis.core.protocol.core.impl.PacketImpl.DEL import static org.apache.activemq.artemis.core.protocol.core.impl.PacketImpl.DISCONNECT; import static org.apache.activemq.artemis.core.protocol.core.impl.PacketImpl.DISCONNECT_CONSUMER; import static org.apache.activemq.artemis.core.protocol.core.impl.PacketImpl.DISCONNECT_V2; +import static org.apache.activemq.artemis.core.protocol.core.impl.PacketImpl.DISCONNECT_V3; import static org.apache.activemq.artemis.core.protocol.core.impl.PacketImpl.EXCEPTION; import static org.apache.activemq.artemis.core.protocol.core.impl.PacketImpl.FEDERATION_DOWNSTREAM_CONNECT; import static org.apache.activemq.artemis.core.protocol.core.impl.PacketImpl.NULL_RESPONSE; @@ -477,6 +483,18 @@ public abstract class PacketDecoder implements Serializable { packet = new FederationDownstreamConnectMessage(); break; } + case CLUSTER_TOPOLOGY_V4: { + packet = new ClusterTopologyChangeMessage_V4(); + break; + } + case CREATESESSION_V2: { + packet = new CreateSessionMessage_V2(); + break; + } + case DISCONNECT_V3: { + packet = new DisconnectMessage_V3(); + break; + } default: { throw ActiveMQClientMessageBundle.BUNDLE.invalidType(packetType); } diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/PacketImpl.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/PacketImpl.java index e4a759b774..85275bf609 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/PacketImpl.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/PacketImpl.java @@ -38,6 +38,8 @@ public class PacketImpl implements Packet { public static final int CONSUMER_PRIORITY_CHANGE_VERSION = ARTEMIS_2_7_0_VERSION; public static final int FQQN_CHANGE_VERSION = ARTEMIS_2_7_0_VERSION; + // 2.18.0 + public static final int ARTEMIS_2_18_0_VERSION = 131; public static final SimpleString OLD_QUEUE_PREFIX = new SimpleString("jms.queue."); public static final SimpleString OLD_TEMP_QUEUE_PREFIX = new SimpleString("jms.tempqueue."); @@ -279,6 +281,11 @@ public class PacketImpl implements Packet { public static final byte FEDERATION_DOWNSTREAM_CONNECT = -16; + public static final byte CLUSTER_TOPOLOGY_V4 = -17; + + public static final byte CREATESESSION_V2 = -18; + + public static final byte DISCONNECT_V3 = -19; // Static -------------------------------------------------------- diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/RemotingConnectionImpl.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/RemotingConnectionImpl.java index 8f4e1b7c6a..c62e0a2a77 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/RemotingConnectionImpl.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/RemotingConnectionImpl.java @@ -26,9 +26,12 @@ import java.util.concurrent.TimeUnit; import org.apache.activemq.artemis.api.core.ActiveMQBuffer; import org.apache.activemq.artemis.api.core.ActiveMQException; +import org.apache.activemq.artemis.api.core.ActiveMQRedirectedException; import org.apache.activemq.artemis.api.core.ActiveMQRemoteDisconnectException; +import org.apache.activemq.artemis.api.core.DisconnectReason; import org.apache.activemq.artemis.api.core.Interceptor; import org.apache.activemq.artemis.api.core.SimpleString; +import org.apache.activemq.artemis.api.core.TransportConfiguration; import org.apache.activemq.artemis.api.core.client.ActiveMQClient; import org.apache.activemq.artemis.core.client.ActiveMQClientLogger; import org.apache.activemq.artemis.core.protocol.core.Channel; @@ -38,6 +41,7 @@ import org.apache.activemq.artemis.core.protocol.core.impl.ChannelImpl.CHANNEL_I import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.DisconnectConsumerWithKillMessage; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.DisconnectMessage; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.DisconnectMessage_V2; +import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.DisconnectMessage_V3; import org.apache.activemq.artemis.core.security.ActiveMQPrincipal; import org.apache.activemq.artemis.spi.core.protocol.AbstractRemotingConnection; import org.apache.activemq.artemis.spi.core.remoting.Connection; @@ -206,7 +210,7 @@ public class RemotingConnectionImpl extends AbstractRemotingConnection implement destroyed = true; } - if (!(me instanceof ActiveMQRemoteDisconnectException)) { + if (!(me instanceof ActiveMQRemoteDisconnectException) && !(me instanceof ActiveMQRedirectedException)) { ActiveMQClientLogger.LOGGER.connectionFailureDetected(transportConnection.getRemoteAddress(), me.getMessage(), me.getType()); } @@ -250,11 +254,16 @@ public class RemotingConnectionImpl extends AbstractRemotingConnection implement @Override public void disconnect(final boolean criticalError) { - disconnect(null, criticalError); + disconnect(criticalError ? DisconnectReason.SHOUT_DOWN_ON_CRITICAL_ERROR : DisconnectReason.SHOUT_DOWN, null, null); } @Override public void disconnect(String scaleDownNodeID, final boolean criticalError) { + disconnect(criticalError ? DisconnectReason.SCALE_DOWN_ON_CRITICAL_ERROR : DisconnectReason.SCALE_DOWN, scaleDownNodeID, null); + } + + @Override + public void disconnect(DisconnectReason reason, String targetNodeID, TransportConfiguration targetConnector) { Channel channel0 = getChannel(ChannelImpl.CHANNEL_ID.PING.id, -1); // And we remove all channels from the connection, this ensures no more packets will be processed after this @@ -263,7 +272,7 @@ public class RemotingConnectionImpl extends AbstractRemotingConnection implement Set allChannels = new HashSet<>(channels.values()); - if (!criticalError) { + if (!reason.isCriticalError()) { removeAllChannels(); } else { // We can't hold a lock if a critical error is happening... @@ -273,15 +282,17 @@ public class RemotingConnectionImpl extends AbstractRemotingConnection implement // Now we are 100% sure that no more packets will be processed we can flush then send the disconnect - if (!criticalError) { + if (!reason.isCriticalError()) { for (Channel channel : allChannels) { channel.flushConfirmations(); } } Packet disconnect; - if (channel0.supports(PacketImpl.DISCONNECT_V2)) { - disconnect = new DisconnectMessage_V2(nodeID, scaleDownNodeID); + if (channel0.supports(PacketImpl.DISCONNECT_V3)) { + disconnect = new DisconnectMessage_V3(nodeID, reason, SimpleString.toSimpleString(targetNodeID), targetConnector); + } else if (channel0.supports(PacketImpl.DISCONNECT_V2)) { + disconnect = new DisconnectMessage_V2(nodeID, reason.isScaleDown() ? targetNodeID : null); } else { disconnect = new DisconnectMessage(nodeID); } diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/wireformat/ClusterTopologyChangeMessage_V3.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/wireformat/ClusterTopologyChangeMessage_V3.java index d371eb56cb..f3bcfd7416 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/wireformat/ClusterTopologyChangeMessage_V3.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/wireformat/ClusterTopologyChangeMessage_V3.java @@ -30,7 +30,17 @@ public class ClusterTopologyChangeMessage_V3 extends ClusterTopologyChangeMessag final String scaleDownGroupName, final Pair pair, final boolean last) { - super(CLUSTER_TOPOLOGY_V3); + this(CLUSTER_TOPOLOGY_V3, uniqueEventID, nodeID, backupGroupName, scaleDownGroupName, pair, last); + } + + protected ClusterTopologyChangeMessage_V3(final byte type, + final long uniqueEventID, + final String nodeID, + final String backupGroupName, + final String scaleDownGroupName, + final Pair pair, + final boolean last) { + super(type); this.nodeID = nodeID; @@ -51,6 +61,10 @@ public class ClusterTopologyChangeMessage_V3 extends ClusterTopologyChangeMessag super(CLUSTER_TOPOLOGY_V3); } + public ClusterTopologyChangeMessage_V3(byte type) { + super(type); + } + public String getScaleDownGroupName() { return scaleDownGroupName; } @@ -75,8 +89,17 @@ public class ClusterTopologyChangeMessage_V3 extends ClusterTopologyChangeMessag return result; } + @Override + protected String getParentString() { + return toString(false); + } + @Override public String toString() { + return toString(true); + } + + private String toString(boolean closed) { StringBuffer buff = new StringBuffer(getParentString()); buff.append(", exit=" + exit); buff.append(", last=" + last); @@ -85,7 +108,9 @@ public class ClusterTopologyChangeMessage_V3 extends ClusterTopologyChangeMessag buff.append(", backupGroupName=" + backupGroupName); buff.append(", uniqueEventID=" + uniqueEventID); buff.append(", scaleDownGroupName=" + scaleDownGroupName); - buff.append("]"); + if (closed) { + buff.append("]"); + } return buff.toString(); } diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/wireformat/ClusterTopologyChangeMessage_V4.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/wireformat/ClusterTopologyChangeMessage_V4.java new file mode 100644 index 0000000000..fde8cd88c3 --- /dev/null +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/wireformat/ClusterTopologyChangeMessage_V4.java @@ -0,0 +1,91 @@ +/* + * 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.activemq.artemis.core.protocol.core.impl.wireformat; + +import org.apache.activemq.artemis.api.core.ActiveMQBuffer; +import org.apache.activemq.artemis.api.core.Pair; +import org.apache.activemq.artemis.api.core.TransportConfiguration; + +public class ClusterTopologyChangeMessage_V4 extends ClusterTopologyChangeMessage_V3 { + + private int serverVersion; + + public ClusterTopologyChangeMessage_V4(final long uniqueEventID, + final String nodeID, + final String backupGroupName, + final String scaleDownGroupName, + final Pair pair, + final boolean last, + final int serverVersion) { + super(CLUSTER_TOPOLOGY_V4, uniqueEventID, nodeID, backupGroupName, scaleDownGroupName, pair, last); + + this.serverVersion = serverVersion; + } + + public ClusterTopologyChangeMessage_V4() { + super(CLUSTER_TOPOLOGY_V4); + } + + public int getServerVersion() { + return serverVersion; + } + + @Override + public void encodeRest(final ActiveMQBuffer buffer) { + super.encodeRest(buffer); + + buffer.writeInt(serverVersion); + } + + @Override + public void decodeRest(final ActiveMQBuffer buffer) { + super.decodeRest(buffer); + + serverVersion = buffer.readInt(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + serverVersion; + return result; + } + + @Override + public String toString() { + StringBuffer buf = new StringBuffer(getParentString()); + buf.append(", clientVersion=" + serverVersion); + buf.append("]"); + return buf.toString(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (!(obj instanceof ClusterTopologyChangeMessage_V4)) { + return false; + } + ClusterTopologyChangeMessage_V4 other = (ClusterTopologyChangeMessage_V4) obj; + return serverVersion == other.serverVersion; + } +} diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/wireformat/CreateSessionMessage.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/wireformat/CreateSessionMessage.java index 1315249e4d..7f82c83924 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/wireformat/CreateSessionMessage.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/wireformat/CreateSessionMessage.java @@ -57,7 +57,23 @@ public class CreateSessionMessage extends PacketImpl { final boolean preAcknowledge, final int windowSize, final String defaultAddress) { - super(CREATESESSION); + this(CREATESESSION, name, sessionChannelID, version, username, password, minLargeMessageSize, xa, autoCommitSends, autoCommitAcks, preAcknowledge, windowSize, defaultAddress); + } + + protected CreateSessionMessage(final byte type, + final String name, + final long sessionChannelID, + final int version, + final String username, + final String password, + final int minLargeMessageSize, + final boolean xa, + final boolean autoCommitSends, + final boolean autoCommitAcks, + final boolean preAcknowledge, + final int windowSize, + final String defaultAddress) { + super(type); this.name = name; @@ -88,6 +104,10 @@ public class CreateSessionMessage extends PacketImpl { super(CREATESESSION); } + protected CreateSessionMessage(final byte type) { + super(type); + } + // Public -------------------------------------------------------- public String getName() { @@ -194,9 +214,18 @@ public class CreateSessionMessage extends PacketImpl { return result; } + @Override + protected String getParentString() { + return toString(false); + } + @Override public String toString() { - StringBuffer buff = new StringBuffer(getParentString()); + return toString(true); + } + + private String toString(boolean closed) { + StringBuffer buff = new StringBuffer(super.getParentString()); buff.append(", autoCommitAcks=" + autoCommitAcks); buff.append(", autoCommitSends=" + autoCommitSends); buff.append(", defaultAddress=" + defaultAddress); @@ -209,7 +238,9 @@ public class CreateSessionMessage extends PacketImpl { buff.append(", version=" + version); buff.append(", windowSize=" + windowSize); buff.append(", xa=" + xa); - buff.append("]"); + if (closed) { + buff.append("]"); + } return buff.toString(); } diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/wireformat/CreateSessionMessage_V2.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/wireformat/CreateSessionMessage_V2.java new file mode 100644 index 0000000000..be2f8d343e --- /dev/null +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/wireformat/CreateSessionMessage_V2.java @@ -0,0 +1,103 @@ +/* + * 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.activemq.artemis.core.protocol.core.impl.wireformat; + +import org.apache.activemq.artemis.api.core.ActiveMQBuffer; + +public class CreateSessionMessage_V2 extends CreateSessionMessage { + + private String clientID = null; + + public CreateSessionMessage_V2(final String name, + final long sessionChannelID, + final int version, + final String username, + final String password, + final int minLargeMessageSize, + final boolean xa, + final boolean autoCommitSends, + final boolean autoCommitAcks, + final boolean preAcknowledge, + final int windowSize, + final String defaultAddress, + final String clientID) { + super(CREATESESSION_V2, name, sessionChannelID, version, username, password, minLargeMessageSize, xa, autoCommitSends, autoCommitAcks, preAcknowledge, windowSize, defaultAddress); + + this.clientID = clientID; + } + + public CreateSessionMessage_V2() { + super(CREATESESSION_V2); + } + + // Public -------------------------------------------------------- + + + public String getClientID() { + return clientID; + } + + @Override + public void encodeRest(final ActiveMQBuffer buffer) { + super.encodeRest(buffer); + + buffer.writeNullableString(clientID); + } + + @Override + public void decodeRest(final ActiveMQBuffer buffer) { + super.decodeRest(buffer); + + clientID = buffer.readNullableString(); + } + + @Override + public String toString() { + StringBuffer buf = new StringBuffer(getParentString()); + buf.append(", metadata=" + clientID); + buf.append("]"); + return buf.toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((clientID == null) ? 0 : clientID.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (!(obj instanceof CreateSessionMessage_V2)) { + return false; + } + CreateSessionMessage_V2 other = (CreateSessionMessage_V2) obj; + if (clientID == null) { + if (other.clientID != null) + return false; + } else if (!clientID.equals(other.clientID)) + return false; + return true; + } +} diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/wireformat/DisconnectMessage_V3.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/wireformat/DisconnectMessage_V3.java new file mode 100644 index 0000000000..671e1aa11b --- /dev/null +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/wireformat/DisconnectMessage_V3.java @@ -0,0 +1,137 @@ +/* + * 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.activemq.artemis.core.protocol.core.impl.wireformat; + +import org.apache.activemq.artemis.api.core.ActiveMQBuffer; +import org.apache.activemq.artemis.api.core.DisconnectReason; +import org.apache.activemq.artemis.api.core.SimpleString; +import org.apache.activemq.artemis.api.core.TransportConfiguration; + +public class DisconnectMessage_V3 extends DisconnectMessage { + + private DisconnectReason reason; + private SimpleString targetNodeID; + private TransportConfiguration targetConnector; + + public DisconnectMessage_V3(final SimpleString nodeID, + final DisconnectReason reason, + final SimpleString targetNodeID, + final TransportConfiguration targetConnector) { + super(DISCONNECT_V3); + + this.nodeID = nodeID; + + this.reason = reason; + + this.targetNodeID = targetNodeID; + + this.targetConnector = targetConnector; + } + + public DisconnectMessage_V3() { + super(DISCONNECT_V3); + } + + // Public -------------------------------------------------------- + + public DisconnectReason getReason() { + return reason; + } + + public SimpleString getTargetNodeID() { + return targetNodeID; + } + + public TransportConfiguration getTargetConnector() { + return targetConnector; + } + + @Override + public void encodeRest(final ActiveMQBuffer buffer) { + super.encodeRest(buffer); + buffer.writeByte(reason == null ? -1 : reason.getType()); + buffer.writeNullableSimpleString(targetNodeID); + if (targetConnector != null) { + buffer.writeBoolean(true); + targetConnector.encode(buffer); + } else { + buffer.writeBoolean(false); + } + } + + @Override + public void decodeRest(final ActiveMQBuffer buffer) { + super.decodeRest(buffer); + reason = DisconnectReason.getType(buffer.readByte()); + targetNodeID = buffer.readNullableSimpleString(); + boolean hasTargetConnector = buffer.readBoolean(); + if (hasTargetConnector) { + targetConnector = new TransportConfiguration(); + targetConnector.decode(buffer); + } else { + targetConnector = null; + } + } + + @Override + public String toString() { + StringBuffer buf = new StringBuffer(getParentString()); + buf.append(", nodeID=" + nodeID); + buf.append(", reason=" + reason); + buf.append(", targetNodeID=" + targetNodeID); + buf.append(", targetConnector=" + targetConnector); + buf.append("]"); + return buf.toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + (reason.getType()); + result = prime * result + ((targetNodeID == null) ? 0 : targetNodeID.hashCode()); + result = prime * result + ((targetConnector == null) ? 0 : targetConnector.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (!(obj instanceof DisconnectMessage_V3)) { + return false; + } + DisconnectMessage_V3 other = (DisconnectMessage_V3) obj; + if (reason == null) { + if (other.reason != null) + return false; + } else if (!reason.equals(other.reason)) + return false; + if (targetNodeID == null) { + if (other.targetNodeID != null) { + return false; + } + } else if (!targetNodeID.equals(other.targetNodeID)) { + return false; + } + return true; + } +} diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/remoting/impl/netty/TransportConstants.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/remoting/impl/netty/TransportConstants.java index 37100875f3..37a4e80bb6 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/remoting/impl/netty/TransportConstants.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/remoting/impl/netty/TransportConstants.java @@ -361,6 +361,10 @@ public class TransportConstants { public static final boolean DEFAULT_PROXY_REMOTE_DNS = false; + public static final String REDIRECT_TO = "redirect-to"; + + public static final String DEFAULT_REDIRECT_TO = null; + private static int parseDefaultVariable(String variableName, int defaultValue) { try { String variable = System.getProperty(TransportConstants.class.getName() + "." + variableName); @@ -437,6 +441,7 @@ public class TransportConstants { allowableAcceptorKeys.add(TransportConstants.QUIET_PERIOD); allowableAcceptorKeys.add(TransportConstants.DISABLE_STOMP_SERVER_HEADER); allowableAcceptorKeys.add(TransportConstants.AUTO_START); + allowableAcceptorKeys.add(TransportConstants.REDIRECT_TO); ALLOWABLE_ACCEPTOR_KEYS = Collections.unmodifiableSet(allowableAcceptorKeys); diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/spi/core/protocol/RemotingConnection.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/spi/core/protocol/RemotingConnection.java index f9f4fa508a..81f378e8fa 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/spi/core/protocol/RemotingConnection.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/spi/core/protocol/RemotingConnection.java @@ -21,6 +21,7 @@ import java.util.concurrent.Future; import org.apache.activemq.artemis.api.core.ActiveMQBuffer; import org.apache.activemq.artemis.api.core.ActiveMQException; +import org.apache.activemq.artemis.api.core.DisconnectReason; import org.apache.activemq.artemis.api.core.SimpleString; import org.apache.activemq.artemis.api.core.TransportConfiguration; import org.apache.activemq.artemis.core.remoting.CloseListener; @@ -183,6 +184,13 @@ public interface RemotingConnection extends BufferHandler { */ void disconnect(String scaleDownNodeID, boolean criticalError); + /** + * Disconnect the connection, closing all channels + */ + default void disconnect(DisconnectReason reason, String targetNodeID, TransportConfiguration targetConnector) { + disconnect(reason.isScaleDown() ? targetNodeID : null, reason.isCriticalError()); + } + /** * returns true if any data has been received since the last time this method was called. * diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/spi/core/remoting/ClientProtocolManager.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/spi/core/remoting/ClientProtocolManager.java index 37e699ea2f..764c2841a8 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/spi/core/remoting/ClientProtocolManager.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/spi/core/remoting/ClientProtocolManager.java @@ -69,7 +69,8 @@ public interface ClientProtocolManager { boolean autoCommitAcks, boolean preAcknowledge, int minLargeMessageSize, - int confirmationWindowSize) throws ActiveMQException; + int confirmationWindowSize, + String clientID) throws ActiveMQException; boolean cleanupBeforeFailover(ActiveMQException cause); diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/spi/core/remoting/Connection.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/spi/core/remoting/Connection.java index 28584aee76..0067c1e3ea 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/spi/core/remoting/Connection.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/spi/core/remoting/Connection.java @@ -172,4 +172,12 @@ public interface Connection { //returns true if one of the configs points to the same //node as this connection does. boolean isSameTarget(TransportConfiguration... configs); + + default String getSNIHostName() { + return null; + } + + default String getRedirectTo() { + return null; + } } diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/spi/core/remoting/TopologyResponseHandler.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/spi/core/remoting/TopologyResponseHandler.java index 55e202c94b..c987339fa2 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/spi/core/remoting/TopologyResponseHandler.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/spi/core/remoting/TopologyResponseHandler.java @@ -16,6 +16,7 @@ */ package org.apache.activemq.artemis.spi.core.remoting; +import org.apache.activemq.artemis.api.core.DisconnectReason; import org.apache.activemq.artemis.api.core.Pair; import org.apache.activemq.artemis.api.core.TransportConfiguration; import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection; @@ -23,7 +24,7 @@ import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection; public interface TopologyResponseHandler { // This is sent when the server is telling the client the node is being disconnected - void nodeDisconnected(RemotingConnection conn, String nodeID, String scaleDownTargetNodeID); + void nodeDisconnected(RemotingConnection conn, String nodeID, DisconnectReason reason, String targetNodeID, TransportConfiguration tagetConnector); void notifyNodeUp(long uniqueEventID, String backupGroupName, diff --git a/artemis-core-client/src/main/resources/activemq-version.properties b/artemis-core-client/src/main/resources/activemq-version.properties index ff65ff998c..e8ece7632d 100644 --- a/artemis-core-client/src/main/resources/activemq-version.properties +++ b/artemis-core-client/src/main/resources/activemq-version.properties @@ -20,4 +20,4 @@ activemq.version.minorVersion=${activemq.version.minorVersion} activemq.version.microVersion=${activemq.version.microVersion} activemq.version.incrementingVersion=${activemq.version.incrementingVersion} activemq.version.versionTag=${activemq.version.versionTag} -activemq.version.compatibleVersionList=121,122,123,124,125,126,127,128,129,130 +activemq.version.compatibleVersionList=121,122,123,124,125,126,127,128,129,130,131 diff --git a/artemis-core-client/src/test/java/org/apache/activemq/artemis/api/core/management/OperationAnnotationTest.java b/artemis-core-client/src/test/java/org/apache/activemq/artemis/api/core/management/OperationAnnotationTest.java index d59799aff6..1451f1593e 100644 --- a/artemis-core-client/src/test/java/org/apache/activemq/artemis/api/core/management/OperationAnnotationTest.java +++ b/artemis-core-client/src/test/java/org/apache/activemq/artemis/api/core/management/OperationAnnotationTest.java @@ -42,7 +42,8 @@ public class OperationAnnotationTest { {DivertControl.class}, {AcceptorControl.class}, {ClusterConnectionControl.class}, - {BroadcastGroupControl.class}}); + {BroadcastGroupControl.class}, + {BrokerBalancerControl.class}}); } private Class managementClass; diff --git a/artemis-jms-client/src/main/java/org/apache/activemq/artemis/jms/client/ActiveMQConnection.java b/artemis-jms-client/src/main/java/org/apache/activemq/artemis/jms/client/ActiveMQConnection.java index b3026ad030..61f5f3fbfb 100644 --- a/artemis-jms-client/src/main/java/org/apache/activemq/artemis/jms/client/ActiveMQConnection.java +++ b/artemis-jms-client/src/main/java/org/apache/activemq/artemis/jms/client/ActiveMQConnection.java @@ -79,14 +79,6 @@ public class ActiveMQConnection extends ActiveMQConnectionForContextImpl impleme public static final SimpleString CONNECTION_ID_PROPERTY_NAME = MessageUtil.CONNECTION_ID_PROPERTY_NAME; - /** - * Just like {@link ClientSession.AddressQuery#JMS_SESSION_IDENTIFIER_PROPERTY} this is - * used to identify the ClientID over JMS Session. - * However this is only used when the JMS Session.clientID is set (which is optional). - * With this property management tools and the server can identify the jms-client-id used over JMS - */ - public static String JMS_SESSION_CLIENT_ID_PROPERTY = "jms-client-id"; - // Static --------------------------------------------------------------------------------------- // Attributes ----------------------------------------------------------------------------------- @@ -271,7 +263,7 @@ public class ActiveMQConnection extends ActiveMQConnectionForContextImpl impleme private void validateClientID(ClientSession validateSession, String clientID) throws InvalidClientIDException, ActiveMQException { try { - validateSession.addUniqueMetaData(JMS_SESSION_CLIENT_ID_PROPERTY, clientID); + validateSession.addUniqueMetaData(ClientSession.JMS_SESSION_CLIENT_ID_PROPERTY, clientID); } catch (ActiveMQException e) { if (e.getType() == ActiveMQExceptionType.DUPLICATE_METADATA) { throw new InvalidClientIDException("clientID=" + clientID + " was already set into another connection"); @@ -605,17 +597,17 @@ public class ActiveMQConnection extends ActiveMQConnectionForContextImpl impleme boolean isBlockOnAcknowledge = sessionFactory.getServerLocator().isBlockOnAcknowledge(); int ackBatchSize = sessionFactory.getServerLocator().getAckBatchSize(); if (acknowledgeMode == Session.SESSION_TRANSACTED) { - session = sessionFactory.createSession(username, password, isXA, false, false, sessionFactory.getServerLocator().isPreAcknowledge(), transactionBatchSize); + session = sessionFactory.createSession(username, password, isXA, false, false, sessionFactory.getServerLocator().isPreAcknowledge(), transactionBatchSize, clientID); } else if (acknowledgeMode == Session.AUTO_ACKNOWLEDGE) { - session = sessionFactory.createSession(username, password, isXA, true, true, sessionFactory.getServerLocator().isPreAcknowledge(), 0); + session = sessionFactory.createSession(username, password, isXA, true, true, sessionFactory.getServerLocator().isPreAcknowledge(), 0, clientID); } else if (acknowledgeMode == Session.DUPS_OK_ACKNOWLEDGE) { - session = sessionFactory.createSession(username, password, isXA, true, true, sessionFactory.getServerLocator().isPreAcknowledge(), dupsOKBatchSize); + session = sessionFactory.createSession(username, password, isXA, true, true, sessionFactory.getServerLocator().isPreAcknowledge(), dupsOKBatchSize, clientID); } else if (acknowledgeMode == Session.CLIENT_ACKNOWLEDGE) { - session = sessionFactory.createSession(username, password, isXA, true, false, sessionFactory.getServerLocator().isPreAcknowledge(), isBlockOnAcknowledge ? transactionBatchSize : ackBatchSize); + session = sessionFactory.createSession(username, password, isXA, true, false, sessionFactory.getServerLocator().isPreAcknowledge(), isBlockOnAcknowledge ? transactionBatchSize : ackBatchSize, clientID); } else if (acknowledgeMode == ActiveMQJMSConstants.INDIVIDUAL_ACKNOWLEDGE) { - session = sessionFactory.createSession(username, password, isXA, true, false, false, isBlockOnAcknowledge ? transactionBatchSize : ackBatchSize); + session = sessionFactory.createSession(username, password, isXA, true, false, false, isBlockOnAcknowledge ? transactionBatchSize : ackBatchSize, clientID); } else if (acknowledgeMode == ActiveMQJMSConstants.PRE_ACKNOWLEDGE) { - session = sessionFactory.createSession(username, password, isXA, true, false, true, transactionBatchSize); + session = sessionFactory.createSession(username, password, isXA, true, false, true, transactionBatchSize, clientID); } else { throw new JMSRuntimeException("Invalid ackmode: " + acknowledgeMode); } @@ -636,8 +628,6 @@ public class ActiveMQConnection extends ActiveMQConnectionForContextImpl impleme session.start(); } - this.addSessionMetaData(session); - return jbs; } catch (ActiveMQException e) { throw JMSExceptionHelper.convertFromActiveMQException(e); @@ -681,13 +671,13 @@ public class ActiveMQConnection extends ActiveMQConnectionForContextImpl impleme public void authorize(boolean validateClientId) throws JMSException { try { - initialSession = sessionFactory.createSession(username, password, false, false, false, false, 0); + initialSession = sessionFactory.createSession(username, password, false, false, false, false, 0, clientID); if (clientID != null) { if (validateClientId) { validateClientID(initialSession, clientID); } else { - initialSession.addMetaData(JMS_SESSION_CLIENT_ID_PROPERTY, clientID); + initialSession.addMetaData(ClientSession.JMS_SESSION_CLIENT_ID_PROPERTY, clientID); } } @@ -703,7 +693,7 @@ public class ActiveMQConnection extends ActiveMQConnectionForContextImpl impleme private void addSessionMetaData(ClientSession session) throws ActiveMQException { session.addMetaData(ClientSession.JMS_SESSION_IDENTIFIER_PROPERTY, ""); if (clientID != null) { - session.addMetaData(JMS_SESSION_CLIENT_ID_PROPERTY, clientID); + session.addMetaData(ClientSession.JMS_SESSION_CLIENT_ID_PROPERTY, clientID); } } diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/broker/ProtonProtocolManager.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/broker/ProtonProtocolManager.java index 6168c31c76..30b4b51532 100644 --- a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/broker/ProtonProtocolManager.java +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/broker/ProtonProtocolManager.java @@ -36,6 +36,7 @@ import org.apache.activemq.artemis.protocol.amqp.client.ProtonClientProtocolMana import org.apache.activemq.artemis.protocol.amqp.connect.mirror.ReferenceNodeStore; import org.apache.activemq.artemis.protocol.amqp.proton.AMQPConnectionContext; import org.apache.activemq.artemis.protocol.amqp.proton.AMQPConstants; +import org.apache.activemq.artemis.protocol.amqp.proton.AMQPRedirectHandler; import org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport; import org.apache.activemq.artemis.protocol.amqp.sasl.ClientSASLFactory; import org.apache.activemq.artemis.protocol.amqp.sasl.MechanismFinder; @@ -53,7 +54,7 @@ import org.jboss.logging.Logger; /** * A proton protocol manager, basically reads the Proton Input and maps proton resources to ActiveMQ Artemis resources */ -public class ProtonProtocolManager extends AbstractProtocolManager implements NotificationListener { +public class ProtonProtocolManager extends AbstractProtocolManager implements NotificationListener { private static final Logger logger = Logger.getLogger(ProtonProtocolManager.class); @@ -105,6 +106,7 @@ public class ProtonProtocolManager extends AbstractProtocolManager + * 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.activemq.artemis.protocol.amqp.proton; + +import org.apache.activemq.artemis.core.server.balancing.RedirectContext; +import org.apache.qpid.proton.engine.Connection; + +public class AMQPRedirectContext extends RedirectContext { + private final Connection protonConnection; + + + public Connection getProtonConnection() { + return protonConnection; + } + + + public AMQPRedirectContext(AMQPConnectionContext connectionContext, Connection protonConnection) { + super(connectionContext.getConnectionCallback().getProtonConnectionDelegate(), connectionContext.getRemoteContainer(), + connectionContext.getSASLResult() != null ? connectionContext.getSASLResult().getUser() : null); + this.protonConnection = protonConnection; + } +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/AMQPRedirectHandler.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/AMQPRedirectHandler.java new file mode 100644 index 0000000000..d852a3aab1 --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/AMQPRedirectHandler.java @@ -0,0 +1,64 @@ +/** + * 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.activemq.artemis.protocol.amqp.proton; + +import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants; +import org.apache.activemq.artemis.core.server.ActiveMQServer; +import org.apache.activemq.artemis.core.server.balancing.RedirectHandler; +import org.apache.activemq.artemis.utils.ConfigurationHelper; +import org.apache.qpid.proton.amqp.transport.ConnectionError; +import org.apache.qpid.proton.amqp.transport.ErrorCondition; +import org.apache.qpid.proton.engine.Connection; + +import java.util.HashMap; +import java.util.Map; + +public class AMQPRedirectHandler extends RedirectHandler { + + public AMQPRedirectHandler(ActiveMQServer server) { + super(server); + } + + + public boolean redirect(AMQPConnectionContext connectionContext, Connection protonConnection) throws Exception { + return redirect(new AMQPRedirectContext(connectionContext, protonConnection)); + } + + @Override + protected void cannotRedirect(AMQPRedirectContext context) throws Exception { + ErrorCondition error = new ErrorCondition(); + error.setCondition(ConnectionError.CONNECTION_FORCED); + error.setDescription(String.format("Broker balancer %s is not ready to redirect", context.getConnection().getTransportConnection().getRedirectTo())); + context.getProtonConnection().setCondition(error); + } + + @Override + protected void redirectTo(AMQPRedirectContext context) throws Exception { + String host = ConfigurationHelper.getStringProperty(TransportConstants.HOST_PROP_NAME, TransportConstants.DEFAULT_HOST, context.getTarget().getConnector().getParams()); + int port = ConfigurationHelper.getIntProperty(TransportConstants.PORT_PROP_NAME, TransportConstants.DEFAULT_PORT, context.getTarget().getConnector().getParams()); + + ErrorCondition error = new ErrorCondition(); + error.setCondition(ConnectionError.REDIRECT); + error.setDescription(String.format("Connection redirected to %s:%d by broker balancer %s", host, port, context.getConnection().getTransportConnection().getRedirectTo())); + Map info = new HashMap(); + info.put(AmqpSupport.NETWORK_HOST, host); + info.put(AmqpSupport.PORT, port); + error.setInfo(info); + context.getProtonConnection().setCondition(error); + } +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/AnonymousServerSASLFactory.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/AnonymousServerSASLFactory.java index 82d3ec17a7..485b0efd62 100644 --- a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/AnonymousServerSASLFactory.java +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/AnonymousServerSASLFactory.java @@ -18,6 +18,7 @@ package org.apache.activemq.artemis.protocol.amqp.sasl; import org.apache.activemq.artemis.core.server.ActiveMQServer; import org.apache.activemq.artemis.protocol.amqp.broker.AmqpInterceptor; +import org.apache.activemq.artemis.protocol.amqp.proton.AMQPRedirectHandler; import org.apache.activemq.artemis.spi.core.protocol.ProtocolManager; import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection; import org.apache.activemq.artemis.spi.core.remoting.Connection; @@ -30,7 +31,7 @@ public class AnonymousServerSASLFactory implements ServerSASLFactory { } @Override - public ServerSASL create(ActiveMQServer server, ProtocolManager manager, Connection connection, + public ServerSASL create(ActiveMQServer server, ProtocolManager manager, Connection connection, RemotingConnection remotingConnection) { return new AnonymousServerSASL(); } diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/ExternalServerSASLFactory.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/ExternalServerSASLFactory.java index e9087bec9d..9bcaf05f2a 100644 --- a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/ExternalServerSASLFactory.java +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/ExternalServerSASLFactory.java @@ -21,6 +21,7 @@ import java.security.Principal; import org.apache.activemq.artemis.core.remoting.CertificateUtil; import org.apache.activemq.artemis.core.server.ActiveMQServer; import org.apache.activemq.artemis.protocol.amqp.broker.AmqpInterceptor; +import org.apache.activemq.artemis.protocol.amqp.proton.AMQPRedirectHandler; import org.apache.activemq.artemis.spi.core.protocol.ProtocolManager; import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection; import org.apache.activemq.artemis.spi.core.remoting.Connection; @@ -39,7 +40,7 @@ public class ExternalServerSASLFactory implements ServerSASLFactory { } @Override - public ServerSASL create(ActiveMQServer server, ProtocolManager manager, Connection connection, + public ServerSASL create(ActiveMQServer server, ProtocolManager manager, Connection connection, RemotingConnection remotingConnection) { // validate ssl cert present Principal principal = CertificateUtil.getPeerPrincipalFromConnection(remotingConnection); diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/GSSAPIServerSASLFactory.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/GSSAPIServerSASLFactory.java index e31632a607..098668e9f8 100644 --- a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/GSSAPIServerSASLFactory.java +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/GSSAPIServerSASLFactory.java @@ -19,6 +19,7 @@ package org.apache.activemq.artemis.protocol.amqp.sasl; import org.apache.activemq.artemis.core.server.ActiveMQServer; import org.apache.activemq.artemis.protocol.amqp.broker.AmqpInterceptor; import org.apache.activemq.artemis.protocol.amqp.broker.ProtonProtocolManager; +import org.apache.activemq.artemis.protocol.amqp.proton.AMQPRedirectHandler; import org.apache.activemq.artemis.spi.core.protocol.ProtocolManager; import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection; import org.apache.activemq.artemis.spi.core.remoting.Connection; @@ -34,7 +35,7 @@ public class GSSAPIServerSASLFactory implements ServerSASLFactory { } @Override - public ServerSASL create(ActiveMQServer server, ProtocolManager manager, Connection connection, + public ServerSASL create(ActiveMQServer server, ProtocolManager manager, Connection connection, RemotingConnection remotingConnection) { if (manager instanceof ProtonProtocolManager) { GSSAPIServerSASL gssapiServerSASL = new GSSAPIServerSASL(); diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/PlainServerSASLFactory.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/PlainServerSASLFactory.java index 5a88a82a9d..2596c820b8 100644 --- a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/PlainServerSASLFactory.java +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/PlainServerSASLFactory.java @@ -18,6 +18,7 @@ package org.apache.activemq.artemis.protocol.amqp.sasl; import org.apache.activemq.artemis.core.server.ActiveMQServer; import org.apache.activemq.artemis.protocol.amqp.broker.AmqpInterceptor; +import org.apache.activemq.artemis.protocol.amqp.proton.AMQPRedirectHandler; import org.apache.activemq.artemis.spi.core.protocol.ProtocolManager; import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection; import org.apache.activemq.artemis.spi.core.remoting.Connection; @@ -30,7 +31,7 @@ public class PlainServerSASLFactory implements ServerSASLFactory { } @Override - public ServerSASL create(ActiveMQServer server, ProtocolManager manager, Connection connection, + public ServerSASL create(ActiveMQServer server, ProtocolManager manager, Connection connection, RemotingConnection remotingConnection) { return new PlainSASL(server.getSecurityStore(), manager.getSecurityDomain(), connection.getProtocolConnection()); } diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/ServerSASLFactory.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/ServerSASLFactory.java index 9831652a61..ded4b2739f 100644 --- a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/ServerSASLFactory.java +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/ServerSASLFactory.java @@ -18,6 +18,7 @@ package org.apache.activemq.artemis.protocol.amqp.sasl; import org.apache.activemq.artemis.core.server.ActiveMQServer; import org.apache.activemq.artemis.protocol.amqp.broker.AmqpInterceptor; +import org.apache.activemq.artemis.protocol.amqp.proton.AMQPRedirectHandler; import org.apache.activemq.artemis.spi.core.protocol.ProtocolManager; import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection; import org.apache.activemq.artemis.spi.core.remoting.Connection; @@ -40,7 +41,7 @@ public interface ServerSASLFactory { * @param remotingConnection * @return a new instance of {@link ServerSASL} that implements the provided mechanism */ - ServerSASL create(ActiveMQServer server, ProtocolManager manager, Connection connection, + ServerSASL create(ActiveMQServer server, ProtocolManager manager, Connection connection, RemotingConnection remotingConnection); /** diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SCRAMServerSASLFactory.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SCRAMServerSASLFactory.java index 67ec428efd..37190052f7 100644 --- a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SCRAMServerSASLFactory.java +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SCRAMServerSASLFactory.java @@ -31,6 +31,7 @@ import javax.security.auth.login.LoginException; import org.apache.activemq.artemis.core.server.ActiveMQServer; import org.apache.activemq.artemis.protocol.amqp.broker.AmqpInterceptor; import org.apache.activemq.artemis.protocol.amqp.broker.ProtonProtocolManager; +import org.apache.activemq.artemis.protocol.amqp.proton.AMQPRedirectHandler; import org.apache.activemq.artemis.protocol.amqp.sasl.ServerSASL; import org.apache.activemq.artemis.protocol.amqp.sasl.ServerSASLFactory; import org.apache.activemq.artemis.spi.core.protocol.ProtocolManager; @@ -67,7 +68,7 @@ public abstract class SCRAMServerSASLFactory implements ServerSASLFactory { } @Override - public ServerSASL create(ActiveMQServer server, ProtocolManager manager, Connection connection, + public ServerSASL create(ActiveMQServer server, ProtocolManager manager, Connection connection, RemotingConnection remotingConnection) { try { if (manager instanceof ProtonProtocolManager) { diff --git a/artemis-protocols/artemis-hqclient-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/hornetq/client/HornetQClientProtocolManager.java b/artemis-protocols/artemis-hqclient-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/hornetq/client/HornetQClientProtocolManager.java index 273038362c..84498d9600 100644 --- a/artemis-protocols/artemis-hqclient-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/hornetq/client/HornetQClientProtocolManager.java +++ b/artemis-protocols/artemis-hqclient-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/hornetq/client/HornetQClientProtocolManager.java @@ -23,11 +23,9 @@ import org.apache.activemq.artemis.core.protocol.core.Channel; import org.apache.activemq.artemis.core.protocol.core.Packet; import org.apache.activemq.artemis.core.protocol.core.impl.ActiveMQClientProtocolManager; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.ClusterTopologyChangeMessage; -import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.CreateSessionMessage; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.CreateSessionResponseMessage; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.SubscribeClusterTopologyUpdatesMessageV2; import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnectorFactory; -import org.apache.activemq.artemis.core.version.Version; import org.apache.activemq.artemis.spi.core.remoting.Connection; import org.apache.activemq.artemis.spi.core.remoting.SessionContext; @@ -49,7 +47,7 @@ public class HornetQClientProtocolManager extends ActiveMQClientProtocolManager } @Override - protected Packet newCreateSessionPacket(Version clientVersion, + protected Packet newCreateSessionPacket(int clientVersion, String name, String username, String password, @@ -59,8 +57,9 @@ public class HornetQClientProtocolManager extends ActiveMQClientProtocolManager boolean preAcknowledge, int minLargeMessageSize, int confirmationWindowSize, - long sessionChannelID) { - return new CreateSessionMessage(name, sessionChannelID, VERSION_PLAYED, username, password, minLargeMessageSize, xa, autoCommitSends, autoCommitAcks, preAcknowledge, confirmationWindowSize, null); + long sessionChannelID, + String clientID) { + return super.newCreateSessionPacket(VERSION_PLAYED, name, username, password, xa, autoCommitSends, autoCommitAcks, preAcknowledge, minLargeMessageSize, confirmationWindowSize, sessionChannelID, clientID); } @Override diff --git a/artemis-protocols/artemis-mqtt-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/mqtt/MQTTProtocolHandler.java b/artemis-protocols/artemis-mqtt-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/mqtt/MQTTProtocolHandler.java index d34ade5180..82bf22192a 100644 --- a/artemis-protocols/artemis-mqtt-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/mqtt/MQTTProtocolHandler.java +++ b/artemis-protocols/artemis-mqtt-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/mqtt/MQTTProtocolHandler.java @@ -28,6 +28,7 @@ import io.netty.handler.codec.mqtt.MqttFixedHeader; import io.netty.handler.codec.mqtt.MqttMessage; import io.netty.handler.codec.mqtt.MqttMessageIdVariableHeader; import io.netty.handler.codec.mqtt.MqttMessageType; +import io.netty.handler.codec.mqtt.MqttProperties; import io.netty.handler.codec.mqtt.MqttPubAckMessage; import io.netty.handler.codec.mqtt.MqttPublishMessage; import io.netty.handler.codec.mqtt.MqttPublishVariableHeader; @@ -176,10 +177,13 @@ public class MQTTProtocolHandler extends ChannelInboundHandlerAdapter { * @param connect */ void handleConnect(MqttConnectMessage connect) throws Exception { - connectionEntry.ttl = connect.variableHeader().keepAliveTimeSeconds() * 1500L; + if (connection.getTransportConnection().getRedirectTo() == null || + !protocolManager.getRedirectHandler().redirect(connection, session, connect)) { + connectionEntry.ttl = connect.variableHeader().keepAliveTimeSeconds() * 1500L; - String clientId = connect.payload().clientIdentifier(); - session.getConnectionManager().connect(clientId, connect.payload().userName(), connect.payload().passwordInBytes(), connect.variableHeader().isWillFlag(), connect.payload().willMessageInBytes(), connect.payload().willTopic(), connect.variableHeader().isWillRetain(), connect.variableHeader().willQos(), connect.variableHeader().isCleanSession()); + String clientId = connect.payload().clientIdentifier(); + session.getConnectionManager().connect(clientId, connect.payload().userName(), connect.payload().passwordInBytes(), connect.variableHeader().isWillFlag(), connect.payload().willMessageInBytes(), connect.payload().willTopic(), connect.variableHeader().isWillRetain(), connect.variableHeader().willQos(), connect.variableHeader().isCleanSession()); + } } void disconnect(boolean error) { @@ -187,8 +191,12 @@ public class MQTTProtocolHandler extends ChannelInboundHandlerAdapter { } void sendConnack(MqttConnectReturnCode returnCode) { + sendConnack(returnCode, MqttProperties.NO_PROPERTIES); + } + + void sendConnack(MqttConnectReturnCode returnCode, MqttProperties properties) { MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.CONNACK, false, MqttQoS.AT_MOST_ONCE, false, 0); - MqttConnAckVariableHeader varHeader = new MqttConnAckVariableHeader(returnCode, true); + MqttConnAckVariableHeader varHeader = new MqttConnAckVariableHeader(returnCode, true, properties); MqttConnAckMessage message = new MqttConnAckMessage(fixedHeader, varHeader); sendToClient(message); } diff --git a/artemis-protocols/artemis-mqtt-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/mqtt/MQTTProtocolManager.java b/artemis-protocols/artemis-mqtt-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/mqtt/MQTTProtocolManager.java index d0f5d1283f..6513247328 100644 --- a/artemis-protocols/artemis-mqtt-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/mqtt/MQTTProtocolManager.java +++ b/artemis-protocols/artemis-mqtt-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/mqtt/MQTTProtocolManager.java @@ -48,7 +48,7 @@ import org.apache.activemq.artemis.utils.collections.TypedProperties; /** * MQTTProtocolManager */ -public class MQTTProtocolManager extends AbstractProtocolManager implements NotificationListener { +public class MQTTProtocolManager extends AbstractProtocolManager implements NotificationListener { private static final List websocketRegistryNames = Arrays.asList("mqtt", "mqttv3.1"); @@ -62,6 +62,8 @@ public class MQTTProtocolManager extends AbstractProtocolManager connectedClients; private final Map sessionStates; + private final MQTTRedirectHandler redirectHandler; + MQTTProtocolManager(ActiveMQServer server, Map connectedClients, Map sessionStates, @@ -72,6 +74,7 @@ public class MQTTProtocolManager extends AbstractProtocolManager + * 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.activemq.artemis.core.protocol.mqtt; + +import io.netty.handler.codec.mqtt.MqttConnectMessage; +import org.apache.activemq.artemis.core.server.balancing.RedirectContext; + +public class MQTTRedirectContext extends RedirectContext { + + private final MQTTSession mqttSession; + + + public MQTTSession getMQTTSession() { + return mqttSession; + } + + + public MQTTRedirectContext(MQTTConnection mqttConnection, MQTTSession mqttSession, MqttConnectMessage connect) { + super(mqttConnection, connect.payload().clientIdentifier(), connect.payload().userName()); + this.mqttSession = mqttSession; + } +} diff --git a/artemis-protocols/artemis-mqtt-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/mqtt/MQTTRedirectHandler.java b/artemis-protocols/artemis-mqtt-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/mqtt/MQTTRedirectHandler.java new file mode 100644 index 0000000000..3b372032c3 --- /dev/null +++ b/artemis-protocols/artemis-mqtt-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/mqtt/MQTTRedirectHandler.java @@ -0,0 +1,55 @@ +/** + * 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.activemq.artemis.core.protocol.mqtt; + +import io.netty.handler.codec.mqtt.MqttConnectMessage; +import io.netty.handler.codec.mqtt.MqttConnectReturnCode; +import io.netty.handler.codec.mqtt.MqttProperties; +import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants; +import org.apache.activemq.artemis.core.server.ActiveMQServer; +import org.apache.activemq.artemis.core.server.balancing.RedirectHandler; +import org.apache.activemq.artemis.utils.ConfigurationHelper; + +public class MQTTRedirectHandler extends RedirectHandler { + + protected MQTTRedirectHandler(ActiveMQServer server) { + super(server); + } + + public boolean redirect(MQTTConnection mqttConnection, MQTTSession mqttSession, MqttConnectMessage connect) throws Exception { + return redirect(new MQTTRedirectContext(mqttConnection, mqttSession, connect)); + } + + @Override + protected void cannotRedirect(MQTTRedirectContext context) throws Exception { + context.getMQTTSession().getProtocolHandler().sendConnack(MqttConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE); + context.getMQTTSession().getProtocolHandler().disconnect(true); + } + + @Override + protected void redirectTo(MQTTRedirectContext context) throws Exception { + String host = ConfigurationHelper.getStringProperty(TransportConstants.HOST_PROP_NAME, TransportConstants.DEFAULT_HOST, context.getTarget().getConnector().getParams()); + int port = ConfigurationHelper.getIntProperty(TransportConstants.PORT_PROP_NAME, TransportConstants.DEFAULT_PORT, context.getTarget().getConnector().getParams()); + + MqttProperties mqttProperties = new MqttProperties(); + mqttProperties.add(new MqttProperties.StringProperty(MqttProperties.MqttPropertyType.SERVER_REFERENCE.value(), String.format("%s:%d", host, port))); + + context.getMQTTSession().getProtocolHandler().sendConnack(MqttConnectReturnCode.CONNECTION_REFUSED_USE_ANOTHER_SERVER, mqttProperties); + context.getMQTTSession().getProtocolHandler().disconnect(true); + } +} diff --git a/artemis-protocols/artemis-openwire-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/openwire/OpenWireConnection.java b/artemis-protocols/artemis-openwire-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/openwire/OpenWireConnection.java index 5b4586d5b0..a77fec6df8 100644 --- a/artemis-protocols/artemis-openwire-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/openwire/OpenWireConnection.java +++ b/artemis-protocols/artemis-openwire-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/openwire/OpenWireConnection.java @@ -1146,6 +1146,12 @@ public class OpenWireConnection extends AbstractRemotingConnection implements Se @Override public Response processAddConnection(ConnectionInfo info) throws Exception { try { + if (transportConnection.getRedirectTo() != null && protocolManager.getRedirectHandler() + .redirect(OpenWireConnection.this, info)) { + shutdown(true); + return null; + } + protocolManager.addConnection(OpenWireConnection.this, info); } catch (Exception e) { Response resp = new ExceptionResponse(e); diff --git a/artemis-protocols/artemis-openwire-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/openwire/OpenWireProtocolManager.java b/artemis-protocols/artemis-openwire-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/openwire/OpenWireProtocolManager.java index bcc39feb3e..852f4dcfda 100644 --- a/artemis-protocols/artemis-openwire-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/openwire/OpenWireProtocolManager.java +++ b/artemis-protocols/artemis-openwire-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/openwire/OpenWireProtocolManager.java @@ -81,7 +81,7 @@ import org.apache.activemq.util.LongSequenceGenerator; import static org.apache.activemq.artemis.core.protocol.openwire.util.OpenWireUtil.SELECTOR_AWARE_OPTION; -public class OpenWireProtocolManager extends AbstractProtocolManager implements ClusterTopologyListener { +public class OpenWireProtocolManager extends AbstractProtocolManager implements ClusterTopologyListener { private static final List websocketRegistryNames = Collections.EMPTY_LIST; @@ -137,6 +137,7 @@ public class OpenWireProtocolManager extends AbstractProtocolManager incomingInterceptors = new ArrayList<>(); private final List outgoingInterceptors = new ArrayList<>(); + private final OpenWireRedirectHandler redirectHandler; protected static class VirtualTopicConfig { public int filterPathTerminus; @@ -187,6 +188,8 @@ public class OpenWireProtocolManager extends AbstractProtocolManager + * 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.activemq.artemis.core.protocol.openwire; + +import org.apache.activemq.artemis.core.server.balancing.RedirectContext; +import org.apache.activemq.command.ConnectionInfo; + +public class OpenWireRedirectContext extends RedirectContext { + + private final OpenWireConnection openWireConnection; + + + public OpenWireConnection getOpenWireConnection() { + return openWireConnection; + } + + + public OpenWireRedirectContext(OpenWireConnection openWireConnection, ConnectionInfo connectionInfo) { + super(openWireConnection.getRemotingConnection(), connectionInfo.getClientId(), connectionInfo.getUserName()); + this.openWireConnection = openWireConnection; + } +} diff --git a/artemis-protocols/artemis-openwire-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/openwire/OpenWireRedirectHandler.java b/artemis-protocols/artemis-openwire-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/openwire/OpenWireRedirectHandler.java new file mode 100644 index 0000000000..83510afad0 --- /dev/null +++ b/artemis-protocols/artemis-openwire-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/openwire/OpenWireRedirectHandler.java @@ -0,0 +1,58 @@ +/** + * 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.activemq.artemis.core.protocol.openwire; + +import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants; +import org.apache.activemq.artemis.core.server.ActiveMQServer; +import org.apache.activemq.artemis.core.server.balancing.RedirectHandler; +import org.apache.activemq.artemis.utils.ConfigurationHelper; +import org.apache.activemq.command.ConnectionControl; +import org.apache.activemq.command.ConnectionInfo; + +public class OpenWireRedirectHandler extends RedirectHandler { + + private final OpenWireProtocolManager protocolManager; + + protected OpenWireRedirectHandler(ActiveMQServer server, OpenWireProtocolManager protocolManager) { + super(server); + this.protocolManager = protocolManager; + } + + public boolean redirect(OpenWireConnection openWireConnection, ConnectionInfo connectionInfo) throws Exception { + if (!connectionInfo.isFaultTolerant()) { + throw new java.lang.IllegalStateException("Client not fault tolerant"); + } + + return redirect(new OpenWireRedirectContext(openWireConnection, connectionInfo)); + } + + @Override + protected void cannotRedirect(OpenWireRedirectContext context) throws Exception { + } + + @Override + protected void redirectTo(OpenWireRedirectContext context) throws Exception { + String host = ConfigurationHelper.getStringProperty(TransportConstants.HOST_PROP_NAME, TransportConstants.DEFAULT_HOST, context.getTarget().getConnector().getParams()); + int port = ConfigurationHelper.getIntProperty(TransportConstants.PORT_PROP_NAME, TransportConstants.DEFAULT_PORT, context.getTarget().getConnector().getParams()); + + ConnectionControl command = protocolManager.newConnectionControl(); + command.setConnectedBrokers(String.format("tcp://%s:%d", host, port)); + command.setRebalanceConnection(true); + context.getOpenWireConnection().dispatchSync(command); + } +} diff --git a/artemis-protocols/artemis-stomp-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/stomp/StompProtocolManager.java b/artemis-protocols/artemis-stomp-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/stomp/StompProtocolManager.java index 8546f9aa82..e0bfecd611 100644 --- a/artemis-protocols/artemis-stomp-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/stomp/StompProtocolManager.java +++ b/artemis-protocols/artemis-stomp-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/stomp/StompProtocolManager.java @@ -38,6 +38,7 @@ import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants; import org.apache.activemq.artemis.core.server.ActiveMQServer; import org.apache.activemq.artemis.core.server.ActiveMQServerLogger; import org.apache.activemq.artemis.core.server.ServerSession; +import org.apache.activemq.artemis.core.server.balancing.RedirectHandler; import org.apache.activemq.artemis.logs.AuditLogger; import org.apache.activemq.artemis.spi.core.protocol.AbstractProtocolManager; import org.apache.activemq.artemis.spi.core.protocol.ConnectionEntry; @@ -52,7 +53,7 @@ import static org.apache.activemq.artemis.core.protocol.stomp.ActiveMQStompProto /** * StompProtocolManager */ -public class StompProtocolManager extends AbstractProtocolManager { +public class StompProtocolManager extends AbstractProtocolManager { private static final List websocketRegistryNames = Arrays.asList("v10.stomp", "v11.stomp", "v12.stomp"); @@ -190,6 +191,11 @@ public class StompProtocolManager extends AbstractProtocolManager getBalancerConfigurations(); + + /** + * Sets the redirects configured for this server. + */ + Configuration setBalancerConfigurations(List configs); + + Configuration addBalancerConfiguration(BrokerBalancerConfiguration config); + /** * Returns the cluster connections configured for this server. *

diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/balancing/BrokerBalancerConfiguration.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/balancing/BrokerBalancerConfiguration.java new file mode 100644 index 0000000000..1da1c04742 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/balancing/BrokerBalancerConfiguration.java @@ -0,0 +1,95 @@ +/* + * 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.activemq.artemis.core.config.balancing; + +import org.apache.activemq.artemis.core.server.balancing.targets.TargetKey; + +import java.io.Serializable; + +public class BrokerBalancerConfiguration implements Serializable { + + private String name = null; + private TargetKey targetKey = TargetKey.SOURCE_IP; + private String targetKeyFilter = null; + private String localTargetFilter = null; + private int cacheTimeout = -1; + private PoolConfiguration poolConfiguration = null; + private PolicyConfiguration policyConfiguration = null; + + public String getName() { + return name; + } + + public BrokerBalancerConfiguration setName(String name) { + this.name = name; + return this; + } + + public TargetKey getTargetKey() { + return targetKey; + } + + public BrokerBalancerConfiguration setTargetKey(TargetKey targetKey) { + this.targetKey = targetKey; + return this; + } + + public String getTargetKeyFilter() { + return targetKeyFilter; + } + + public BrokerBalancerConfiguration setTargetKeyFilter(String targetKeyFilter) { + this.targetKeyFilter = targetKeyFilter; + return this; + } + + public String getLocalTargetFilter() { + return localTargetFilter; + } + + public BrokerBalancerConfiguration setLocalTargetFilter(String localTargetFilter) { + this.localTargetFilter = localTargetFilter; + return this; + } + + public int getCacheTimeout() { + return cacheTimeout; + } + + public BrokerBalancerConfiguration setCacheTimeout(int cacheTimeout) { + this.cacheTimeout = cacheTimeout; + return this; + } + + public PolicyConfiguration getPolicyConfiguration() { + return policyConfiguration; + } + + public BrokerBalancerConfiguration setPolicyConfiguration(PolicyConfiguration policyConfiguration) { + this.policyConfiguration = policyConfiguration; + return this; + } + + public PoolConfiguration getPoolConfiguration() { + return poolConfiguration; + } + + public BrokerBalancerConfiguration setPoolConfiguration(PoolConfiguration poolConfiguration) { + this.poolConfiguration = poolConfiguration; + return this; + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/balancing/PolicyConfiguration.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/balancing/PolicyConfiguration.java new file mode 100644 index 0000000000..f1f863055d --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/balancing/PolicyConfiguration.java @@ -0,0 +1,45 @@ +/** + * 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.activemq.artemis.core.config.balancing; + +import java.io.Serializable; +import java.util.Map; + +public class PolicyConfiguration implements Serializable { + private String name; + + private Map properties; + + public String getName() { + return name; + } + + public PolicyConfiguration setName(String name) { + this.name = name; + return this; + } + + public Map getProperties() { + return properties; + } + + public PolicyConfiguration setProperties(Map properties) { + this.properties = properties; + return this; + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/balancing/PoolConfiguration.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/balancing/PoolConfiguration.java new file mode 100644 index 0000000000..699184a22f --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/balancing/PoolConfiguration.java @@ -0,0 +1,123 @@ +/** + * 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.activemq.artemis.core.config.balancing; + +import java.io.Serializable; +import java.util.Collections; +import java.util.List; + +public class PoolConfiguration implements Serializable { + private String username; + + private String password; + + private boolean localTargetEnabled = false; + + private String clusterConnection = null; + + private List staticConnectors = Collections.emptyList(); + + private String discoveryGroupName = null; + + private int checkPeriod = 5000; + + private int quorumSize = 1; + + private int quorumTimeout = 3000; + + public String getUsername() { + return username; + } + + public PoolConfiguration setUsername(String username) { + this.username = username; + return this; + } + + public String getPassword() { + return password; + } + + public PoolConfiguration setPassword(String password) { + this.password = password; + return this; + } + + public int getCheckPeriod() { + return checkPeriod; + } + + public PoolConfiguration setCheckPeriod(int checkPeriod) { + this.checkPeriod = checkPeriod; + return this; + } + + public int getQuorumSize() { + return quorumSize; + } + + public PoolConfiguration setQuorumSize(int quorumSize) { + this.quorumSize = quorumSize; + return this; + } + + public int getQuorumTimeout() { + return quorumTimeout; + } + + public PoolConfiguration setQuorumTimeout(int quorumTimeout) { + this.quorumTimeout = quorumTimeout; + return this; + } + + public boolean isLocalTargetEnabled() { + return localTargetEnabled; + } + + public PoolConfiguration setLocalTargetEnabled(boolean localTargetEnabled) { + this.localTargetEnabled = localTargetEnabled; + return this; + } + + public String getClusterConnection() { + return clusterConnection; + } + + public PoolConfiguration setClusterConnection(String clusterConnection) { + this.clusterConnection = clusterConnection; + return this; + } + + public List getStaticConnectors() { + return staticConnectors; + } + + public PoolConfiguration setStaticConnectors(List staticConnectors) { + this.staticConnectors = staticConnectors; + return this; + } + + public String getDiscoveryGroupName() { + return discoveryGroupName; + } + + public PoolConfiguration setDiscoveryGroupName(String discoveryGroupName) { + this.discoveryGroupName = discoveryGroupName; + return this; + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImpl.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImpl.java index d36a804566..6aa7785f7c 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImpl.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImpl.java @@ -47,6 +47,7 @@ import org.apache.activemq.artemis.api.core.DiscoveryGroupConfiguration; import org.apache.activemq.artemis.api.core.QueueConfiguration; import org.apache.activemq.artemis.api.core.SimpleString; import org.apache.activemq.artemis.api.core.TransportConfiguration; +import org.apache.activemq.artemis.core.config.balancing.BrokerBalancerConfiguration; import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPBrokerConnectConfiguration; import org.apache.activemq.artemis.core.config.BridgeConfiguration; import org.apache.activemq.artemis.core.config.ClusterConnectionConfiguration; @@ -166,6 +167,8 @@ public class ConfigurationImpl implements Configuration, Serializable { protected List divertConfigurations = new ArrayList<>(); + protected List brokerBalancerConfigurations = new ArrayList<>(); + protected List clusterConfigurations = new ArrayList<>(); protected List amqpBrokerConnectConfigurations = new ArrayList<>(); @@ -820,6 +823,23 @@ public class ConfigurationImpl implements Configuration, Serializable { return this; } + @Override + public List getBalancerConfigurations() { + return brokerBalancerConfigurations; + } + + @Override + public ConfigurationImpl setBalancerConfigurations(final List configs) { + brokerBalancerConfigurations = configs; + return this; + } + + @Override + public ConfigurationImpl addBalancerConfiguration(final BrokerBalancerConfiguration config) { + brokerBalancerConfigurations.add(config); + return this; + } + @Deprecated @Override public List getQueueConfigurations() { diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/Validators.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/Validators.java index a6e04a89e1..4f1e2eedab 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/Validators.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/Validators.java @@ -18,6 +18,7 @@ package org.apache.activemq.artemis.core.config.impl; import java.util.EnumSet; +import org.apache.activemq.artemis.core.server.balancing.targets.TargetKey; import org.apache.activemq.artemis.core.server.ActiveMQMessageBundle; import org.apache.activemq.artemis.core.server.ComponentConfigurationRoutingType; import org.apache.activemq.artemis.core.server.JournalType; @@ -272,4 +273,14 @@ public final class Validators { } } }; + + public static final Validator TARGET_KEY = new Validator() { + @Override + public void validate(final String name, final Object value) { + String val = (String) value; + if (val == null || !EnumSet.allOf(TargetKey.class).contains(TargetKey.valueOf(val))) { + throw ActiveMQMessageBundle.BUNDLE.invalidTargetKey(val); + } + } + }; } diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/deployers/impl/FileConfigurationParser.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/deployers/impl/FileConfigurationParser.java index 93eb2dbb0e..d2e1d75cab 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/deployers/impl/FileConfigurationParser.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/deployers/impl/FileConfigurationParser.java @@ -46,6 +46,8 @@ import org.apache.activemq.artemis.api.core.SimpleString; import org.apache.activemq.artemis.api.core.TransportConfiguration; import org.apache.activemq.artemis.api.core.UDPBroadcastEndpointFactory; import org.apache.activemq.artemis.api.core.client.ActiveMQClient; +import org.apache.activemq.artemis.core.config.balancing.BrokerBalancerConfiguration; +import org.apache.activemq.artemis.core.config.balancing.PolicyConfiguration; import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPBrokerConnectConfiguration; import org.apache.activemq.artemis.core.config.BridgeConfiguration; import org.apache.activemq.artemis.core.config.ClusterConnectionConfiguration; @@ -62,6 +64,7 @@ import org.apache.activemq.artemis.core.config.WildcardConfiguration; import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPBrokerConnectionElement; import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPBrokerConnectionAddressType; import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPMirrorBrokerConnectionElement; +import org.apache.activemq.artemis.core.config.balancing.PoolConfiguration; import org.apache.activemq.artemis.core.config.federation.FederationAddressPolicyConfiguration; import org.apache.activemq.artemis.core.config.federation.FederationDownstreamConfiguration; import org.apache.activemq.artemis.core.config.federation.FederationPolicySet; @@ -88,6 +91,8 @@ import org.apache.activemq.artemis.core.server.ActiveMQServerLogger; import org.apache.activemq.artemis.core.server.ComponentConfigurationRoutingType; import org.apache.activemq.artemis.core.server.JournalType; import org.apache.activemq.artemis.core.server.SecuritySettingPlugin; +import org.apache.activemq.artemis.core.server.balancing.policies.PolicyFactoryResolver; +import org.apache.activemq.artemis.core.server.balancing.targets.TargetKey; import org.apache.activemq.artemis.core.server.cluster.impl.MessageLoadBalancingType; import org.apache.activemq.artemis.core.server.group.impl.GroupingHandlerConfiguration; import org.apache.activemq.artemis.core.server.metrics.ActiveMQMetricsPlugin; @@ -624,6 +629,21 @@ public final class FileConfigurationParser extends XMLConfigurationUtil { parseDivertConfiguration(dvNode, config); } + + NodeList ccBalancers = e.getElementsByTagName("broker-balancers"); + + if (ccBalancers != null) { + NodeList ccBalancer = e.getElementsByTagName("broker-balancer"); + + if (ccBalancer != null) { + for (int i = 0; i < ccBalancer.getLength(); i++) { + Element ccNode = (Element) ccBalancer.item(i); + + parseBalancerConfiguration(ccNode, config); + } + } + } + // Persistence config config.setLargeMessagesDirectory(getString(e, "large-messages-directory", config.getLargeMessagesDirectory(), Validators.NOT_NULL_OR_EMPTY)); @@ -2620,7 +2640,87 @@ public final class FileConfigurationParser extends XMLConfigurationUtil { mainConfig.getDivertConfigurations().add(config); } - /** + private void parseBalancerConfiguration(final Element e, final Configuration config) throws Exception { + BrokerBalancerConfiguration brokerBalancerConfiguration = new BrokerBalancerConfiguration(); + + brokerBalancerConfiguration.setName(e.getAttribute("name")); + + brokerBalancerConfiguration.setTargetKey(TargetKey.valueOf(getString(e, "target-key", brokerBalancerConfiguration.getTargetKey().name(), Validators.TARGET_KEY))); + + brokerBalancerConfiguration.setTargetKeyFilter(getString(e, "target-key-filter", brokerBalancerConfiguration.getTargetKeyFilter(), Validators.NO_CHECK)); + + brokerBalancerConfiguration.setLocalTargetFilter(getString(e, "local-target-filter", brokerBalancerConfiguration.getLocalTargetFilter(), Validators.NO_CHECK)); + + brokerBalancerConfiguration.setCacheTimeout(getInteger(e, "cache-timeout", + brokerBalancerConfiguration.getCacheTimeout(), Validators.MINUS_ONE_OR_GE_ZERO)); + + PolicyConfiguration policyConfiguration = null; + PoolConfiguration poolConfiguration = null; + NodeList children = e.getChildNodes(); + + for (int j = 0; j < children.getLength(); j++) { + Node child = children.item(j); + + if (child.getNodeName().equals("policy")) { + policyConfiguration = new PolicyConfiguration(); + parsePolicyConfiguration((Element)child, policyConfiguration); + brokerBalancerConfiguration.setPolicyConfiguration(policyConfiguration); + } else if (child.getNodeName().equals("pool")) { + poolConfiguration = new PoolConfiguration(); + parsePoolConfiguration((Element) child, config, poolConfiguration); + brokerBalancerConfiguration.setPoolConfiguration(poolConfiguration); + } + } + + config.getBalancerConfigurations().add(brokerBalancerConfiguration); + } + + private void parsePolicyConfiguration(final Element e, final PolicyConfiguration policyConfiguration) throws ClassNotFoundException { + String name = e.getAttribute("name"); + + PolicyFactoryResolver.getInstance().resolve(name); + + policyConfiguration.setName(name); + + policyConfiguration.setProperties(getMapOfChildPropertyElements(e)); + } + + private void parsePoolConfiguration(final Element e, final Configuration config, final PoolConfiguration poolConfiguration) throws Exception { + poolConfiguration.setUsername(getString(e, "username", null, Validators.NO_CHECK)); + + String password = getString(e, "password", null, Validators.NO_CHECK); + poolConfiguration.setPassword(password != null ? PasswordMaskingUtil.resolveMask( + config.isMaskPassword(), password, config.getPasswordCodec()) : null); + + poolConfiguration.setCheckPeriod(getInteger(e, "check-period", + poolConfiguration.getCheckPeriod(), Validators.GT_ZERO)); + + poolConfiguration.setQuorumSize(getInteger(e, "quorum-size", + poolConfiguration.getQuorumSize(), Validators.GT_ZERO)); + + poolConfiguration.setQuorumTimeout(getInteger(e, "quorum-timeout", + poolConfiguration.getQuorumTimeout(), Validators.GE_ZERO)); + + poolConfiguration.setLocalTargetEnabled(getBoolean(e, "local-target-enabled", poolConfiguration.isLocalTargetEnabled())); + + poolConfiguration.setClusterConnection(getString(e, "cluster-connection", null, Validators.NO_CHECK)); + + NodeList children = e.getChildNodes(); + + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + + if (child.getNodeName().equals("discovery-group-ref")) { + poolConfiguration.setDiscoveryGroupName(child.getAttributes().getNamedItem("discovery-group-name").getNodeValue()); + } else if (child.getNodeName().equals("static-connectors")) { + List staticConnectorNames = new ArrayList<>(); + getStaticConnectors(staticConnectorNames, child); + poolConfiguration.setStaticConnectors(staticConnectorNames); + } + } + } + + /**RedirectConfiguration * @param e */ protected void parseWildcardConfiguration(final Element e, final Configuration mainConfig) { diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/management/impl/BrokerBalancerControlImpl.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/management/impl/BrokerBalancerControlImpl.java new file mode 100644 index 0000000000..72963bb0c4 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/management/impl/BrokerBalancerControlImpl.java @@ -0,0 +1,160 @@ +/** + * 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.activemq.artemis.core.management.impl; + +import org.apache.activemq.artemis.api.core.TransportConfiguration; +import org.apache.activemq.artemis.api.core.management.BrokerBalancerControl; +import org.apache.activemq.artemis.core.persistence.StorageManager; +import org.apache.activemq.artemis.core.server.balancing.BrokerBalancer; +import org.apache.activemq.artemis.core.server.balancing.targets.Target; +import org.apache.activemq.artemis.utils.JsonLoader; + +import javax.json.JsonObjectBuilder; +import javax.management.MBeanAttributeInfo; +import javax.management.MBeanOperationInfo; +import javax.management.NotCompliantMBeanException; +import javax.management.openmbean.CompositeData; +import javax.management.openmbean.CompositeDataSupport; +import javax.management.openmbean.CompositeType; +import javax.management.openmbean.OpenDataException; +import javax.management.openmbean.OpenType; +import javax.management.openmbean.SimpleType; +import javax.management.openmbean.TabularData; +import javax.management.openmbean.TabularDataSupport; +import javax.management.openmbean.TabularType; +import java.util.Map; + +public class BrokerBalancerControlImpl extends AbstractControl implements BrokerBalancerControl { + private final BrokerBalancer balancer; + + + private static CompositeType parameterType; + + private static TabularType parametersType; + + private static CompositeType transportConfigurationType; + + private static CompositeType targetType; + + + public BrokerBalancerControlImpl(final BrokerBalancer balancer, final StorageManager storageManager) throws NotCompliantMBeanException { + super(BrokerBalancerControl.class, storageManager); + this.balancer = balancer; + } + + @Override + public CompositeData getTarget(String key) throws Exception { + Target target = balancer.getTarget(key); + + if (target != null) { + CompositeData connectorData = null; + TransportConfiguration connector = target.getConnector(); + + if (connector != null) { + TabularData paramsData = new TabularDataSupport(getParametersType()); + for (Map.Entry param : connector.getParams().entrySet()) { + paramsData.put(new CompositeDataSupport(getParameterType(), new String[]{"key", "value"}, + new Object[]{param.getKey(), param == null ? param : param.getValue().toString()})); + } + + connectorData = new CompositeDataSupport(getTransportConfigurationType(), + new String[]{"name", "factoryClassName", "params"}, + new Object[]{connector.getName(), connector.getFactoryClassName(), paramsData}); + } + + CompositeData targetData = new CompositeDataSupport(getTargetCompositeType(), + new String[]{"nodeID", "local", "connector"}, + new Object[]{target.getNodeID(), target.isLocal(), connectorData}); + + return targetData; + } + + return null; + } + + @Override + public String getTargetAsJSON(String key) { + Target target = balancer.getTarget(key); + + if (target != null) { + TransportConfiguration connector = target.getConnector(); + + JsonObjectBuilder targetDataBuilder = JsonLoader.createObjectBuilder() + .add("nodeID", target.getNodeID()) + .add("local", target.isLocal()); + + if (connector == null) { + targetDataBuilder.addNull("connector"); + } else { + targetDataBuilder.add("connector", connector.toJson()); + } + + return targetDataBuilder.build().toString(); + } + + return null; + } + + @Override + protected MBeanOperationInfo[] fillMBeanOperationInfo() { + return MBeanInfoHelper.getMBeanOperationsInfo(BrokerBalancerControl.class); + } + + @Override + protected MBeanAttributeInfo[] fillMBeanAttributeInfo() { + return MBeanInfoHelper.getMBeanAttributesInfo(BrokerBalancerControl.class); + } + + + private CompositeType getParameterType() throws OpenDataException { + if (parameterType == null) { + parameterType = new CompositeType("java.util.Map.Entry", + "Parameter", new String[]{"key", "value"}, new String[]{"Parameter key", "Parameter value"}, + new OpenType[]{SimpleType.STRING, SimpleType.STRING}); + } + return parameterType; + } + + private TabularType getParametersType() throws OpenDataException { + if (parametersType == null) { + parametersType = new TabularType("java.util.Map", + "Parameters", getParameterType(), new String[]{"key"}); + } + return parametersType; + } + + private CompositeType getTransportConfigurationType() throws OpenDataException { + if (transportConfigurationType == null) { + transportConfigurationType = new CompositeType(TransportConfiguration.class.getName(), + "TransportConfiguration", new String[]{"name", "factoryClassName", "params"}, + new String[]{"TransportConfiguration name", "TransportConfiguration factoryClassName", "TransportConfiguration params"}, + new OpenType[]{SimpleType.STRING, SimpleType.STRING, getParametersType()}); + } + return transportConfigurationType; + } + + private CompositeType getTargetCompositeType() throws OpenDataException { + if (targetType == null) { + targetType = new CompositeType(Target.class.getName(), + "Target", new String[]{"nodeID", "local", "connector"}, + new String[]{"Target nodeID", "Target local", "Target connector"}, + new OpenType[]{SimpleType.STRING, SimpleType.BOOLEAN, getTransportConfigurationType()}); + } + return targetType; + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/management/impl/view/ConsumerView.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/management/impl/view/ConsumerView.java index 34eeffb74c..17919cada0 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/management/impl/view/ConsumerView.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/management/impl/view/ConsumerView.java @@ -55,7 +55,7 @@ public class ConsumerView extends ActiveMQAbstractView { String consumerClientID = consumer.getConnectionClientID(); if (consumerClientID == null && session.getMetaData(ClientSession.JMS_SESSION_IDENTIFIER_PROPERTY) != null) { //for the special case for JMS - consumerClientID = session.getMetaData("jms-client-id"); + consumerClientID = session.getMetaData(ClientSession.JMS_SESSION_CLIENT_ID_PROPERTY); } JsonObjectBuilder obj = JsonLoader.createObjectBuilder() diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/management/impl/view/ProducerView.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/management/impl/view/ProducerView.java index ac3fcf6ddd..fb4a6d6ddf 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/management/impl/view/ProducerView.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/management/impl/view/ProducerView.java @@ -54,7 +54,7 @@ public class ProducerView extends ActiveMQAbstractView { String sessionClientID = session.getRemotingConnection().getClientID(); //for the special case for JMS if (sessionClientID == null && session.getMetaData(ClientSession.JMS_SESSION_IDENTIFIER_PROPERTY) != null) { - sessionClientID = session.getMetaData("jms-client-id"); + sessionClientID = session.getMetaData(ClientSession.JMS_SESSION_CLIENT_ID_PROPERTY); } JsonObjectBuilder obj = JsonLoader.createObjectBuilder() diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/protocol/ProtocolHandler.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/protocol/ProtocolHandler.java index 6bd0d2b4d2..4286eac032 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/protocol/ProtocolHandler.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/protocol/ProtocolHandler.java @@ -44,6 +44,7 @@ import org.apache.activemq.artemis.core.remoting.impl.netty.HttpAcceptorHandler; import org.apache.activemq.artemis.core.remoting.impl.netty.HttpKeepAliveRunnable; import org.apache.activemq.artemis.core.remoting.impl.netty.NettyAcceptor; import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnector; +import org.apache.activemq.artemis.core.remoting.impl.netty.NettySNIHostnameHandler; import org.apache.activemq.artemis.core.remoting.impl.netty.NettyServerConnection; import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants; import org.apache.activemq.artemis.core.server.ActiveMQServerLogger; @@ -111,6 +112,7 @@ public class ProtocolHandler { private int handshakeTimeout; + private NettySNIHostnameHandler nettySNIHostnameHandler; ProtocolDecoder(boolean http, boolean httpEnabled) { this.http = http; @@ -120,6 +122,8 @@ public class ProtocolHandler { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { + nettySNIHostnameHandler = ctx.pipeline().get(NettySNIHostnameHandler.class); + if (handshakeTimeout > 0) { timeoutFuture = scheduledThreadPool.schedule( () -> { ActiveMQServerLogger.LOGGER.handshakeTimeout(handshakeTimeout, nettyAcceptor.getName(), ctx.channel().remoteAddress().toString()); @@ -220,6 +224,7 @@ public class ProtocolHandler { protocolManagerToUse.addChannelHandlers(pipeline); pipeline.addLast("handler", channelHandler); NettyServerConnection connection = channelHandler.createConnection(ctx, protocolToUse, httpEnabled); + connection.setSNIHostname(nettySNIHostnameHandler != null ? nettySNIHostnameHandler.getHostname() : null); protocolManagerToUse.handshake(connection, new ChannelBufferWrapper(in)); pipeline.remove(this); diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/ActiveMQPacketHandler.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/ActiveMQPacketHandler.java index e90bcb2208..e63dbf5fc4 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/ActiveMQPacketHandler.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/ActiveMQPacketHandler.java @@ -37,6 +37,7 @@ import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.CheckFailo import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.CheckFailoverReplyMessage; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.CreateQueueMessage; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.CreateSessionMessage; +import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.CreateSessionMessage_V2; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.CreateSessionResponseMessage; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.ReattachSessionMessage; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.ReattachSessionResponseMessage; @@ -89,7 +90,8 @@ public class ActiveMQPacketHandler implements ChannelHandler { } switch (type) { - case PacketImpl.CREATESESSION: { + case PacketImpl.CREATESESSION: + case PacketImpl.CREATESESSION_V2: { CreateSessionMessage request = (CreateSessionMessage) packet; handleCreateSession(request); @@ -157,6 +159,14 @@ public class ActiveMQPacketHandler implements ChannelHandler { ActiveMQServerLogger.LOGGER.incompatibleVersionAfterConnect(request.getVersion(), connection.getChannelVersion()); } + if (request instanceof CreateSessionMessage_V2) { + connection.setClientID(((CreateSessionMessage_V2) request).getClientID()); + } + + if (connection.getTransportConnection().getRedirectTo() != null) { + protocolManager.getRedirectHandler().redirect(connection, request); + } + Channel channel = connection.getChannel(request.getSessionChannelID(), request.getWindowSize()); ActiveMQPrincipal activeMQPrincipal = null; @@ -187,6 +197,8 @@ public class ActiveMQPacketHandler implements ChannelHandler { if (e.getType() == ActiveMQExceptionType.INCOMPATIBLE_CLIENT_SERVER_VERSIONS) { incompatibleVersion = true; logger.debug("Sending ActiveMQException after Incompatible client", e); + } else if (e.getType() == ActiveMQExceptionType.REDIRECTED) { + logger.debug("Sending ActiveMQException after redirected client", e); } else { ActiveMQServerLogger.LOGGER.failedToCreateSession(e); } diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/ActiveMQRedirectContext.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/ActiveMQRedirectContext.java new file mode 100644 index 0000000000..575c9c0475 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/ActiveMQRedirectContext.java @@ -0,0 +1,28 @@ +/** + * 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.activemq.artemis.core.protocol.core.impl; + +import org.apache.activemq.artemis.core.protocol.core.CoreRemotingConnection; +import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.CreateSessionMessage; +import org.apache.activemq.artemis.core.server.balancing.RedirectContext; + +public class ActiveMQRedirectContext extends RedirectContext { + public ActiveMQRedirectContext(CoreRemotingConnection connection, CreateSessionMessage message) { + super(connection, connection.getClientID(), message.getUsername()); + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/ActiveMQRedirectHandler.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/ActiveMQRedirectHandler.java new file mode 100644 index 0000000000..937bd273bd --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/ActiveMQRedirectHandler.java @@ -0,0 +1,52 @@ +/** + * 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.activemq.artemis.core.protocol.core.impl; + +import org.apache.activemq.artemis.api.core.DisconnectReason; +import org.apache.activemq.artemis.core.protocol.core.CoreRemotingConnection; +import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.CreateSessionMessage; +import org.apache.activemq.artemis.core.server.ActiveMQMessageBundle; +import org.apache.activemq.artemis.core.server.ActiveMQServer; +import org.apache.activemq.artemis.core.server.balancing.RedirectHandler; + +public class ActiveMQRedirectHandler extends RedirectHandler { + + public ActiveMQRedirectHandler(ActiveMQServer server) { + super(server); + } + + public boolean redirect(CoreRemotingConnection connection, CreateSessionMessage message) throws Exception { + if (!connection.isVersionSupportRedirect()) { + throw ActiveMQMessageBundle.BUNDLE.incompatibleClientServer(); + } + + return redirect(new ActiveMQRedirectContext(connection, message)); + } + + @Override + public void cannotRedirect(ActiveMQRedirectContext context) throws Exception { + throw ActiveMQMessageBundle.BUNDLE.cannotRedirect(); + } + + @Override + public void redirectTo(ActiveMQRedirectContext context) throws Exception { + context.getConnection().disconnect(DisconnectReason.REDIRECT, context.getTarget().getNodeID(), context.getTarget().getConnector()); + + throw ActiveMQMessageBundle.BUNDLE.redirectConnection(context.getTarget().getConnector()); + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/CoreProtocolManager.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/CoreProtocolManager.java index 84baf255c1..c56513b171 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/CoreProtocolManager.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/protocol/core/impl/CoreProtocolManager.java @@ -55,6 +55,7 @@ import org.apache.activemq.artemis.core.protocol.core.impl.ChannelImpl.CHANNEL_I import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.ClusterTopologyChangeMessage; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.ClusterTopologyChangeMessage_V2; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.ClusterTopologyChangeMessage_V3; +import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.ClusterTopologyChangeMessage_V4; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.FederationDownstreamConnectMessage; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.Ping; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.SubscribeClusterTopologyUpdatesMessage; @@ -72,7 +73,7 @@ import org.apache.activemq.artemis.spi.core.remoting.Acceptor; import org.apache.activemq.artemis.spi.core.remoting.Connection; import org.jboss.logging.Logger; -public class CoreProtocolManager implements ProtocolManager { +public class CoreProtocolManager implements ProtocolManager { private static final Logger logger = Logger.getLogger(CoreProtocolManager.class); @@ -90,6 +91,8 @@ public class CoreProtocolManager implements ProtocolManager { private String securityDomain; + private final ActiveMQRedirectHandler redirectHandler; + public CoreProtocolManager(final CoreProtocolManagerFactory factory, final ActiveMQServer server, final List incomingInterceptors, @@ -101,6 +104,8 @@ public class CoreProtocolManager implements ProtocolManager { this.incomingInterceptors = incomingInterceptors; this.outgoingInterceptors = outgoingInterceptors; + + this.redirectHandler = new ActiveMQRedirectHandler(server); } @Override @@ -233,6 +238,11 @@ public class CoreProtocolManager implements ProtocolManager { return securityDomain; } + @Override + public ActiveMQRedirectHandler getRedirectHandler() { + return redirectHandler; + } + private boolean isArtemis(ActiveMQBuffer buffer) { return buffer.getByte(0) == 'A' && buffer.getByte(1) == 'R' && @@ -304,7 +314,13 @@ public class CoreProtocolManager implements ProtocolManager { entry.connectionExecutor.execute(new Runnable() { @Override public void run() { - if (channel0.supports(PacketImpl.CLUSTER_TOPOLOGY_V3)) { + if (channel0.supports(PacketImpl.CLUSTER_TOPOLOGY_V4)) { + channel0.send(new ClusterTopologyChangeMessage_V4( + topologyMember.getUniqueEventID(), nodeID, + topologyMember.getBackupGroupName(), + topologyMember.getScaleDownGroupName(), connectorPair, + last, server.getVersion().getIncrementingVersion())); + } else if (channel0.supports(PacketImpl.CLUSTER_TOPOLOGY_V3)) { channel0.send(new ClusterTopologyChangeMessage_V3( topologyMember.getUniqueEventID(), nodeID, topologyMember.getBackupGroupName(), @@ -380,7 +396,10 @@ public class CoreProtocolManager implements ProtocolManager { String nodeId = server.getNodeID().toString(); Pair emptyConfig = new Pair<>( null, null); - if (channel0.supports(PacketImpl.CLUSTER_TOPOLOGY_V2)) { + if (channel0.supports(PacketImpl.CLUSTER_TOPOLOGY_V4)) { + channel0.send(new ClusterTopologyChangeMessage_V4(System.currentTimeMillis(), nodeId, + null, null, emptyConfig, true, server.getVersion().getIncrementingVersion())); + } else if (channel0.supports(PacketImpl.CLUSTER_TOPOLOGY_V2)) { channel0.send( new ClusterTopologyChangeMessage_V2(System.currentTimeMillis(), nodeId, null, emptyConfig, true)); diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/remoting/impl/netty/NettyAcceptor.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/remoting/impl/netty/NettyAcceptor.java index 72c732f620..2005c2dad6 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/remoting/impl/netty/NettyAcceptor.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/remoting/impl/netty/NettyAcceptor.java @@ -237,6 +237,8 @@ public class NettyAcceptor extends AbstractAcceptor { private final boolean autoStart; + private final String redirectTo; + final AtomicBoolean warningPrinted = new AtomicBoolean(false); final Executor failureExecutor; @@ -378,6 +380,8 @@ public class NettyAcceptor extends AbstractAcceptor { connectionsAllowed = ConfigurationHelper.getLongProperty(TransportConstants.CONNECTIONS_ALLOWED, TransportConstants.DEFAULT_CONNECTIONS_ALLOWED, configuration); autoStart = ConfigurationHelper.getBooleanProperty(TransportConstants.AUTO_START, TransportConstants.DEFAULT_AUTO_START, configuration); + + redirectTo = ConfigurationHelper.getStringProperty(TransportConstants.REDIRECT_TO, TransportConstants.DEFAULT_REDIRECT_TO, configuration); } private Object loadSSLContext() { @@ -460,6 +464,7 @@ public class NettyAcceptor extends AbstractAcceptor { if (sslEnabled) { final Pair peerInfo = getPeerInfo(channel); try { + pipeline.addLast("sni", new NettySNIHostnameHandler()); pipeline.addLast("ssl", getSslHandler(channel.alloc(), peerInfo.getA(), peerInfo.getB())); pipeline.addLast("sslHandshakeExceptionHandler", new SslHandshakeExceptionHandler()); } catch (Exception e) { @@ -930,7 +935,7 @@ public class NettyAcceptor extends AbstractAcceptor { super.channelActive(ctx); Listener connectionListener = new Listener(); - NettyServerConnection nc = new NettyServerConnection(configuration, ctx.channel(), connectionListener, !httpEnabled && batchDelay > 0, directDeliver); + NettyServerConnection nc = new NettyServerConnection(configuration, ctx.channel(), connectionListener, !httpEnabled && batchDelay > 0, directDeliver, redirectTo); connectionListener.connectionCreated(NettyAcceptor.this, nc, protocolHandler.getProtocol(protocol)); diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/remoting/impl/netty/NettySNIHostnameHandler.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/remoting/impl/netty/NettySNIHostnameHandler.java new file mode 100644 index 0000000000..9345738a6e --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/remoting/impl/netty/NettySNIHostnameHandler.java @@ -0,0 +1,42 @@ +/** + * 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.activemq.artemis.core.remoting.impl.netty; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.ssl.AbstractSniHandler; +import io.netty.util.concurrent.Future; + +public class NettySNIHostnameHandler extends AbstractSniHandler { + + private String hostname = null; + + public String getHostname() { + return hostname; + } + + @Override + protected Future lookup(ChannelHandlerContext ctx, String hostname) throws Exception { + return ctx.executor().newPromise().setSuccess(null); + } + + @Override + protected void onLookupComplete(ChannelHandlerContext ctx, String hostname, Future future) throws Exception { + this.hostname = hostname; + ctx.pipeline().remove(this); + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/remoting/impl/netty/NettyServerConnection.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/remoting/impl/netty/NettyServerConnection.java index f9e1b3d2f2..7fadb1864a 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/remoting/impl/netty/NettyServerConnection.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/remoting/impl/netty/NettyServerConnection.java @@ -23,12 +23,32 @@ import org.apache.activemq.artemis.spi.core.remoting.ServerConnectionLifeCycleLi public class NettyServerConnection extends NettyConnection { + private String sniHostname; + + private final String redirectTo; + public NettyServerConnection(Map configuration, Channel channel, ServerConnectionLifeCycleListener listener, boolean batchingEnabled, - boolean directDeliver) { + boolean directDeliver, + String redirectTo) { super(configuration, channel, listener, batchingEnabled, directDeliver); + + this.redirectTo = redirectTo; } + @Override + public String getSNIHostName() { + return sniHostname; + } + + public void setSNIHostname(String sniHostname) { + this.sniHostname = sniHostname; + } + + @Override + public String getRedirectTo() { + return redirectTo; + } } diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQMessageBundle.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQMessageBundle.java index e37fddd82f..f1e6170920 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQMessageBundle.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQMessageBundle.java @@ -39,6 +39,7 @@ import org.apache.activemq.artemis.api.core.ActiveMQInvalidTransientQueueUseExce import org.apache.activemq.artemis.api.core.ActiveMQNonExistentQueueException; import org.apache.activemq.artemis.api.core.ActiveMQQueueExistsException; import org.apache.activemq.artemis.api.core.ActiveMQQueueMaxConsumerLimitReached; +import org.apache.activemq.artemis.api.core.ActiveMQRedirectedException; import org.apache.activemq.artemis.api.core.ActiveMQReplicationTimeooutException; import org.apache.activemq.artemis.api.core.ActiveMQSecurityException; import org.apache.activemq.artemis.api.core.ActiveMQSessionCreationException; @@ -46,6 +47,7 @@ import org.apache.activemq.artemis.api.core.ActiveMQUnexpectedRoutingTypeForAddr import org.apache.activemq.artemis.api.core.DiscoveryGroupConfiguration; import org.apache.activemq.artemis.api.core.RoutingType; import org.apache.activemq.artemis.api.core.SimpleString; +import org.apache.activemq.artemis.api.core.TransportConfiguration; import org.apache.activemq.artemis.core.io.SequentialFile; import org.apache.activemq.artemis.core.postoffice.Binding; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.ReplicationSyncFileMessage; @@ -504,4 +506,13 @@ public interface ActiveMQMessageBundle { @Message(id = 229235, value = "Incompatible binding with name {0} already exists: {1}", format = Message.Format.MESSAGE_FORMAT) ActiveMQIllegalStateException bindingAlreadyExists(String name, String binding); + + @Message(id = 229236, value = "Invalid target key {0}", format = Message.Format.MESSAGE_FORMAT) + IllegalArgumentException invalidTargetKey(String val); + + @Message(id = 229237, value = "Connection redirected to {0}", format = Message.Format.MESSAGE_FORMAT) + ActiveMQRedirectedException redirectConnection(TransportConfiguration connector); + + @Message(id = 229238, value = "No target to redirect the connection") + ActiveMQRedirectedException cannotRedirect(); } diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServer.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServer.java index e3249793e6..27cb0d6d1b 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServer.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServer.java @@ -67,6 +67,7 @@ import org.apache.activemq.artemis.core.server.plugin.ActiveMQServerMessagePlugi import org.apache.activemq.artemis.core.server.plugin.ActiveMQServerQueuePlugin; import org.apache.activemq.artemis.core.server.plugin.ActiveMQServerResourcePlugin; import org.apache.activemq.artemis.core.server.plugin.ActiveMQServerSessionPlugin; +import org.apache.activemq.artemis.core.server.balancing.BrokerBalancerManager; import org.apache.activemq.artemis.core.server.reload.ReloadManager; import org.apache.activemq.artemis.core.settings.HierarchicalRepository; import org.apache.activemq.artemis.core.settings.impl.AddressSettings; @@ -943,4 +944,6 @@ public interface ActiveMQServer extends ServiceComponent { double getDiskStoreUsage(); void reloadConfigurationFile() throws Exception; + + BrokerBalancerManager getBalancerManager(); } diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServerLogger.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServerLogger.java index 7d46a0d6a6..bb72f57ebd 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServerLogger.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServerLogger.java @@ -40,6 +40,7 @@ import org.apache.activemq.artemis.core.persistence.OperationContext; import org.apache.activemq.artemis.core.protocol.core.Packet; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.BackupReplicationStartFailedMessage; import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants; +import org.apache.activemq.artemis.core.server.balancing.targets.Target; import org.apache.activemq.artemis.core.server.cluster.Bridge; import org.apache.activemq.artemis.core.server.cluster.impl.BridgeImpl; import org.apache.activemq.artemis.core.server.cluster.impl.ClusterConnectionImpl; @@ -47,6 +48,7 @@ import org.apache.activemq.artemis.core.server.cluster.qourum.ServerConnectVote; import org.apache.activemq.artemis.core.server.impl.ActiveMQServerImpl; import org.apache.activemq.artemis.core.server.impl.ServerSessionImpl; import org.apache.activemq.artemis.core.server.management.Notification; +import org.apache.activemq.artemis.spi.core.remoting.Connection; import org.jboss.logging.BasicLogger; import org.jboss.logging.Logger; import org.jboss.logging.annotations.Cause; @@ -451,6 +453,14 @@ public interface ActiveMQServerLogger extends BasicLogger { @Message(id = 221084, value = "Requested {0} quorum votes", format = Message.Format.MESSAGE_FORMAT) void requestedQuorumVotes(int vote); + @LogMessage(level = Logger.Level.INFO) + @Message(id = 221085, value = "Redirect {0} to {1}", format = Message.Format.MESSAGE_FORMAT) + void redirectClientConnection(Connection connection, Target target); + + @LogMessage(level = Logger.Level.INFO) + @Message(id = 221086, value = "Cannot redirect {0}", format = Message.Format.MESSAGE_FORMAT) + void cannotRedirectClientConnection(Connection connection); + @LogMessage(level = Logger.Level.WARN) @Message(id = 222000, value = "ActiveMQServer is being finalized and has not been stopped. Please remember to stop the server before letting it go out of scope", format = Message.Format.MESSAGE_FORMAT) @@ -2156,4 +2166,8 @@ public interface ActiveMQServerLogger extends BasicLogger { @LogMessage(level = Logger.Level.WARN) @Message(id = 224108, value = "Stopped paging on address ''{0}''; size is currently: {1} bytes; max-size-bytes: {2}; global-size-bytes: {3}", format = Message.Format.MESSAGE_FORMAT) void pageStoreStop(SimpleString storeName, long addressSize, long maxSize, long globalMaxSize); + + @LogMessage(level = Logger.Level.WARN) + @Message(id = 224109, value = "BrokerBalancer {0} not found", format = Message.Format.MESSAGE_FORMAT) + void brokerBalancerNotFound(String name); } diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/BrokerBalancer.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/BrokerBalancer.java new file mode 100644 index 0000000000..93e38b1613 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/BrokerBalancer.java @@ -0,0 +1,193 @@ +/** + * 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.activemq.artemis.core.server.balancing; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration; +import org.apache.activemq.artemis.core.server.ActiveMQComponent; +import org.apache.activemq.artemis.core.server.balancing.policies.Policy; +import org.apache.activemq.artemis.core.server.balancing.pools.Pool; +import org.apache.activemq.artemis.core.server.balancing.targets.Target; +import org.apache.activemq.artemis.core.server.balancing.targets.TargetKey; +import org.apache.activemq.artemis.core.server.balancing.targets.TargetKeyResolver; +import org.apache.activemq.artemis.spi.core.remoting.Connection; +import org.jboss.logging.Logger; + +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; + +public class BrokerBalancer implements ActiveMQComponent { + private static final Logger logger = Logger.getLogger(BrokerBalancer.class); + + + public static final String CLIENT_ID_PREFIX = ActiveMQDefaultConfiguration.DEFAULT_INTERNAL_NAMING_PREFIX + "balancer.client."; + + + private final String name; + + private final TargetKey targetKey; + + private final TargetKeyResolver targetKeyResolver; + + private final Target localTarget; + + private final Pattern localTargetFilter; + + private final Pool pool; + + private final Policy policy; + + private final Cache cache; + + private volatile boolean started = false; + + public String getName() { + return name; + } + + public TargetKey getTargetKey() { + return targetKey; + } + + public Target getLocalTarget() { + return localTarget; + } + + public String getLocalTargetFilter() { + return localTargetFilter != null ? localTargetFilter.pattern() : null; + } + + public Pool getPool() { + return pool; + } + + public Policy getPolicy() { + return policy; + } + + public Cache getCache() { + return cache; + } + + @Override + public boolean isStarted() { + return started; + } + + + public BrokerBalancer(final String name, final TargetKey targetKey, final String targetKeyFilter, final Target localTarget, final String localTargetFilter, final Pool pool, final Policy policy, final int cacheTimeout) { + this.name = name; + + this.targetKey = targetKey; + + this.targetKeyResolver = new TargetKeyResolver(targetKey, targetKeyFilter); + + this.localTarget = localTarget; + + this.localTargetFilter = localTargetFilter != null ? Pattern.compile(localTargetFilter) : null; + + this.pool = pool; + + this.policy = policy; + + if (cacheTimeout == -1) { + this.cache = CacheBuilder.newBuilder().build(); + } else if (cacheTimeout > 0) { + this.cache = CacheBuilder.newBuilder().expireAfterAccess(cacheTimeout, TimeUnit.MILLISECONDS).build(); + } else { + this.cache = null; + } + } + + @Override + public void start() throws Exception { + pool.start(); + + started = true; + } + + @Override + public void stop() throws Exception { + started = false; + + pool.stop(); + } + + public Target getTarget(Connection connection, String clientID, String username) { + if (clientID != null && clientID.startsWith(BrokerBalancer.CLIENT_ID_PREFIX)) { + if (logger.isDebugEnabled()) { + logger.debug("The clientID [" + clientID + "] starts with BrokerBalancer.CLIENT_ID_PREFIX"); + } + + return localTarget; + } + + return getTarget(targetKeyResolver.resolve(connection, clientID, username)); + } + + public Target getTarget(String key) { + + if (this.localTargetFilter != null && this.localTargetFilter.matcher(key).matches()) { + if (logger.isDebugEnabled()) { + logger.debug("The " + targetKey + "[" + key + "] matches the localTargetFilter " + localTargetFilter.pattern()); + } + + return localTarget; + } + + Target target = null; + + if (cache != null) { + target = cache.getIfPresent(key); + } + + if (target != null) { + if (pool.isTargetReady(target)) { + if (logger.isDebugEnabled()) { + logger.debug("The cache returns [" + target + "] ready for " + targetKey + "[" + key + "]"); + } + + return target; + } + + if (logger.isDebugEnabled()) { + logger.debug("The cache returns [" + target + "] not ready for " + targetKey + "[" + key + "]"); + } + } + + List targets = pool.getTargets(); + + target = policy.selectTarget(targets, key); + + if (logger.isDebugEnabled()) { + logger.debug("The policy selects [" + target + "] from " + targets + " for " + targetKey + "[" + key + "]"); + } + + if (target != null && cache != null) { + if (logger.isDebugEnabled()) { + logger.debug("Caching " + targetKey + "[" + key + "] for [" + target + "]"); + } + + cache.put(key, target); + } + + return target; + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/BrokerBalancerManager.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/BrokerBalancerManager.java new file mode 100644 index 0000000000..fc5fba65b9 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/BrokerBalancerManager.java @@ -0,0 +1,191 @@ +/** + * 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.activemq.artemis.core.server.balancing; + +import org.apache.activemq.artemis.api.core.DiscoveryGroupConfiguration; +import org.apache.activemq.artemis.api.core.TransportConfiguration; +import org.apache.activemq.artemis.core.cluster.DiscoveryGroup; +import org.apache.activemq.artemis.core.config.balancing.BrokerBalancerConfiguration; +import org.apache.activemq.artemis.core.config.balancing.PolicyConfiguration; +import org.apache.activemq.artemis.core.config.balancing.PoolConfiguration; +import org.apache.activemq.artemis.core.config.Configuration; +import org.apache.activemq.artemis.core.server.ActiveMQComponent; +import org.apache.activemq.artemis.core.server.ActiveMQServer; +import org.apache.activemq.artemis.core.server.balancing.policies.Policy; +import org.apache.activemq.artemis.core.server.balancing.policies.PolicyFactory; +import org.apache.activemq.artemis.core.server.balancing.policies.PolicyFactoryResolver; +import org.apache.activemq.artemis.core.server.balancing.pools.ClusterPool; +import org.apache.activemq.artemis.core.server.balancing.pools.DiscoveryGroupService; +import org.apache.activemq.artemis.core.server.balancing.pools.DiscoveryPool; +import org.apache.activemq.artemis.core.server.balancing.pools.DiscoveryService; +import org.apache.activemq.artemis.core.server.balancing.pools.Pool; +import org.apache.activemq.artemis.core.server.balancing.pools.StaticPool; +import org.apache.activemq.artemis.core.server.balancing.targets.ActiveMQTargetFactory; +import org.apache.activemq.artemis.core.server.balancing.targets.LocalTarget; +import org.apache.activemq.artemis.core.server.balancing.targets.Target; +import org.apache.activemq.artemis.core.server.balancing.targets.TargetFactory; +import org.apache.activemq.artemis.core.server.cluster.ClusterConnection; +import org.jboss.logging.Logger; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ScheduledExecutorService; + +public final class BrokerBalancerManager implements ActiveMQComponent { + private static final Logger logger = Logger.getLogger(BrokerBalancerManager.class); + + + private final Configuration config; + + private final ActiveMQServer server; + + private final ScheduledExecutorService scheduledExecutor; + + private volatile boolean started = false; + + private Map balancerControllers = new HashMap<>(); + + + @Override + public boolean isStarted() { + return started; + } + + + public BrokerBalancerManager(final Configuration config, final ActiveMQServer server, ScheduledExecutorService scheduledExecutor) { + this.config = config; + this.server = server; + this.scheduledExecutor = scheduledExecutor; + } + + public void deploy() throws Exception { + for (BrokerBalancerConfiguration balancerConfig : config.getBalancerConfigurations()) { + deployBrokerBalancer(balancerConfig); + } + } + + public void deployBrokerBalancer(BrokerBalancerConfiguration config) throws Exception { + if (logger.isDebugEnabled()) { + logger.debugf("Deploying BrokerBalancer " + config.getName()); + } + + Target localTarget = new LocalTarget(null, server); + + Pool pool = deployPool(config.getPoolConfiguration(), localTarget); + + Policy policy = deployPolicy(config.getPolicyConfiguration(), pool); + + BrokerBalancer balancer = new BrokerBalancer(config.getName(), config.getTargetKey(), config.getTargetKeyFilter(), + localTarget, config.getLocalTargetFilter(), pool, policy, config.getCacheTimeout()); + + balancerControllers.put(balancer.getName(), balancer); + + server.getManagementService().registerBrokerBalancer(balancer); + } + + private Pool deployPool(PoolConfiguration config, Target localTarget) throws Exception { + Pool pool; + TargetFactory targetFactory = new ActiveMQTargetFactory(); + + targetFactory.setUsername(config.getUsername()); + targetFactory.setPassword(config.getPassword()); + + if (config.getClusterConnection() != null) { + ClusterConnection clusterConnection = server.getClusterManager() + .getClusterConnection(config.getClusterConnection()); + + pool = new ClusterPool(targetFactory, scheduledExecutor, config.getCheckPeriod(), clusterConnection); + } else if (config.getDiscoveryGroupName() != null) { + DiscoveryGroupConfiguration discoveryGroupConfiguration = server.getConfiguration(). + getDiscoveryGroupConfigurations().get(config.getDiscoveryGroupName()); + + DiscoveryService discoveryService = new DiscoveryGroupService(new DiscoveryGroup(server.getNodeID().toString(), config.getDiscoveryGroupName(), + discoveryGroupConfiguration.getRefreshTimeout(), discoveryGroupConfiguration.getBroadcastEndpointFactory(), null)); + + pool = new DiscoveryPool(targetFactory, scheduledExecutor, config.getCheckPeriod(), discoveryService); + } else if (config.getStaticConnectors() != null) { + Map connectorConfigurations = + server.getConfiguration().getConnectorConfigurations(); + + List staticConnectors = new ArrayList<>(); + for (String staticConnector : config.getStaticConnectors()) { + TransportConfiguration connector = connectorConfigurations.get(staticConnector); + + if (connector != null) { + staticConnectors.add(connector); + } else { + logger.warn("Static connector not found: " + config.isLocalTargetEnabled()); + } + } + + pool = new StaticPool(targetFactory, scheduledExecutor, config.getCheckPeriod(), staticConnectors); + } else { + throw new IllegalStateException("Pool configuration not valid"); + } + + pool.setUsername(config.getUsername()); + pool.setPassword(config.getPassword()); + pool.setQuorumSize(config.getQuorumSize()); + pool.setQuorumTimeout(config.getQuorumTimeout()); + + if (config.isLocalTargetEnabled()) { + pool.addTarget(localTarget); + } + + return pool; + } + + private Policy deployPolicy(PolicyConfiguration policyConfig, Pool pool) throws ClassNotFoundException { + PolicyFactory policyFactory = PolicyFactoryResolver.getInstance().resolve(policyConfig.getName()); + + Policy policy = policyFactory.createPolicy(policyConfig.getName()); + + policy.init(policyConfig.getProperties()); + + if (policy.getTargetProbe() != null) { + pool.addTargetProbe(policy.getTargetProbe()); + } + + return policy; + } + + public BrokerBalancer getBalancer(String name) { + return balancerControllers.get(name); + } + + @Override + public void start() throws Exception { + for (BrokerBalancer brokerBalancer : balancerControllers.values()) { + brokerBalancer.start(); + } + + started = true; + } + + @Override + public void stop() throws Exception { + started = false; + + for (BrokerBalancer balancer : balancerControllers.values()) { + balancer.stop(); + server.getManagementService().unregisterBrokerBalancer(balancer.getName()); + } + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/RedirectContext.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/RedirectContext.java new file mode 100644 index 0000000000..76a2a5437d --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/RedirectContext.java @@ -0,0 +1,57 @@ +/** + * 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.activemq.artemis.core.server.balancing; + +import org.apache.activemq.artemis.core.server.balancing.targets.Target; +import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection; + +public class RedirectContext { + private final RemotingConnection connection; + + private final String clientID; + + private final String username; + + private Target target; + + public RemotingConnection getConnection() { + return connection; + } + + public String getClientID() { + return clientID; + } + + public String getUsername() { + return username; + } + + public Target getTarget() { + return target; + } + + public void setTarget(Target target) { + this.target = target; + } + + public RedirectContext(RemotingConnection connection, String clientID, String username) { + this.connection = connection; + this.clientID = clientID; + this.username = username; + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/RedirectHandler.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/RedirectHandler.java new file mode 100644 index 0000000000..89ace13057 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/RedirectHandler.java @@ -0,0 +1,74 @@ +/** + * 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.activemq.artemis.core.server.balancing; + +import org.apache.activemq.artemis.core.server.ActiveMQServer; +import org.apache.activemq.artemis.core.server.ActiveMQServerLogger; +import org.apache.activemq.artemis.spi.core.remoting.Connection; + +public abstract class RedirectHandler { + private final ActiveMQServer server; + + + public ActiveMQServer getServer() { + return server; + } + + + protected RedirectHandler(ActiveMQServer server) { + this.server = server; + } + + protected abstract void cannotRedirect(T context) throws Exception; + + protected abstract void redirectTo(T context) throws Exception; + + protected boolean redirect(T context) throws Exception { + Connection transportConnection = context.getConnection().getTransportConnection(); + + BrokerBalancer brokerBalancer = getServer().getBalancerManager().getBalancer(transportConnection.getRedirectTo()); + + if (brokerBalancer == null) { + ActiveMQServerLogger.LOGGER.brokerBalancerNotFound(transportConnection.getRedirectTo()); + + cannotRedirect(context); + + return true; + } + + context.setTarget(brokerBalancer.getTarget(transportConnection, context.getClientID(), context.getUsername())); + + if (context.getTarget() == null) { + ActiveMQServerLogger.LOGGER.cannotRedirectClientConnection(transportConnection); + + cannotRedirect(context); + + return true; + } + + ActiveMQServerLogger.LOGGER.redirectClientConnection(transportConnection, context.getTarget()); + + if (!context.getTarget().isLocal()) { + redirectTo(context); + + return true; + } + + return false; + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/policies/AbstractPolicy.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/policies/AbstractPolicy.java new file mode 100644 index 0000000000..8cb7a92783 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/policies/AbstractPolicy.java @@ -0,0 +1,52 @@ +/** + * 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.activemq.artemis.core.server.balancing.policies; + +import org.apache.activemq.artemis.core.server.balancing.targets.TargetProbe; + +import java.util.Map; + +public abstract class AbstractPolicy implements Policy { + private final String name; + + private Map properties; + + @Override + public String getName() { + return name; + } + + @Override + public Map getProperties() { + return properties; + } + + @Override + public TargetProbe getTargetProbe() { + return null; + } + + @Override + public void init(Map properties) { + this.properties = properties; + } + + public AbstractPolicy(final String name) { + this.name = name; + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/policies/ConsistentHashPolicy.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/policies/ConsistentHashPolicy.java new file mode 100644 index 0000000000..77d4076539 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/policies/ConsistentHashPolicy.java @@ -0,0 +1,75 @@ +/** + * 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.activemq.artemis.core.server.balancing.policies; + +import org.apache.activemq.artemis.core.server.balancing.targets.Target; + +import java.util.List; +import java.util.Map; +import java.util.NavigableMap; +import java.util.TreeMap; + +public class ConsistentHashPolicy extends AbstractPolicy { + public static final String NAME = "CONSISTENT_HASH"; + + public ConsistentHashPolicy() { + super(NAME); + } + + protected ConsistentHashPolicy(String name) { + super(name); + } + + @Override + public Target selectTarget(List targets, String key) { + if (targets.size() > 1) { + NavigableMap consistentTargets = new TreeMap<>(); + + for (Target target : targets) { + consistentTargets.put(getHash(target.getNodeID()), target); + } + + if (consistentTargets.size() > 0) { + Map.Entry consistentEntry = consistentTargets.floorEntry(getHash(key)); + + if (consistentEntry == null) { + consistentEntry = consistentTargets.firstEntry(); + } + + return consistentEntry.getValue(); + } + } else if (targets.size() > 0) { + return targets.get(0); + } + + return null; + } + + private int getHash(String str) { + final int FNV_INIT = 0x811c9dc5; + final int FNV_PRIME = 0x01000193; + + int hash = FNV_INIT; + + for (int i = 0; i < str.length(); i++) { + hash = (hash ^ str.charAt(i)) * FNV_PRIME; + } + + return hash; + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/policies/DefaultPolicyFactory.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/policies/DefaultPolicyFactory.java new file mode 100644 index 0000000000..aa39787456 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/policies/DefaultPolicyFactory.java @@ -0,0 +1,49 @@ +/** + * 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.activemq.artemis.core.server.balancing.policies; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +public class DefaultPolicyFactory extends PolicyFactory { + private static final Map> supportedPolicies = new HashMap<>(); + + static { + supportedPolicies.put(ConsistentHashPolicy.NAME, () -> new ConsistentHashPolicy()); + supportedPolicies.put(FirstElementPolicy.NAME, () -> new FirstElementPolicy()); + supportedPolicies.put(LeastConnectionsPolicy.NAME, () -> new LeastConnectionsPolicy()); + supportedPolicies.put(RoundRobinPolicy.NAME, () -> new RoundRobinPolicy()); + } + + @Override + public String[] getSupportedPolicies() { + return supportedPolicies.keySet().toArray(new String[supportedPolicies.size()]); + } + + @Override + public AbstractPolicy createPolicy(String policyName) { + Supplier policySupplier = supportedPolicies.get(policyName); + + if (policySupplier == null) { + throw new IllegalArgumentException("Policy not supported: " + policyName); + } + + return policySupplier.get(); + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/policies/FirstElementPolicy.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/policies/FirstElementPolicy.java new file mode 100644 index 0000000000..de8bf5b4f5 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/policies/FirstElementPolicy.java @@ -0,0 +1,43 @@ +/** + * 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.activemq.artemis.core.server.balancing.policies; + +import org.apache.activemq.artemis.core.server.balancing.targets.Target; + +import java.util.List; + +public class FirstElementPolicy extends AbstractPolicy { + public static final String NAME = "FIRST_ELEMENT"; + + public FirstElementPolicy() { + super(NAME); + } + + protected FirstElementPolicy(String name) { + super(name); + } + + @Override + public Target selectTarget(List targets, String key) { + if (targets.size() > 0) { + return targets.get(0); + } + + return null; + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/policies/LeastConnectionsPolicy.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/policies/LeastConnectionsPolicy.java new file mode 100644 index 0000000000..184cfc9d64 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/policies/LeastConnectionsPolicy.java @@ -0,0 +1,133 @@ +/** + * 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.activemq.artemis.core.server.balancing.policies; + +import org.apache.activemq.artemis.core.server.balancing.targets.Target; +import org.apache.activemq.artemis.core.server.balancing.targets.TargetProbe; +import org.jboss.logging.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.NavigableMap; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; + +public class LeastConnectionsPolicy extends RoundRobinPolicy { + private static final Logger logger = Logger.getLogger(LeastConnectionsPolicy.class); + + public static final String NAME = "LEAST_CONNECTIONS"; + + public static final String UPDATE_CONNECTION_COUNT_PROBE_NAME = "UPDATE_CONNECTION_COUNT_PROBE"; + + public static final String CONNECTION_COUNT_THRESHOLD = "CONNECTION_COUNT_THRESHOLD"; + + + private final Map connectionCountCache = new ConcurrentHashMap<>(); + + + private int connectionCountThreshold = 0; + + + private final TargetProbe targetProbe = new TargetProbe(UPDATE_CONNECTION_COUNT_PROBE_NAME) { + @Override + public boolean check(Target target) { + try { + Integer connectionCount = target.getAttribute("broker", "ConnectionCount", Integer.class, 3000); + + if (connectionCount < connectionCountThreshold) { + if (logger.isDebugEnabled()) { + logger.debug("Updating the connection count to 0/" + connectionCount + " for the target " + target); + } + + connectionCount = 0; + } else if (logger.isDebugEnabled()) { + logger.debug("Updating the connection count to 0/" + connectionCount + " for the target " + target); + } + + connectionCountCache.put(target, connectionCount); + + return true; + } catch (Exception e) { + logger.warn("Error on updating the connectionCount for the target " + target, e); + + return false; + } + } + }; + + @Override + public TargetProbe getTargetProbe() { + return targetProbe; + } + + public LeastConnectionsPolicy() { + super(NAME); + } + + @Override + public void init(Map properties) { + super.init(properties); + + if (properties != null) { + if (properties.containsKey(CONNECTION_COUNT_THRESHOLD)) { + connectionCountThreshold = Integer.valueOf(properties.get(CONNECTION_COUNT_THRESHOLD)); + } + } + } + + @Override + public Target selectTarget(List targets, String key) { + if (targets.size() > 1) { + NavigableMap> sortedTargets = new TreeMap<>(); + + for (Target target : targets) { + Integer connectionCount = connectionCountCache.get(target); + + if (connectionCount == null) { + connectionCount = Integer.MAX_VALUE; + } + + List leastTargets = sortedTargets.get(connectionCount); + + if (leastTargets == null) { + leastTargets = new ArrayList<>(); + sortedTargets.put(connectionCount, leastTargets); + } + + leastTargets.add(target); + } + + if (logger.isDebugEnabled()) { + logger.debug("LeastConnectionsPolicy.sortedTargets: " + sortedTargets); + } + + List selectedTargets = sortedTargets.firstEntry().getValue(); + + if (selectedTargets.size() > 1) { + return super.selectTarget(selectedTargets, key); + } else { + return selectedTargets.get(0); + } + } else if (targets.size() > 0) { + return targets.get(0); + } + + return null; + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/policies/Policy.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/policies/Policy.java new file mode 100644 index 0000000000..e74f2ea3d7 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/policies/Policy.java @@ -0,0 +1,36 @@ +/** + * 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.activemq.artemis.core.server.balancing.policies; + +import org.apache.activemq.artemis.core.server.balancing.targets.Target; +import org.apache.activemq.artemis.core.server.balancing.targets.TargetProbe; + +import java.util.List; +import java.util.Map; + +public interface Policy { + String getName(); + + TargetProbe getTargetProbe(); + + Map getProperties(); + + void init(Map properties); + + Target selectTarget(List targets, String key); +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/policies/PolicyFactory.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/policies/PolicyFactory.java new file mode 100644 index 0000000000..4c745ee982 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/policies/PolicyFactory.java @@ -0,0 +1,24 @@ +/** + * 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.activemq.artemis.core.server.balancing.policies; + +public abstract class PolicyFactory { + public abstract String[] getSupportedPolicies(); + + public abstract Policy createPolicy(String policyName); +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/policies/PolicyFactoryResolver.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/policies/PolicyFactoryResolver.java new file mode 100644 index 0000000000..dab4f9359b --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/policies/PolicyFactoryResolver.java @@ -0,0 +1,74 @@ +/** + * 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.activemq.artemis.core.server.balancing.policies; + +import org.apache.activemq.artemis.core.server.balancing.BrokerBalancer; + +import java.util.HashMap; +import java.util.Map; +import java.util.ServiceLoader; + +public class PolicyFactoryResolver { + private static PolicyFactoryResolver instance; + + public static PolicyFactoryResolver getInstance() { + if (instance == null) { + instance = new PolicyFactoryResolver(); + } + return instance; + } + + private final Map policyFactories = new HashMap<>(); + + private PolicyFactoryResolver() { + registerPolicyFactory(new DefaultPolicyFactory()); + + loadPolicyFactories(); + } + + public PolicyFactory resolve(String policyName) throws ClassNotFoundException { + PolicyFactory policyFactory = policyFactories.get(policyName); + + if (policyFactory == null) { + throw new ClassNotFoundException("No PolicyFactory found for the policy " + policyName); + } + + return policyFactory; + } + + private void loadPolicyFactories() { + ServiceLoader serviceLoader = ServiceLoader.load( + PolicyFactory.class, BrokerBalancer.class.getClassLoader()); + + for (PolicyFactory policyFactory : serviceLoader) { + registerPolicyFactory(policyFactory); + } + } + + public void registerPolicyFactory(PolicyFactory policyFactory) { + for (String policyName : policyFactory.getSupportedPolicies()) { + policyFactories.put(policyName, policyFactory); + } + } + + public void unregisterPolicyFactory(PolicyFactory policyFactory) { + for (String policyName : policyFactory.getSupportedPolicies()) { + policyFactories.remove(policyName, policyFactory); + } + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/policies/RoundRobinPolicy.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/policies/RoundRobinPolicy.java new file mode 100644 index 0000000000..7698d5e892 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/policies/RoundRobinPolicy.java @@ -0,0 +1,47 @@ +/** + * 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.activemq.artemis.core.server.balancing.policies; + +import org.apache.activemq.artemis.core.server.balancing.targets.Target; +import org.apache.activemq.artemis.utils.RandomUtil; + +import java.util.List; + +public class RoundRobinPolicy extends AbstractPolicy { + public static final String NAME = "ROUND_ROBIN"; + + private int pos = RandomUtil.randomInterval(0, Integer.MAX_VALUE); + + public RoundRobinPolicy() { + super(NAME); + } + + protected RoundRobinPolicy(String name) { + super(name); + } + + @Override + public Target selectTarget(List targets, String key) { + if (targets.size() > 0) { + pos = pos % targets.size(); + return targets.get(pos++); + } + + return null; + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/pools/AbstractPool.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/pools/AbstractPool.java new file mode 100644 index 0000000000..5ab614505c --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/pools/AbstractPool.java @@ -0,0 +1,243 @@ +/** + * 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.activemq.artemis.core.server.balancing.pools; + +import org.apache.activemq.artemis.api.core.TransportConfiguration; +import org.apache.activemq.artemis.core.server.balancing.targets.Target; +import org.apache.activemq.artemis.core.server.balancing.targets.TargetFactory; +import org.apache.activemq.artemis.core.server.balancing.targets.TargetMonitor; +import org.apache.activemq.artemis.core.server.balancing.targets.TargetProbe; +import org.jboss.logging.Logger; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.LockSupport; +import java.util.stream.Collectors; + +public abstract class AbstractPool implements Pool { + private static final Logger logger = Logger.getLogger(AbstractPool.class); + + private final TargetFactory targetFactory; + + private final ScheduledExecutorService scheduledExecutor; + + private final int checkPeriod; + + private final List targetProbes = new ArrayList<>(); + + private final Map targets = new ConcurrentHashMap<>(); + + private final List targetMonitors = new CopyOnWriteArrayList<>(); + + private String username; + + private String password; + + private int quorumSize; + + private int quorumTimeout; + + private long quorumTimeoutNanos; + + private final long quorumParkNanos = TimeUnit.MILLISECONDS.toNanos(100); + + private volatile boolean started = false; + + + @Override + public String getUsername() { + return username; + } + + @Override + public void setUsername(String username) { + this.username = username; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public void setPassword(String password) { + this.password = password; + } + + @Override + public int getCheckPeriod() { + return checkPeriod; + } + + @Override + public int getQuorumSize() { + return quorumSize; + } + + @Override + public int getQuorumTimeout() { + return quorumTimeout; + } + + @Override + public void setQuorumTimeout(int quorumTimeout) { + this.quorumTimeout = quorumTimeout; + this.quorumTimeoutNanos = TimeUnit.MILLISECONDS.toNanos(quorumTimeout); + } + + @Override + public void setQuorumSize(int quorumSize) { + this.quorumSize = quorumSize; + } + + @Override + public List getAllTargets() { + return targetMonitors.stream().map(targetMonitor -> targetMonitor.getTarget()).collect(Collectors.toList()); + } + + @Override + public List getTargets() { + List targets = targetMonitors.stream().filter(targetMonitor -> targetMonitor.isTargetReady()) + .map(targetMonitor -> targetMonitor.getTarget()).collect(Collectors.toList()); + + if (quorumTimeout > 0 && targets.size() < quorumSize) { + final long deadline = System.nanoTime() + quorumTimeoutNanos; + while (targets.size() < quorumSize && (System.nanoTime() - deadline) < 0) { + targets = targetMonitors.stream().filter(targetMonitor -> targetMonitor.isTargetReady()) + .map(targetMonitor -> targetMonitor.getTarget()).collect(Collectors.toList()); + + LockSupport.parkNanos(quorumParkNanos); + } + } + + if (logger.isDebugEnabled()) { + logger.debugf("Ready targets are " + targets + " / " + targetMonitors + " and quorumSize is " + quorumSize); + } + + return targets.size() < quorumSize ? Collections.emptyList() : targets; + } + + @Override + public List getTargetProbes() { + return targetProbes; + } + + @Override + public boolean isStarted() { + return started; + } + + + public AbstractPool(TargetFactory targetFactory, ScheduledExecutorService scheduledExecutor, int checkPeriod) { + this.targetFactory = targetFactory; + + this.scheduledExecutor = scheduledExecutor; + + this.checkPeriod = checkPeriod; + } + + @Override + public Target getTarget(String nodeId) { + for (TargetMonitor targetMonitor : targetMonitors) { + if (nodeId.equals(targetMonitor.getTarget().getNodeID())) { + return targetMonitor.getTarget(); + } + } + + return null; + } + + @Override + public boolean isTargetReady(Target target) { + TargetMonitor targetMonitor = targets.get(target); + + return targetMonitor != null ? targetMonitor.isTargetReady() : false; + } + + @Override + public void addTargetProbe(TargetProbe probe) { + targetProbes.add(probe); + } + + @Override + public void removeTargetProbe(TargetProbe probe) { + targetProbes.remove(probe); + } + + @Override + public void start() throws Exception { + started = true; + + for (TargetMonitor targetMonitor : targetMonitors) { + targetMonitor.start(); + } + } + + @Override + public void stop() throws Exception { + started = false; + + List targetMonitors = new ArrayList<>(this.targetMonitors); + + for (TargetMonitor targetMonitor : targetMonitors) { + removeTarget(targetMonitor.getTarget()); + } + } + + protected void addTarget(TransportConfiguration connector, String nodeID) { + addTarget(targetFactory.createTarget(connector, nodeID)); + } + + @Override + public boolean addTarget(Target target) { + TargetMonitor targetMonitor = new TargetMonitor(scheduledExecutor, checkPeriod, target, targetProbes); + + if (targets.putIfAbsent(target, targetMonitor) != null) { + return false; + } + + targetMonitors.add(targetMonitor); + + if (started) { + targetMonitor.start(); + } + + return true; + } + + @Override + public boolean removeTarget(Target target) { + TargetMonitor targetMonitor = targets.remove(target); + + if (targetMonitor == null) { + return false; + } + + targetMonitors.remove(targetMonitor); + + targetMonitor.stop(); + + return true; + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/pools/ClusterPool.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/pools/ClusterPool.java new file mode 100644 index 0000000000..e2a7a2801d --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/pools/ClusterPool.java @@ -0,0 +1,69 @@ +/** + * 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.activemq.artemis.core.server.balancing.pools; + +import org.apache.activemq.artemis.api.core.client.ClusterTopologyListener; +import org.apache.activemq.artemis.api.core.client.TopologyMember; +import org.apache.activemq.artemis.core.server.balancing.targets.TargetFactory; +import org.apache.activemq.artemis.core.server.cluster.ClusterConnection; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledExecutorService; + +public class ClusterPool extends AbstractPool implements ClusterTopologyListener { + private final ClusterConnection clusterConnection; + + private final Map clusterMembers = new ConcurrentHashMap<>(); + + public ClusterPool(TargetFactory targetFactory, ScheduledExecutorService scheduledExecutor, + int checkPeriod, ClusterConnection clusterConnection) { + super(targetFactory, scheduledExecutor, checkPeriod); + + this.clusterConnection = clusterConnection; + } + + @Override + public void start() throws Exception { + super.start(); + + clusterConnection.addClusterTopologyListener(this); + } + + @Override + public void stop() throws Exception { + clusterConnection.removeClusterTopologyListener(this); + + super.stop(); + } + + @Override + public void nodeUP(TopologyMember member, boolean last) { + if (!clusterConnection.getNodeID().equals(member.getNodeId()) && + clusterMembers.putIfAbsent(member.getNodeId(), member) == null) { + addTarget(member.getLive(), member.getNodeId()); + } + } + + @Override + public void nodeDown(long eventUID, String nodeID) { + if (clusterMembers.remove(nodeID) != null) { + removeTarget(getTarget(nodeID)); + } + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/pools/DiscoveryGroupService.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/pools/DiscoveryGroupService.java new file mode 100644 index 0000000000..47fbc6e551 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/pools/DiscoveryGroupService.java @@ -0,0 +1,87 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.activemq.artemis.core.server.balancing.pools; + +import org.apache.activemq.artemis.core.cluster.DiscoveryEntry; +import org.apache.activemq.artemis.core.cluster.DiscoveryGroup; +import org.apache.activemq.artemis.core.cluster.DiscoveryListener; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class DiscoveryGroupService extends DiscoveryService implements DiscoveryListener { + private final DiscoveryGroup discoveryGroup; + + private final Map entries = new ConcurrentHashMap<>(); + + public DiscoveryGroupService(DiscoveryGroup discoveryGroup) { + this.discoveryGroup = discoveryGroup; + } + + @Override + public void start() throws Exception { + discoveryGroup.registerListener(this); + + discoveryGroup.start(); + } + + @Override + public void stop() throws Exception { + discoveryGroup.unregisterListener(this); + + discoveryGroup.stop(); + + entries.clear(); + } + + @Override + public boolean isStarted() { + return discoveryGroup.isStarted(); + } + + @Override + public void connectorsChanged(List newEntries) { + Map oldEntries = new HashMap<>(entries); + + for (DiscoveryEntry newEntry : newEntries) { + Entry oldEntry = oldEntries.remove(newEntry.getNodeID()); + + if (oldEntry == null) { + Entry addingEntry = new Entry(newEntry.getNodeID(), newEntry.getConnector()); + + entries.put(addingEntry.getNodeID(), addingEntry); + + fireEntryAddedEvent(addingEntry); + } else if (!newEntry.getConnector().equals(oldEntry.getConnector())) { + Entry updatingEntry = new Entry(newEntry.getNodeID(), newEntry.getConnector()); + + entries.put(updatingEntry.getNodeID(), updatingEntry); + + fireEntryUpdatedEvent(oldEntry, updatingEntry); + } + } + + oldEntries.forEach((nodeID, entry) -> { + entries.remove(nodeID); + + fireEntryRemovedEvent(entry); + }); + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/pools/DiscoveryPool.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/pools/DiscoveryPool.java new file mode 100644 index 0000000000..03c3a8c343 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/pools/DiscoveryPool.java @@ -0,0 +1,70 @@ +/** + * 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.activemq.artemis.core.server.balancing.pools; + +import org.apache.activemq.artemis.core.server.balancing.targets.TargetFactory; + +import java.util.concurrent.ScheduledExecutorService; + +public class DiscoveryPool extends AbstractPool implements DiscoveryService.Listener { + private final DiscoveryService discoveryService; + + public DiscoveryPool(TargetFactory targetFactory, ScheduledExecutorService scheduledExecutor, + int checkPeriod, DiscoveryService discoveryService) { + super(targetFactory, scheduledExecutor, checkPeriod); + + this.discoveryService = discoveryService; + } + + @Override + public void start() throws Exception { + super.start(); + + discoveryService.setListener(this); + + discoveryService.start(); + } + + @Override + public void stop() throws Exception { + super.stop(); + + if (discoveryService != null) { + discoveryService.setListener(null); + + discoveryService.stop(); + } + } + + @Override + public void entryAdded(DiscoveryService.Entry entry) { + addTarget(entry.getConnector(), entry.getNodeID()); + } + + @Override + public void entryRemoved(DiscoveryService.Entry entry) { + removeTarget(getTarget(entry.getNodeID())); + } + + @Override + public void entryUpdated(DiscoveryService.Entry oldEntry, DiscoveryService.Entry newEntry) { + removeTarget(getTarget(oldEntry.getNodeID())); + + addTarget(newEntry.getConnector(), newEntry.getNodeID()); + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/pools/DiscoveryService.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/pools/DiscoveryService.java new file mode 100644 index 0000000000..a45fedec6e --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/pools/DiscoveryService.java @@ -0,0 +1,88 @@ +/** + * 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.activemq.artemis.core.server.balancing.pools; + +import org.apache.activemq.artemis.api.core.TransportConfiguration; +import org.apache.activemq.artemis.core.server.ActiveMQComponent; + +public abstract class DiscoveryService implements ActiveMQComponent { + + private Listener listener; + + public Listener getListener() { + return listener; + } + + public void setListener(Listener listener) { + this.listener = listener; + } + + protected void fireEntryAddedEvent(Entry entry) { + if (listener != null) { + this.listener.entryAdded(entry); + } + } + + protected void fireEntryRemovedEvent(Entry entry) { + if (listener != null) { + this.listener.entryRemoved(entry); + } + } + + protected void fireEntryUpdatedEvent(Entry oldEntry, Entry newEntry) { + if (listener != null) { + this.listener.entryUpdated(oldEntry, newEntry); + } + } + + + public interface Listener { + void entryAdded(Entry entry); + + void entryRemoved(Entry entry); + + void entryUpdated(Entry oldEntry, Entry newEntry); + } + + public class Entry { + private final String nodeID; + private final TransportConfiguration connector; + + public String getNodeID() { + return nodeID; + } + + public TransportConfiguration getConnector() { + return connector; + } + + public Entry(String nodeID, TransportConfiguration connector) { + this.nodeID = nodeID; + this.connector = connector; + } + + @Override + public String toString() { + StringBuilder stringBuilder = new StringBuilder(Entry.class.getSimpleName()); + stringBuilder.append("(nodeID=" + nodeID); + stringBuilder.append(", connector=" + connector); + stringBuilder.append(") "); + return stringBuilder.toString(); + } + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/pools/Pool.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/pools/Pool.java new file mode 100644 index 0000000000..db6b733147 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/pools/Pool.java @@ -0,0 +1,65 @@ +/** + * 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.activemq.artemis.core.server.balancing.pools; + +import org.apache.activemq.artemis.core.server.ActiveMQComponent; +import org.apache.activemq.artemis.core.server.balancing.targets.Target; +import org.apache.activemq.artemis.core.server.balancing.targets.TargetProbe; + +import java.util.List; + +public interface Pool extends ActiveMQComponent { + String getUsername(); + + void setUsername(String username); + + String getPassword(); + + void setPassword(String password); + + int getQuorumSize(); + + void setQuorumSize(int quorumSize); + + int getQuorumTimeout(); + + void setQuorumTimeout(int quorumTimeout); + + int getCheckPeriod(); + + + + Target getTarget(String nodeId); + + boolean isTargetReady(Target target); + + List getTargets(); + + List getAllTargets(); + + boolean addTarget(Target target); + + boolean removeTarget(Target target); + + + List getTargetProbes(); + + void addTargetProbe(TargetProbe probe); + + void removeTargetProbe(TargetProbe probe); +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/pools/StaticPool.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/pools/StaticPool.java new file mode 100644 index 0000000000..c652d4a11a --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/pools/StaticPool.java @@ -0,0 +1,44 @@ +/** + * 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.activemq.artemis.core.server.balancing.pools; + +import org.apache.activemq.artemis.api.core.TransportConfiguration; +import org.apache.activemq.artemis.core.server.balancing.targets.TargetFactory; + +import java.util.List; +import java.util.concurrent.ScheduledExecutorService; + +public class StaticPool extends AbstractPool { + private final List staticConnectors; + + public StaticPool(TargetFactory targetFactory, ScheduledExecutorService scheduledExecutor, + int checkPeriod, List staticConnectors) { + super(targetFactory, scheduledExecutor, checkPeriod); + + this.staticConnectors = staticConnectors; + } + + @Override + public void start() throws Exception { + super.start(); + + for (TransportConfiguration staticConnector : staticConnectors) { + addTarget(staticConnector, null); + } + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/AbstractTarget.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/AbstractTarget.java new file mode 100644 index 0000000000..3f123e5a86 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/AbstractTarget.java @@ -0,0 +1,112 @@ +/** + * 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.activemq.artemis.core.server.balancing.targets; + +import org.apache.activemq.artemis.api.core.TransportConfiguration; + +public abstract class AbstractTarget implements Target { + private final TransportConfiguration connector; + + private String nodeID; + + private String username; + + private String password; + + private int checkPeriod; + + private TargetListener listener; + + @Override + public String getNodeID() { + return nodeID; + } + + protected void setNodeID(String nodeID) { + this.nodeID = nodeID; + } + + @Override + public String getUsername() { + return username; + } + + @Override + public void setUsername(String username) { + this.username = username; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public void setPassword(String password) { + this.password = password; + } + + @Override + public int getCheckPeriod() { + return checkPeriod; + } + + @Override + public void setCheckPeriod(int checkPeriod) { + this.checkPeriod = checkPeriod; + } + + @Override + public TargetListener getListener() { + return listener; + } + + @Override + public void setListener(TargetListener listener) { + this.listener = listener; + } + + @Override + public TransportConfiguration getConnector() { + return connector; + } + + + public AbstractTarget(TransportConfiguration connector, String nodeID) { + this.connector = connector; + this.nodeID = nodeID; + } + + + protected void fireConnectedEvent() { + if (listener != null) { + listener.targetConnected(); + } + } + + protected void fireDisconnectedEvent() { + if (listener != null) { + listener.targetDisconnected(); + } + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + " [connector=" + connector + ", nodeID=" + nodeID + "]"; + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/AbstractTargetFactory.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/AbstractTargetFactory.java new file mode 100644 index 0000000000..9ca15f311d --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/AbstractTargetFactory.java @@ -0,0 +1,45 @@ +/** + * 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.activemq.artemis.core.server.balancing.targets; + +public abstract class AbstractTargetFactory implements TargetFactory { + + private String username; + + private String password; + + @Override + public String getUsername() { + return username; + } + + @Override + public void setUsername(String username) { + this.username = username; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public void setPassword(String password) { + this.password = password; + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/ActiveMQTarget.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/ActiveMQTarget.java new file mode 100644 index 0000000000..0fc1cadc64 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/ActiveMQTarget.java @@ -0,0 +1,132 @@ +/** + * 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.activemq.artemis.core.server.balancing.targets; + +import org.apache.activemq.artemis.api.core.ActiveMQException; +import org.apache.activemq.artemis.api.core.TransportConfiguration; +import org.apache.activemq.artemis.api.core.client.ActiveMQClient; +import org.apache.activemq.artemis.api.core.client.ClientSessionFactory; +import org.apache.activemq.artemis.api.core.client.ServerLocator; +import org.apache.activemq.artemis.api.core.management.ActiveMQManagementProxy; +import org.apache.activemq.artemis.api.core.management.ResourceNames; +import org.apache.activemq.artemis.core.remoting.FailureListener; +import org.apache.activemq.artemis.core.server.balancing.BrokerBalancer; +import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection; +import org.apache.activemq.artemis.utils.UUIDGenerator; +import org.jboss.logging.Logger; + +public class ActiveMQTarget extends AbstractTarget implements FailureListener { + private static final Logger logger = Logger.getLogger(ActiveMQTarget.class); + + private boolean connected = false; + + private final ServerLocator serverLocator; + + private ClientSessionFactory sessionFactory; + private RemotingConnection remotingConnection; + private ActiveMQManagementProxy managementProxy; + + @Override + public boolean isLocal() { + return false; + } + + @Override + public boolean isConnected() { + return connected; + } + + public ActiveMQTarget(TransportConfiguration connector, String nodeID) { + super(connector, nodeID); + + serverLocator = ActiveMQClient.createServerLocatorWithoutHA(connector); + } + + + @Override + public void connect() throws Exception { + sessionFactory = serverLocator.createSessionFactory(); + + remotingConnection = sessionFactory.getConnection(); + remotingConnection.addFailureListener(this); + + managementProxy = new ActiveMQManagementProxy(sessionFactory.createSession(getUsername(), getPassword(), + false, true, true, false, ActiveMQClient.DEFAULT_ACK_BATCH_SIZE, + BrokerBalancer.CLIENT_ID_PREFIX + UUIDGenerator.getInstance().generateStringUUID()).start()); + + connected = true; + + fireConnectedEvent(); + } + + @Override + public void disconnect() throws Exception { + if (connected) { + connected = false; + + managementProxy.close(); + + remotingConnection.removeFailureListener(this); + + sessionFactory.close(); + + fireDisconnectedEvent(); + } + } + + @Override + public boolean checkReadiness() { + try { + if (getNodeID() == null) { + setNodeID(getAttribute(ResourceNames.BROKER, "NodeID", String.class, 3000)); + } + + return getAttribute(ResourceNames.BROKER, "Active", Boolean.class, 3000); + } catch (Exception e) { + logger.warn("Error on check readiness", e); + } + + return false; + } + + @Override + public T getAttribute(String resourceName, String attributeName, Class attributeClass, int timeout) throws Exception { + return managementProxy.getAttribute(resourceName, attributeName, attributeClass, timeout); + } + + @Override + public T invokeOperation(String resourceName, String operationName, Object[] operationParams, Class operationClass, int timeout) throws Exception { + return managementProxy.invokeOperation(resourceName, operationName, operationParams, operationClass, timeout); + } + + @Override + public void connectionFailed(ActiveMQException exception, boolean failedOver) { + connectionFailed(exception, failedOver, null); + } + + @Override + public void connectionFailed(ActiveMQException exception, boolean failedOver, String scaleDownTargetNodeID) { + try { + if (connected) { + disconnect(); + } + } catch (Exception e) { + logger.debug("Exception on disconnecting: ", e); + } + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/ActiveMQTargetFactory.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/ActiveMQTargetFactory.java new file mode 100644 index 0000000000..b7cb9f9446 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/ActiveMQTargetFactory.java @@ -0,0 +1,32 @@ +/** + * 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.activemq.artemis.core.server.balancing.targets; + +import org.apache.activemq.artemis.api.core.TransportConfiguration; + +public class ActiveMQTargetFactory extends AbstractTargetFactory { + @Override + public Target createTarget(TransportConfiguration connector, String nodeID) { + Target target = new ActiveMQTarget(connector, nodeID); + + target.setUsername(getUsername()); + target.setPassword(getPassword()); + + return target; + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/LocalTarget.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/LocalTarget.java new file mode 100644 index 0000000000..86043fefe3 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/LocalTarget.java @@ -0,0 +1,69 @@ +/** + * 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.activemq.artemis.core.server.balancing.targets; + +import org.apache.activemq.artemis.api.core.TransportConfiguration; +import org.apache.activemq.artemis.core.server.ActiveMQServer; +import org.apache.activemq.artemis.core.server.management.ManagementService; + +public class LocalTarget extends AbstractTarget { + private final ActiveMQServer server; + private final ManagementService managementService; + + public LocalTarget(TransportConfiguration connector, ActiveMQServer server) { + super(connector, server.getNodeID().toString()); + + this.server = server; + this.managementService = server.getManagementService(); + } + + @Override + public boolean isLocal() { + return true; + } + + @Override + public boolean isConnected() { + return true; + } + + @Override + public void connect() throws Exception { + + } + + @Override + public void disconnect() throws Exception { + + } + + @Override + public boolean checkReadiness() { + return true; + } + + @Override + public T getAttribute(String resourceName, String attributeName, Class attributeClass, int timeout) throws Exception { + return (T)managementService.getAttribute(resourceName, attributeName); + } + + @Override + public T invokeOperation(String resourceName, String operationName, Object[] operationParams, Class operationClass, int timeout) throws Exception { + return (T)managementService.invokeOperation(resourceName, operationName, operationParams); + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/Target.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/Target.java new file mode 100644 index 0000000000..f82331ba5b --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/Target.java @@ -0,0 +1,59 @@ +/** + * 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.activemq.artemis.core.server.balancing.targets; + +import org.apache.activemq.artemis.api.core.TransportConfiguration; + +public interface Target { + + boolean isLocal(); + + String getNodeID(); + + TransportConfiguration getConnector(); + + String getUsername(); + + void setUsername(String username); + + String getPassword(); + + void setPassword(String password); + + int getCheckPeriod(); + + void setCheckPeriod(int checkPeriod); + + TargetListener getListener(); + + void setListener(TargetListener listener); + + boolean isConnected(); + + void connect() throws Exception; + + void disconnect() throws Exception; + + + boolean checkReadiness(); + + + T getAttribute(String resourceName, String attributeName, Class attributeClass, int timeout) throws Exception; + + T invokeOperation(String resourceName, String operationName, Object[] operationParams, Class operationClass, int timeout) throws Exception; +} \ No newline at end of file diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/TargetFactory.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/TargetFactory.java new file mode 100644 index 0000000000..508be40200 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/TargetFactory.java @@ -0,0 +1,32 @@ +/** + * 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.activemq.artemis.core.server.balancing.targets; + +import org.apache.activemq.artemis.api.core.TransportConfiguration; + +public interface TargetFactory { + String getUsername(); + + void setUsername(String username); + + String getPassword(); + + void setPassword(String password); + + Target createTarget(TransportConfiguration connector, String nodeID); +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/TargetKey.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/TargetKey.java new file mode 100644 index 0000000000..d01b932f63 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/TargetKey.java @@ -0,0 +1,53 @@ +/** + * 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.activemq.artemis.core.server.balancing.targets; + +public enum TargetKey { + CLIENT_ID, SNI_HOST, SOURCE_IP, USER_NAME; + + public static final String validValues; + + static { + StringBuffer stringBuffer = new StringBuffer(); + for (TargetKey type : TargetKey.values()) { + + if (stringBuffer.length() != 0) { + stringBuffer.append(","); + } + + stringBuffer.append(type.name()); + } + + validValues = stringBuffer.toString(); + } + + public static TargetKey getType(String type) { + switch (type) { + case "CLIENT_ID": + return CLIENT_ID; + case "SNI_HOST": + return SNI_HOST; + case "SOURCE_IP": + return SOURCE_IP; + case "USER_NAME": + return USER_NAME; + default: + throw new IllegalStateException("Invalid RedirectKey:" + type + " valid Types: " + validValues); + } + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/TargetKeyResolver.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/TargetKeyResolver.java new file mode 100644 index 0000000000..dac82deedf --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/TargetKeyResolver.java @@ -0,0 +1,108 @@ +/** + * 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.activemq.artemis.core.server.balancing.targets; + +import org.apache.activemq.artemis.spi.core.remoting.Connection; +import org.jboss.logging.Logger; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class TargetKeyResolver { + public static final String DEFAULT_KEY_VALUE = "DEFAULT"; + + + private static final Logger logger = Logger.getLogger(TargetKeyResolver.class); + + private static final char SOCKET_ADDRESS_DELIMITER = ':'; + private static final String SOCKET_ADDRESS_PREFIX = "/"; + + + private final TargetKey key; + + private final Pattern keyFilter; + + + public TargetKey getKey() { + return key; + } + + public String getKeyFilter() { + return keyFilter != null ? keyFilter.pattern() : null; + } + + public TargetKeyResolver(TargetKey key, String keyFilter) { + this.key = key; + + this.keyFilter = keyFilter != null ? Pattern.compile(keyFilter) : null; + } + + public String resolve(Connection connection, String clientID, String username) { + String keyValue = null; + + switch (key) { + case CLIENT_ID: + keyValue = clientID; + break; + case SNI_HOST: + if (connection != null) { + keyValue = connection.getSNIHostName(); + } + break; + case SOURCE_IP: + if (connection != null && connection.getRemoteAddress() != null) { + keyValue = connection.getRemoteAddress(); + + boolean hasPrefix = keyValue.startsWith(SOCKET_ADDRESS_PREFIX); + int delimiterIndex = keyValue.lastIndexOf(SOCKET_ADDRESS_DELIMITER); + + if (hasPrefix || delimiterIndex > 0) { + keyValue = keyValue.substring(hasPrefix ? SOCKET_ADDRESS_PREFIX.length() : 0, + delimiterIndex > 0 ? delimiterIndex : keyValue.length()); + } + } + break; + case USER_NAME: + keyValue = username; + break; + default: + throw new IllegalStateException("Unexpected value: " + key); + } + + if (logger.isDebugEnabled()) { + logger.debugf("keyValue for %s: %s", key, keyValue); + } + + if (keyValue == null) { + keyValue = DEFAULT_KEY_VALUE; + } else if (keyFilter != null) { + Matcher keyMatcher = keyFilter.matcher(keyValue); + + if (keyMatcher.find()) { + keyValue = keyMatcher.group(); + + if (logger.isDebugEnabled()) { + logger.debugf("keyValue with filter %s: %s", keyFilter, keyValue); + } + } + } + + + return keyValue; + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/TargetListener.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/TargetListener.java new file mode 100644 index 0000000000..266d736989 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/TargetListener.java @@ -0,0 +1,24 @@ +/** + * 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.activemq.artemis.core.server.balancing.targets; + +public interface TargetListener { + void targetConnected(); + + void targetDisconnected(); +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/TargetMonitor.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/TargetMonitor.java new file mode 100644 index 0000000000..b1865da476 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/TargetMonitor.java @@ -0,0 +1,129 @@ +/** + * 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.activemq.artemis.core.server.balancing.targets; + +import org.apache.activemq.artemis.core.server.ActiveMQScheduledComponent; +import org.jboss.logging.Logger; + +import java.util.List; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class TargetMonitor extends ActiveMQScheduledComponent implements TargetListener { + private static final Logger logger = Logger.getLogger(TargetMonitor.class); + + + private final Target target; + + private final List targetProbes; + + private volatile boolean targetReady = false; + + + public Target getTarget() { + return target; + } + + public boolean isTargetReady() { + return targetReady; + } + + + public TargetMonitor(ScheduledExecutorService scheduledExecutorService, int checkPeriod, Target target, List targetProbes) { + super(scheduledExecutorService, 0, checkPeriod, TimeUnit.MILLISECONDS, false); + + this.target = target; + this.targetProbes = targetProbes; + } + + @Override + public synchronized void start() { + target.setListener(this); + + super.start(); + } + + @Override + public synchronized void stop() { + super.stop(); + + targetReady = false; + + target.setListener(null); + + try { + target.disconnect(); + } catch (Exception e) { + logger.debug("Error on disconnecting target " + target, e); + } + } + + @Override + public void run() { + try { + if (!target.isConnected()) { + if (logger.isDebugEnabled()) { + logger.debug("Connecting to " + target); + } + + target.connect(); + } + + targetReady = target.checkReadiness() && checkTargetProbes(); + + if (logger.isDebugEnabled()) { + if (targetReady) { + logger.debug(target + " is ready"); + } else { + logger.debug(target + " is not ready"); + } + } + } catch (Exception e) { + logger.warn("Error monitoring " + target, e); + + targetReady = false; + } + } + + private boolean checkTargetProbes() { + for (TargetProbe targetProbe : targetProbes) { + if (!targetProbe.check(target)) { + logger.info(targetProbe.getName() + " has failed on " + target); + return false; + } + } + + return true; + } + + @Override + public void targetConnected() { + + } + + @Override + public void targetDisconnected() { + targetReady = false; + } + + + @Override + public String toString() { + return this.getClass().getSimpleName() + " [target=" + target + ", targetReady=" + targetReady + "]"; + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/TargetProbe.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/TargetProbe.java new file mode 100644 index 0000000000..1d80067b12 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/balancing/targets/TargetProbe.java @@ -0,0 +1,32 @@ +/** + * 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.activemq.artemis.core.server.balancing.targets; + +public abstract class TargetProbe { + private final String name; + + public String getName() { + return name; + } + + public TargetProbe(String name) { + this.name = name; + } + + public abstract boolean check(Target target); +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/ActiveMQServerImpl.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/ActiveMQServerImpl.java index dac6cdee4a..841744697e 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/ActiveMQServerImpl.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/ActiveMQServerImpl.java @@ -172,6 +172,7 @@ import org.apache.activemq.artemis.core.server.plugin.ActiveMQServerMessagePlugi import org.apache.activemq.artemis.core.server.plugin.ActiveMQServerQueuePlugin; import org.apache.activemq.artemis.core.server.plugin.ActiveMQServerResourcePlugin; import org.apache.activemq.artemis.core.server.plugin.ActiveMQServerSessionPlugin; +import org.apache.activemq.artemis.core.server.balancing.BrokerBalancerManager; import org.apache.activemq.artemis.core.server.reload.ReloadManager; import org.apache.activemq.artemis.core.server.reload.ReloadManagerImpl; import org.apache.activemq.artemis.core.server.transformer.Transformer; @@ -289,6 +290,8 @@ public class ActiveMQServerImpl implements ActiveMQServer { private volatile RemotingService remotingService; + private volatile BrokerBalancerManager balancerManager; + private final List protocolManagerFactories = new ArrayList<>(); private final List protocolServices = new ArrayList<>(); @@ -1194,6 +1197,8 @@ public class ActiveMQServerImpl implements ActiveMQServer { } } + stopComponent(balancerManager); + stopComponent(connectorsService); // we stop the groupingHandler before we stop the cluster manager so binding mappings @@ -1632,6 +1637,11 @@ public class ActiveMQServerImpl implements ActiveMQServer { return clusterManager; } + @Override + public BrokerBalancerManager getBalancerManager() { + return balancerManager; + } + public BackupManager getBackupManager() { return backupManager; } @@ -3107,6 +3117,10 @@ public class ActiveMQServerImpl implements ActiveMQServer { federationManager.deploy(); + balancerManager = new BrokerBalancerManager(configuration, this, scheduledPool); + + balancerManager.deploy(); + remotingService = new RemotingServiceImpl(clusterManager, configuration, this, managementService, scheduledPool, protocolManagerFactories, executorFactory.getExecutor(), serviceRegistry); messagingServerControl = managementService.registerServer(postOffice, securityStore, storageManager, configuration, addressSettingsRepository, securityRepository, resourceManager, remotingService, this, queueFactory, scheduledPool, pagingManager, haPolicy.isBackup()); @@ -3270,6 +3284,8 @@ public class ActiveMQServerImpl implements ActiveMQServer { federationManager.start(); } + balancerManager.start(); + startProtocolServices(); if (nodeManager.getNodeId() == null) { diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/management/ManagementService.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/management/ManagementService.java index c47804ae19..a3bbe6248b 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/management/ManagementService.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/management/ManagementService.java @@ -44,6 +44,7 @@ import org.apache.activemq.artemis.core.server.ActiveMQServer; import org.apache.activemq.artemis.core.server.Divert; import org.apache.activemq.artemis.core.server.Queue; import org.apache.activemq.artemis.core.server.QueueFactory; +import org.apache.activemq.artemis.core.server.balancing.BrokerBalancer; import org.apache.activemq.artemis.core.server.cluster.Bridge; import org.apache.activemq.artemis.core.server.cluster.BroadcastGroup; import org.apache.activemq.artemis.core.server.cluster.ClusterConnection; @@ -127,6 +128,10 @@ public interface ManagementService extends NotificationService, ActiveMQComponen void unregisterCluster(String name) throws Exception; + void registerBrokerBalancer(BrokerBalancer balancer) throws Exception; + + void unregisterBrokerBalancer(String name) throws Exception; + Object getResource(String resourceName); Object[] getResources(Class resourceType); @@ -136,4 +141,8 @@ public interface ManagementService extends NotificationService, ActiveMQComponen void registerHawtioSecurity(ArtemisMBeanServerGuard securityMBean) throws Exception; void unregisterHawtioSecurity() throws Exception; + + Object getAttribute(String resourceName, String attribute); + + Object invokeOperation(String resourceName, String operation, Object[] params) throws Exception; } diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/management/impl/ManagementServiceImpl.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/management/impl/ManagementServiceImpl.java index 5d9115b040..60c268cc24 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/management/impl/ManagementServiceImpl.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/management/impl/ManagementServiceImpl.java @@ -51,6 +51,7 @@ import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl; import org.apache.activemq.artemis.api.core.management.AddressControl; import org.apache.activemq.artemis.api.core.management.BaseBroadcastGroupControl; import org.apache.activemq.artemis.api.core.management.BridgeControl; +import org.apache.activemq.artemis.api.core.management.BrokerBalancerControl; import org.apache.activemq.artemis.api.core.management.ClusterConnectionControl; import org.apache.activemq.artemis.api.core.management.DivertControl; import org.apache.activemq.artemis.api.core.management.ManagementHelper; @@ -66,6 +67,7 @@ import org.apache.activemq.artemis.core.management.impl.AddressControlImpl; import org.apache.activemq.artemis.core.management.impl.BaseBroadcastGroupControlImpl; import org.apache.activemq.artemis.core.management.impl.BridgeControlImpl; import org.apache.activemq.artemis.core.management.impl.BroadcastGroupControlImpl; +import org.apache.activemq.artemis.core.management.impl.BrokerBalancerControlImpl; import org.apache.activemq.artemis.core.management.impl.ClusterConnectionControlImpl; import org.apache.activemq.artemis.core.management.impl.DivertControlImpl; import org.apache.activemq.artemis.core.management.impl.JGroupsChannelBroadcastGroupControlImpl; @@ -88,6 +90,7 @@ import org.apache.activemq.artemis.core.server.ActiveMQServerLogger; import org.apache.activemq.artemis.core.server.Divert; import org.apache.activemq.artemis.core.server.Queue; import org.apache.activemq.artemis.core.server.QueueFactory; +import org.apache.activemq.artemis.core.server.balancing.BrokerBalancer; import org.apache.activemq.artemis.core.server.cluster.Bridge; import org.apache.activemq.artemis.core.server.cluster.BroadcastGroup; import org.apache.activemq.artemis.core.server.cluster.ClusterConnection; @@ -493,6 +496,25 @@ public class ManagementServiceImpl implements ManagementService { unregisterFromRegistry(ResourceNames.CORE_CLUSTER_CONNECTION + name); } + @Override + public synchronized void registerBrokerBalancer(final BrokerBalancer balancer) throws Exception { + ObjectName objectName = objectNameBuilder.getBrokerBalancerObjectName(balancer.getName()); + BrokerBalancerControl brokerBalancerControl = new BrokerBalancerControlImpl(balancer, storageManager); + registerInJMX(objectName, brokerBalancerControl); + registerInRegistry(ResourceNames.BROKER_BALANCER + balancer.getName(), brokerBalancerControl); + + if (logger.isDebugEnabled()) { + logger.debug("registered broker balancer " + objectName); + } + } + + @Override + public synchronized void unregisterBrokerBalancer(final String name) throws Exception { + ObjectName objectName = objectNameBuilder.getBrokerBalancerObjectName(name); + unregisterFromJMX(objectName); + unregisterFromRegistry(ResourceNames.BROKER_BALANCER + name); + } + @Override public void registerHawtioSecurity(ArtemisMBeanServerGuard mBeanServerGuard) throws Exception { ObjectName objectName = objectNameBuilder.getManagementContextObjectName(); @@ -831,6 +853,7 @@ public class ManagementServiceImpl implements ManagementService { notificationsEnabled = enabled; } + @Override public Object getAttribute(final String resourceName, final String attribute) { try { Object resource = registry.get(resourceName); @@ -857,7 +880,8 @@ public class ManagementServiceImpl implements ManagementService { } } - private Object invokeOperation(final String resourceName, + @Override + public Object invokeOperation(final String resourceName, final String operation, final Object[] params) throws Exception { Object resource = registry.get(resourceName); diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/protocol/AbstractProtocolManager.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/protocol/AbstractProtocolManager.java index 7a4d3b175a..7d540b7df7 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/protocol/AbstractProtocolManager.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/protocol/AbstractProtocolManager.java @@ -27,8 +27,9 @@ import org.apache.activemq.artemis.api.core.BaseInterceptor; import org.apache.activemq.artemis.api.core.SimpleString; import org.apache.activemq.artemis.core.server.ActiveMQServerLogger; import org.apache.activemq.artemis.api.core.RoutingType; +import org.apache.activemq.artemis.core.server.balancing.RedirectHandler; -public abstract class AbstractProtocolManager, C extends RemotingConnection> implements ProtocolManager { +public abstract class AbstractProtocolManager, C extends RemotingConnection, R extends RedirectHandler> implements ProtocolManager { private final Map prefixes = new HashMap<>(); diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/protocol/ProtocolManager.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/protocol/ProtocolManager.java index 770034de15..ee6ec226c6 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/protocol/ProtocolManager.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/protocol/ProtocolManager.java @@ -25,12 +25,13 @@ import org.apache.activemq.artemis.api.core.BaseInterceptor; import org.apache.activemq.artemis.api.core.RoutingType; import org.apache.activemq.artemis.api.core.SimpleString; import org.apache.activemq.artemis.core.remoting.impl.netty.NettyServerConnection; +import org.apache.activemq.artemis.core.server.balancing.RedirectHandler; import org.apache.activemq.artemis.spi.core.remoting.Acceptor; import org.apache.activemq.artemis.spi.core.remoting.Connection; /** * Info: ProtocolManager is loaded by {@link org.apache.activemq.artemis.core.remoting.server.impl.RemotingServiceImpl#loadProtocolManagerFactories(Iterable)} */ -public interface ProtocolManager

{ +public interface ProtocolManager

{ ProtocolManagerFactory

getFactory(); @@ -78,4 +79,6 @@ public interface ProtocolManager

{ void setSecurityDomain(String securityDomain); String getSecurityDomain(); + + R getRedirectHandler(); } diff --git a/artemis-server/src/main/resources/schema/artemis-configuration.xsd b/artemis-server/src/main/resources/schema/artemis-configuration.xsd index 69e06cdcc2..e35e1f33a6 100644 --- a/artemis-server/src/main/resources/schema/artemis-configuration.xsd +++ b/artemis-server/src/main/resources/schema/artemis-configuration.xsd @@ -619,6 +619,20 @@ + + + + A list of balancers + + + + + + + + + + @@ -2078,6 +2092,167 @@ + + + + + + the optional target key + + + + + + + the filter for the target key + + + + + + + the filter to get the local target + + + + + + + the time period for a cache entry to remain active + + + + + + + the policy configuration + + + + + + + the pool configuration + + + + + + + + a unique name for the broker balancer + + + + + + + + + + + + + + + + + + + + + properties to configure a policy + + + + + + + + the name of the policy + + + + + + + + + + + + the username to access the targets + + + + + + + the password to access the targets + + + + + + + the period (in milliseconds) used to check if a target is ready + + + + + + + the minimum number of ready targets + + + + + + + the timeout (in milliseconds) used to get the minimum number of ready targets + + + + + + + true means that the local target is enabled + + + + + + + + the name of a cluster connection + + + + + + + + + + + + + + + + + name of discovery group used by this bridge + + + + + + + + + + + diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/config/impl/FileConfigurationTest.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/config/impl/FileConfigurationTest.java index 07f11ab558..b7027b458e 100644 --- a/artemis-server/src/test/java/org/apache/activemq/artemis/core/config/impl/FileConfigurationTest.java +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/config/impl/FileConfigurationTest.java @@ -46,6 +46,7 @@ import org.apache.activemq.artemis.core.config.DivertConfiguration; import org.apache.activemq.artemis.core.config.FileDeploymentManager; import org.apache.activemq.artemis.core.config.HAPolicyConfiguration; import org.apache.activemq.artemis.core.config.MetricsConfiguration; +import org.apache.activemq.artemis.core.config.balancing.BrokerBalancerConfiguration; import org.apache.activemq.artemis.core.config.ha.LiveOnlyPolicyConfiguration; import org.apache.activemq.artemis.core.journal.impl.JournalImpl; import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants; @@ -54,6 +55,10 @@ import org.apache.activemq.artemis.core.server.ComponentConfigurationRoutingType import org.apache.activemq.artemis.core.server.JournalType; import org.apache.activemq.artemis.core.server.Queue; import org.apache.activemq.artemis.core.server.SecuritySettingPlugin; +import org.apache.activemq.artemis.core.server.balancing.policies.ConsistentHashPolicy; +import org.apache.activemq.artemis.core.server.balancing.policies.FirstElementPolicy; +import org.apache.activemq.artemis.core.server.balancing.policies.LeastConnectionsPolicy; +import org.apache.activemq.artemis.core.server.balancing.targets.TargetKey; import org.apache.activemq.artemis.core.server.cluster.impl.MessageLoadBalancingType; import org.apache.activemq.artemis.core.server.impl.ActiveMQServerImpl; import org.apache.activemq.artemis.core.server.impl.LegacyLDAPSecuritySettingPlugin; @@ -260,6 +265,38 @@ public class FileConfigurationTest extends ConfigurationImplTest { } } + Assert.assertEquals(3, conf.getBalancerConfigurations().size()); + for (BrokerBalancerConfiguration bc : conf.getBalancerConfigurations()) { + if (bc.getName().equals("simple-balancer")) { + Assert.assertEquals(bc.getTargetKey(), TargetKey.USER_NAME); + Assert.assertNull(bc.getLocalTargetFilter()); + Assert.assertEquals(bc.getPolicyConfiguration().getName(), FirstElementPolicy.NAME); + Assert.assertEquals(false, bc.getPoolConfiguration().isLocalTargetEnabled()); + Assert.assertEquals("connector1", bc.getPoolConfiguration().getStaticConnectors().get(0)); + Assert.assertEquals(null, bc.getPoolConfiguration().getDiscoveryGroupName()); + } else if (bc.getName().equals("consistent-hash-balancer")) { + Assert.assertEquals(bc.getTargetKey(), TargetKey.SNI_HOST); + Assert.assertEquals(bc.getTargetKeyFilter(), "^[^.]+"); + Assert.assertEquals(bc.getLocalTargetFilter(), "DEFAULT"); + Assert.assertEquals(bc.getPolicyConfiguration().getName(), ConsistentHashPolicy.NAME); + Assert.assertEquals(1000, bc.getPoolConfiguration().getCheckPeriod()); + Assert.assertEquals(true, bc.getPoolConfiguration().isLocalTargetEnabled()); + Assert.assertEquals(Collections.emptyList(), bc.getPoolConfiguration().getStaticConnectors()); + Assert.assertEquals("dg1", bc.getPoolConfiguration().getDiscoveryGroupName()); + } else { + Assert.assertEquals(bc.getTargetKey(), TargetKey.SOURCE_IP); + Assert.assertEquals("least-connections-balancer", bc.getName()); + Assert.assertEquals(60000, bc.getCacheTimeout()); + Assert.assertEquals(bc.getPolicyConfiguration().getName(), LeastConnectionsPolicy.NAME); + Assert.assertEquals(3000, bc.getPoolConfiguration().getCheckPeriod()); + Assert.assertEquals(2, bc.getPoolConfiguration().getQuorumSize()); + Assert.assertEquals(1000, bc.getPoolConfiguration().getQuorumTimeout()); + Assert.assertEquals(false, bc.getPoolConfiguration().isLocalTargetEnabled()); + Assert.assertEquals(Collections.emptyList(), bc.getPoolConfiguration().getStaticConnectors()); + Assert.assertEquals("dg2", bc.getPoolConfiguration().getDiscoveryGroupName()); + } + } + Assert.assertEquals(4, conf.getBridgeConfigurations().size()); for (BridgeConfiguration bc : conf.getBridgeConfigurations()) { if (bc.getName().equals("bridge1")) { diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/policies/ConsistentHashPolicyTest.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/policies/ConsistentHashPolicyTest.java new file mode 100644 index 0000000000..d81677ec5b --- /dev/null +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/policies/ConsistentHashPolicyTest.java @@ -0,0 +1,55 @@ +/** + * 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.activemq.artemis.core.server.balancing.policies; + +import org.apache.activemq.artemis.core.server.balancing.targets.MockTarget; +import org.apache.activemq.artemis.core.server.balancing.targets.Target; +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; + +public class ConsistentHashPolicyTest extends PolicyTestBase { + + @Override + protected AbstractPolicy createPolicy() { + return new ConsistentHashPolicy(); + } + + @Test + public void testPolicyWithMultipleTargets() { + AbstractPolicy policy = createPolicy(); + Target selectedTarget; + Target previousTarget; + + ArrayList targets = new ArrayList<>(); + for (int i = 0; i < MULTIPLE_TARGETS; i++) { + targets.add(new MockTarget()); + } + + selectedTarget = policy.selectTarget(targets, "test"); + previousTarget = selectedTarget; + + selectedTarget = policy.selectTarget(targets, "test"); + Assert.assertEquals(previousTarget, selectedTarget); + + targets.remove(previousTarget); + selectedTarget = policy.selectTarget(targets, "test"); + Assert.assertNotEquals(previousTarget, selectedTarget); + } +} diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/policies/FirstElementPolicyTest.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/policies/FirstElementPolicyTest.java new file mode 100644 index 0000000000..b84c8e0080 --- /dev/null +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/policies/FirstElementPolicyTest.java @@ -0,0 +1,47 @@ +/** + * 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.activemq.artemis.core.server.balancing.policies; + +import org.apache.activemq.artemis.core.server.balancing.targets.MockTarget; +import org.apache.activemq.artemis.core.server.balancing.targets.Target; +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; + +public class FirstElementPolicyTest extends PolicyTestBase { + + @Override + protected AbstractPolicy createPolicy() { + return new FirstElementPolicy(); + } + + @Test + public void testPolicyWithMultipleTargets() { + AbstractPolicy policy = createPolicy(); + + ArrayList targets = new ArrayList<>(); + for (int i = 0; i < MULTIPLE_TARGETS; i++) { + targets.add(new MockTarget()); + } + + Target selectedTarget = policy.selectTarget(targets, "test"); + + Assert.assertEquals(selectedTarget, targets.get(0)); + } +} diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/policies/LeastConnectionsPolicyTest.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/policies/LeastConnectionsPolicyTest.java new file mode 100644 index 0000000000..b272f49d66 --- /dev/null +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/policies/LeastConnectionsPolicyTest.java @@ -0,0 +1,95 @@ +/** + * 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.activemq.artemis.core.server.balancing.policies; + +import org.apache.activemq.artemis.core.server.balancing.targets.MockTarget; +import org.apache.activemq.artemis.core.server.balancing.targets.Target; +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +public class LeastConnectionsPolicyTest extends PolicyTestBase { + + @Override + protected AbstractPolicy createPolicy() { + return new LeastConnectionsPolicy(); + } + + @Test + public void testPolicyWithMultipleTargets() { + AbstractPolicy policy = createPolicy(); + Target selectedTarget = null; + Set selectedTargets; + + + ArrayList targets = new ArrayList<>(); + for (int i = 0; i < MULTIPLE_TARGETS; i++) { + targets.add(new MockTarget().setConnected(true).setReady(true)); + } + + + selectedTargets = new HashSet<>(); + for (int i = 0; i < MULTIPLE_TARGETS; i++) { + selectedTarget = policy.selectTarget(targets, "test"); + selectedTargets.add(selectedTarget); + } + Assert.assertEquals(MULTIPLE_TARGETS, selectedTargets.size()); + + + targets.forEach(target -> { + ((MockTarget)target).setAttributeValue("broker", "ConnectionCount", 3); + policy.getTargetProbe().check(target); + }); + + selectedTargets = new HashSet<>(); + for (int i = 0; i < MULTIPLE_TARGETS; i++) { + selectedTarget = policy.selectTarget(targets, "test"); + selectedTargets.add(selectedTarget); + } + Assert.assertEquals(MULTIPLE_TARGETS, selectedTargets.size()); + + + ((MockTarget)targets.get(0)).setAttributeValue("broker", "ConnectionCount", 2); + targets.forEach(target -> policy.getTargetProbe().check(target)); + + selectedTargets = new HashSet<>(); + for (int i = 0; i < MULTIPLE_TARGETS; i++) { + selectedTarget = policy.selectTarget(targets, "test"); + selectedTargets.add(selectedTarget); + } + Assert.assertEquals(1, selectedTargets.size()); + Assert.assertTrue(selectedTargets.contains(targets.get(0))); + + + ((MockTarget)targets.get(1)).setAttributeValue("broker", "ConnectionCount", 1); + ((MockTarget)targets.get(2)).setAttributeValue("broker", "ConnectionCount", 1); + targets.forEach(target -> policy.getTargetProbe().check(target)); + + selectedTargets = new HashSet<>(); + for (int i = 0; i < MULTIPLE_TARGETS; i++) { + selectedTarget = policy.selectTarget(targets, "test"); + selectedTargets.add(selectedTarget); + } + Assert.assertEquals(2, selectedTargets.size()); + Assert.assertTrue(selectedTargets.contains(targets.get(1))); + Assert.assertTrue(selectedTargets.contains(targets.get(2))); + } +} diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/policies/PolicyTestBase.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/policies/PolicyTestBase.java new file mode 100644 index 0000000000..962010bb22 --- /dev/null +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/policies/PolicyTestBase.java @@ -0,0 +1,52 @@ +/** + * 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.activemq.artemis.core.server.balancing.policies; + +import org.apache.activemq.artemis.core.server.balancing.targets.MockTarget; +import org.apache.activemq.artemis.core.server.balancing.targets.Target; +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Collections; + +public abstract class PolicyTestBase { + public static final int MULTIPLE_TARGETS = 10; + + protected abstract AbstractPolicy createPolicy(); + + @Test + public void testPolicyWithNoTarget() { + AbstractPolicy policy = createPolicy(); + + Target selectedTarget = policy.selectTarget(Collections.emptyList(), "test"); + + Assert.assertNull(selectedTarget); + } + + @Test + public void testPolicyWithSingleTarget() { + AbstractPolicy policy = createPolicy(); + + ArrayList targets = new ArrayList<>(); + targets.add(new MockTarget()); + + Target selectedTarget = policy.selectTarget(targets, "test"); + Assert.assertEquals(selectedTarget, targets.get(0)); + } +} diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/policies/RoundRobinPolicyTest.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/policies/RoundRobinPolicyTest.java new file mode 100644 index 0000000000..70b7b74f04 --- /dev/null +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/policies/RoundRobinPolicyTest.java @@ -0,0 +1,58 @@ +/** + * 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.activemq.artemis.core.server.balancing.policies; + +import org.apache.activemq.artemis.core.server.balancing.targets.MockTarget; +import org.apache.activemq.artemis.core.server.balancing.targets.Target; +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class RoundRobinPolicyTest extends PolicyTestBase { + + @Override + protected AbstractPolicy createPolicy() { + return new RoundRobinPolicy(); + } + + @Test + public void testPolicyWithMultipleTargets() { + AbstractPolicy policy = createPolicy(); + Target selectedTarget = null; + Set selectedTargets = new HashSet<>(); + List previousTargets = new ArrayList<>(); + + ArrayList targets = new ArrayList<>(); + for (int i = 0; i < MULTIPLE_TARGETS; i++) { + targets.add(new MockTarget()); + } + + selectedTargets = new HashSet<>(); + for (int i = 0; i < MULTIPLE_TARGETS; i++) { + selectedTarget = policy.selectTarget(targets, "test"); + selectedTargets.add(selectedTarget); + Assert.assertTrue("Iteration failed: " + i, !previousTargets.contains(selectedTarget)); + previousTargets.add(selectedTarget); + } + Assert.assertEquals(MULTIPLE_TARGETS, selectedTargets.size()); + } +} diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/pools/DiscoveryPoolTest.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/pools/DiscoveryPoolTest.java new file mode 100644 index 0000000000..b637ebe808 --- /dev/null +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/pools/DiscoveryPoolTest.java @@ -0,0 +1,174 @@ +/** + * 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.activemq.artemis.core.server.balancing.pools; + +import org.apache.activemq.artemis.core.server.balancing.targets.MockTargetFactory; +import org.apache.activemq.artemis.core.server.balancing.targets.MockTargetProbe; +import org.apache.activemq.artemis.core.server.balancing.targets.TargetFactory; +import org.apache.activemq.artemis.utils.Wait; +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.stream.Stream; + +public class DiscoveryPoolTest extends PoolTestBase { + + @Test + public void testPoolAddingRemovingAllEntries() throws Exception { + testPoolChangingEntries(5, 10, 10); + } + + @Test + public void testPoolAddingRemovingPartialEntries() throws Exception { + testPoolChangingEntries(5, 10, 5); + } + + @Test + public void testPoolAddingRemovingAllEntriesAfterStart() throws Exception { + testPoolChangingEntries(0, 10, 10); + } + + @Test + public void testPoolAddingRemovingPartialEntriesAfterStart() throws Exception { + testPoolChangingEntries(0, 10, 5); + } + + private void testPoolChangingEntries(int initialEntries, int addingEntries, int removingEntries) throws Exception { + MockTargetFactory targetFactory = new MockTargetFactory(); + MockTargetProbe targetProbe = new MockTargetProbe("TEST", true); + MockDiscoveryService discoveryService = new MockDiscoveryService(); + + targetProbe.setChecked(true); + + // Simulate initial entries. + List initialNodeIDs = new ArrayList<>(); + for (int i = 0; i < initialEntries; i++) { + initialNodeIDs.add(discoveryService.addEntry().getNodeID()); + } + + Pool pool = createDiscoveryPool(targetFactory, discoveryService); + + pool.addTargetProbe(targetProbe); + + pool.start(); + + try { + targetFactory.getCreatedTargets().forEach(mockTarget -> mockTarget.setConnectable(true)); + targetFactory.getCreatedTargets().forEach(mockTarget -> mockTarget.setReady(true)); + + Wait.assertEquals(initialEntries, () -> pool.getTargets().size(), CHECK_TIMEOUT); + Assert.assertEquals(initialEntries, pool.getAllTargets().size()); + Assert.assertEquals(initialEntries, targetFactory.getCreatedTargets().size()); + initialNodeIDs.forEach(nodeID -> Assert.assertTrue(pool.isTargetReady(pool.getTarget(nodeID)))); + + // Simulate adding entries. + List addedNodeIDs = new ArrayList<>(); + for (int i = 0; i < addingEntries; i++) { + addedNodeIDs.add(discoveryService.addEntry().getNodeID()); + } + + Assert.assertEquals(initialEntries, pool.getTargets().size()); + Assert.assertEquals(initialEntries + addingEntries, pool.getAllTargets().size()); + Assert.assertEquals(initialEntries + addingEntries, targetFactory.getCreatedTargets().size()); + initialNodeIDs.forEach(nodeID -> { + Assert.assertTrue(pool.isTargetReady(pool.getTarget(nodeID))); + Assert.assertTrue(targetProbe.getTargetExecutions(pool.getTarget(nodeID)) > 0); + }); + addedNodeIDs.forEach(nodeID -> { + Assert.assertFalse(pool.isTargetReady(pool.getTarget(nodeID))); + Assert.assertEquals(0, targetProbe.getTargetExecutions(pool.getTarget(nodeID))); + }); + + + targetFactory.getCreatedTargets().forEach(mockTarget -> mockTarget.setConnectable(true)); + + Assert.assertEquals(initialEntries, pool.getTargets().size()); + Assert.assertEquals(initialEntries + addingEntries, pool.getAllTargets().size()); + Assert.assertEquals(initialEntries + addingEntries, targetFactory.getCreatedTargets().size()); + initialNodeIDs.forEach(nodeID -> { + Assert.assertTrue(pool.isTargetReady(pool.getTarget(nodeID))); + Assert.assertTrue(targetProbe.getTargetExecutions(pool.getTarget(nodeID)) > 0); + }); + addedNodeIDs.forEach(nodeID -> { + Assert.assertFalse(pool.isTargetReady(pool.getTarget(nodeID))); + Assert.assertEquals(0, targetProbe.getTargetExecutions(pool.getTarget(nodeID))); + }); + + targetFactory.getCreatedTargets().forEach(mockTarget -> mockTarget.setReady(true)); + + Wait.assertEquals(initialEntries + addingEntries, () -> pool.getTargets().size(), CHECK_TIMEOUT); + Assert.assertEquals(initialEntries + addingEntries, pool.getAllTargets().size()); + Assert.assertEquals(initialEntries + addingEntries, targetFactory.getCreatedTargets().size()); + Stream.concat(initialNodeIDs.stream(), addedNodeIDs.stream()).forEach(nodeID -> { + Assert.assertTrue(pool.isTargetReady(pool.getTarget(nodeID))); + Assert.assertTrue(targetProbe.getTargetExecutions(pool.getTarget(nodeID)) > 0); + }); + + if (removingEntries > 0) { + // Simulate removing entries. + List removingNodeIDs = new ArrayList<>(); + for (int i = 0; i < removingEntries; i++) { + removingNodeIDs.add(discoveryService.removeEntry(targetFactory. + getCreatedTargets().get(i).getNodeID()).getNodeID()); + } + + Assert.assertEquals(initialEntries + addingEntries - removingEntries, pool.getTargets().size()); + Assert.assertEquals(initialEntries + addingEntries - removingEntries, pool.getAllTargets().size()); + Assert.assertEquals(initialEntries + addingEntries, targetFactory.getCreatedTargets().size()); + Stream.concat(initialNodeIDs.stream(), addedNodeIDs.stream()).forEach(nodeID -> { + if (removingNodeIDs.contains(nodeID)) { + Assert.assertNull(pool.getTarget(nodeID)); + Assert.assertEquals(0, targetProbe.getTargetExecutions(pool.getTarget(nodeID))); + } else { + Assert.assertTrue(pool.isTargetReady(pool.getTarget(nodeID))); + Assert.assertTrue(targetProbe.getTargetExecutions(pool.getTarget(nodeID)) > 0); + } + }); + } else { + Assert.assertEquals(initialEntries + addingEntries, pool.getTargets().size()); + Assert.assertEquals(initialEntries + addingEntries, pool.getAllTargets().size()); + Assert.assertEquals(initialEntries + addingEntries, targetFactory.getCreatedTargets().size()); + Stream.concat(initialNodeIDs.stream(), addedNodeIDs.stream()).forEach(nodeID -> { + Assert.assertTrue(pool.isTargetReady(pool.getTarget(nodeID))); + Assert.assertTrue(targetProbe.getTargetExecutions(pool.getTarget(nodeID)) > 0); + }); + } + } finally { + pool.stop(); + } + } + + + @Override + protected Pool createPool(TargetFactory targetFactory, int targets) { + MockDiscoveryService discoveryService = new MockDiscoveryService(); + + for (int i = 0; i < targets; i++) { + discoveryService.addEntry(); + } + + return createDiscoveryPool(targetFactory, discoveryService); + } + + private DiscoveryPool createDiscoveryPool(TargetFactory targetFactory, DiscoveryService discoveryService) { + return new DiscoveryPool(targetFactory, new ScheduledThreadPoolExecutor(0), CHECK_PERIOD, discoveryService); + } +} diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/pools/MockDiscoveryService.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/pools/MockDiscoveryService.java new file mode 100644 index 0000000000..f9d19828ec --- /dev/null +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/pools/MockDiscoveryService.java @@ -0,0 +1,87 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.activemq.artemis.core.server.balancing.pools; + +import org.apache.activemq.artemis.api.core.TransportConfiguration; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class MockDiscoveryService extends DiscoveryService { + private final Map entries = new HashMap<>(); + + private final Map pendingEntries = new HashMap<>(); + + private volatile boolean started; + + + public Map getEntries() { + return entries; + } + + public Map getPendingEntries() { + return pendingEntries; + } + + @Override + public boolean isStarted() { + return started; + } + + public Entry addEntry() { + return addEntry(new Entry(UUID.randomUUID().toString(), new TransportConfiguration())); + } + + public Entry addEntry(Entry entry) { + if (started) { + entries.put(entry.getNodeID(), entry); + fireEntryAddedEvent(entry); + } else { + pendingEntries.put(entry.getNodeID(), entry); + } + + return entry; + } + + public Entry removeEntry(String nodeID) { + if (started) { + Entry removedEntry = entries.remove(nodeID); + fireEntryRemovedEvent(removedEntry); + return removedEntry; + } else { + return pendingEntries.remove(nodeID); + } + } + + + @Override + public void start() throws Exception { + started = true; + + pendingEntries.forEach((nodeID, entry) -> { + entries.put(nodeID, entry); + fireEntryAddedEvent(entry); + }); + } + + @Override + public void stop() throws Exception { + started = false; + } +} diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/pools/PoolTestBase.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/pools/PoolTestBase.java new file mode 100644 index 0000000000..1f7505ebd8 --- /dev/null +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/pools/PoolTestBase.java @@ -0,0 +1,190 @@ +/** + * 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.activemq.artemis.core.server.balancing.pools; + +import org.apache.activemq.artemis.core.server.balancing.targets.MockTargetFactory; +import org.apache.activemq.artemis.core.server.balancing.targets.MockTargetProbe; +import org.apache.activemq.artemis.core.server.balancing.targets.TargetFactory; +import org.apache.activemq.artemis.utils.Wait; +import org.junit.Assert; +import org.junit.Test; + +public abstract class PoolTestBase { + public static final int MULTIPLE_TARGETS = 10; + + public static final int CHECK_PERIOD = 100; + public static final int CHECK_TIMEOUT = 2 * CHECK_PERIOD; + + + protected abstract Pool createPool(TargetFactory targetFactory, int targets); + + + @Test + public void testPoolWithNoTargets() throws Exception { + testPoolTargets(0); + } + + @Test + public void testPoolWithSingleTarget() throws Exception { + testPoolTargets(1); + } + + @Test + public void testPoolWithMultipleTargets() throws Exception { + testPoolTargets(MULTIPLE_TARGETS); + } + + @Test + public void testPoolQuorumWithMultipleTargets() throws Exception { + final int targets = MULTIPLE_TARGETS; + final int quorumSize = 2; + + Assert.assertTrue(targets - quorumSize > 2); + + MockTargetFactory targetFactory = new MockTargetFactory().setConnectable(true).setReady(true); + Pool pool = createPool(targetFactory, targets); + + pool.setQuorumSize(quorumSize); + + Assert.assertEquals(0, pool.getTargets().size()); + + pool.start(); + + Wait.assertEquals(targets, () -> pool.getTargets().size(), CHECK_TIMEOUT); + + targetFactory.getCreatedTargets().stream().limit(targets - quorumSize + 1) + .forEach(mockTarget -> mockTarget.setReady(false)); + + Wait.assertEquals(0, () -> pool.getTargets().size(), CHECK_TIMEOUT); + + targetFactory.getCreatedTargets().get(0).setReady(true); + + Wait.assertEquals(quorumSize, () -> pool.getTargets().size(), CHECK_TIMEOUT); + + pool.setQuorumSize(quorumSize + 1); + + Wait.assertEquals(0, () -> pool.getTargets().size(), CHECK_TIMEOUT); + + targetFactory.getCreatedTargets().get(1).setReady(true); + + Wait.assertEquals(quorumSize + 1, () -> pool.getTargets().size(), CHECK_TIMEOUT); + } + + + private void testPoolTargets(int targets) throws Exception { + MockTargetFactory targetFactory = new MockTargetFactory(); + MockTargetProbe targetProbe = new MockTargetProbe("TEST", false); + Pool pool = createPool(targetFactory, targets); + + pool.addTargetProbe(targetProbe); + + Assert.assertEquals(0, pool.getTargets().size()); + Assert.assertEquals(0, pool.getAllTargets().size()); + Assert.assertEquals(0, targetFactory.getCreatedTargets().size()); + targetFactory.getCreatedTargets().forEach(mockTarget -> { + Assert.assertFalse(pool.isTargetReady(mockTarget)); + Assert.assertEquals(0, targetProbe.getTargetExecutions(mockTarget)); + }); + + pool.start(); + + try { + Assert.assertEquals(0, pool.getTargets().size()); + Assert.assertEquals(targets, pool.getAllTargets().size()); + Assert.assertEquals(targets, targetFactory.getCreatedTargets().size()); + targetFactory.getCreatedTargets().forEach(mockTarget -> { + Assert.assertFalse(pool.isTargetReady(mockTarget)); + Assert.assertEquals(0, targetProbe.getTargetExecutions(mockTarget)); + }); + + if (targets > 0) { + targetFactory.getCreatedTargets().forEach(mockTarget -> mockTarget.setConnectable(true)); + + Assert.assertEquals(0, pool.getTargets().size()); + Assert.assertEquals(targets, pool.getAllTargets().size()); + Assert.assertEquals(targets, targetFactory.getCreatedTargets().size()); + targetFactory.getCreatedTargets().forEach(mockTarget -> { + Assert.assertFalse(pool.isTargetReady(mockTarget)); + Assert.assertEquals(0, targetProbe.getTargetExecutions(mockTarget)); + }); + + targetFactory.getCreatedTargets().forEach(mockTarget -> mockTarget.setReady(true)); + + Assert.assertEquals(0, pool.getTargets().size()); + Assert.assertEquals(targets, pool.getAllTargets().size()); + Assert.assertEquals(targets, targetFactory.getCreatedTargets().size()); + targetFactory.getCreatedTargets().forEach(mockTarget -> { + Assert.assertFalse(pool.isTargetReady(mockTarget)); + Wait.assertTrue(() -> targetProbe.getTargetExecutions(mockTarget) > 0, CHECK_TIMEOUT); + }); + + targetProbe.setChecked(true); + + Wait.assertEquals(targets, () -> pool.getTargets().size(), CHECK_TIMEOUT); + Assert.assertEquals(targets, pool.getAllTargets().size()); + Assert.assertEquals(targets, targetFactory.getCreatedTargets().size()); + targetFactory.getCreatedTargets().forEach(mockTarget -> { + Assert.assertTrue(pool.isTargetReady(mockTarget)); + Assert.assertTrue(targetProbe.getTargetExecutions(mockTarget) > 0); + }); + + targetFactory.getCreatedTargets().forEach(mockTarget -> { + mockTarget.setConnectable(false); + try { + mockTarget.disconnect(); + } catch (Exception ignore) { + } + }); + + Wait.assertEquals(0, () -> pool.getTargets().size(), CHECK_TIMEOUT); + Assert.assertEquals(targets, pool.getAllTargets().size()); + Assert.assertEquals(targets, targetFactory.getCreatedTargets().size()); + targetFactory.getCreatedTargets().forEach(mockTarget -> { + Assert.assertFalse(pool.isTargetReady(mockTarget)); + Assert.assertTrue(targetProbe.getTargetExecutions(mockTarget) > 0); + }); + + targetProbe.clearTargetExecutions(); + + targetFactory.getCreatedTargets().forEach(mockTarget -> mockTarget.setConnectable(true)); + + Wait.assertEquals(targets, () -> pool.getTargets().size(), CHECK_TIMEOUT); + Assert.assertEquals(targets, pool.getAllTargets().size()); + Assert.assertEquals(targets, targetFactory.getCreatedTargets().size()); + targetFactory.getCreatedTargets().forEach(mockTarget -> { + Assert.assertTrue(pool.isTargetReady(mockTarget)); + Assert.assertTrue(targetProbe.getTargetExecutions(mockTarget) > 0); + }); + + targetProbe.clearTargetExecutions(); + + targetProbe.setChecked(false); + + Wait.assertEquals(0, () -> pool.getTargets().size(), CHECK_TIMEOUT); + Assert.assertEquals(targets, pool.getAllTargets().size()); + Assert.assertEquals(targets, targetFactory.getCreatedTargets().size()); + targetFactory.getCreatedTargets().forEach(mockTarget -> { + Assert.assertFalse(pool.isTargetReady(mockTarget)); + Assert.assertTrue(targetProbe.getTargetExecutions(mockTarget) > 0); + }); + } + } finally { + pool.stop(); + } + } +} diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/pools/StaticPoolTest.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/pools/StaticPoolTest.java new file mode 100644 index 0000000000..6a489071ac --- /dev/null +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/pools/StaticPoolTest.java @@ -0,0 +1,39 @@ +/** + * 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.activemq.artemis.core.server.balancing.pools; + +import org.apache.activemq.artemis.api.core.TransportConfiguration; +import org.apache.activemq.artemis.core.server.balancing.targets.TargetFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ScheduledThreadPoolExecutor; + +public class StaticPoolTest extends PoolTestBase { + + @Override + protected Pool createPool(TargetFactory targetFactory, int targets) { + List staticConnectors = new ArrayList<>(); + + for (int i = 0; i < targets; i++) { + staticConnectors.add(new TransportConfiguration()); + } + + return new StaticPool(targetFactory, new ScheduledThreadPoolExecutor(0), CHECK_PERIOD, staticConnectors); + } +} diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/targets/MockTarget.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/targets/MockTarget.java new file mode 100644 index 0000000000..3c31d6f0a4 --- /dev/null +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/targets/MockTarget.java @@ -0,0 +1,156 @@ +/** + * 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.activemq.artemis.core.server.balancing.targets; + +import org.apache.activemq.artemis.api.core.TransportConfiguration; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class MockTarget extends AbstractTarget { + private boolean local = false; + + private boolean connected = false; + + private boolean connectable = false; + + private boolean ready = false; + + private Map attributeValues = new HashMap<>(); + + private Map operationReturnValues = new HashMap<>(); + + + @Override + public boolean isLocal() { + return false; + } + + public MockTarget setLocal(boolean local) { + this.local = local; + return this; + } + + @Override + public boolean isConnected() { + return connected; + } + + public boolean isConnectable() { + return connectable; + } + + public MockTarget setConnected(boolean connected) { + this.connected = connected; + return this; + } + + public MockTarget setConnectable(boolean connectable) { + this.connectable = connectable; + return this; + } + + public boolean isReady() { + return ready; + } + + public MockTarget setReady(boolean ready) { + this.ready = ready; + return this; + } + + public Map getAttributeValues() { + return attributeValues; + } + + public void setAttributeValues(Map attributeValues) { + this.attributeValues = attributeValues; + } + + public Map getOperationReturnValues() { + return operationReturnValues; + } + + public void setOperationReturnValues(Map operationReturnValues) { + this.operationReturnValues = operationReturnValues; + } + + public MockTarget() { + this(new TransportConfiguration(), UUID.randomUUID().toString()); + } + + public MockTarget(TransportConfiguration connector, String nodeID) { + super(connector, nodeID); + } + + @Override + public void connect() throws Exception { + if (!connectable) { + throw new IllegalStateException("Target not connectable"); + } + + if (getNodeID() == null) { + setNodeID(UUID.randomUUID().toString()); + } + + connected = true; + + fireConnectedEvent(); + } + + @Override + public void disconnect() throws Exception { + connected = false; + + fireDisconnectedEvent(); + } + + @Override + public boolean checkReadiness() { + return ready; + } + + @Override + public T getAttribute(String resourceName, String attributeName, Class attributeClass, int timeout) throws Exception { + checkConnection(); + + return (T)attributeValues.get(resourceName + attributeName); + } + + @Override + public T invokeOperation(String resourceName, String operationName, Object[] operationParams, Class operationClass, int timeout) throws Exception { + checkConnection(); + + return (T)operationReturnValues.get(resourceName + operationName); + } + + public void setAttributeValue(String resourceName, String attributeName, Object value) { + attributeValues.put(resourceName + attributeName, value); + } + + public void setOperationReturnValue(String resourceName, String attributeName, Object value) { + operationReturnValues.put(resourceName + attributeName, value); + } + + private void checkConnection() { + if (!connected) { + throw new IllegalStateException("Target not connected"); + } + } +} diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/targets/MockTargetFactory.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/targets/MockTargetFactory.java new file mode 100644 index 0000000000..e0e0933192 --- /dev/null +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/targets/MockTargetFactory.java @@ -0,0 +1,105 @@ +/** + * 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.activemq.artemis.core.server.balancing.targets; + +import org.apache.activemq.artemis.api.core.TransportConfiguration; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class MockTargetFactory extends AbstractTargetFactory { + + private final List createdTargets = new ArrayList<>(); + + private Boolean connectable = null; + + private Boolean ready = null; + + private Map attributeValues = null; + + private Map operationReturnValues = null; + + public Boolean getConnectable() { + return connectable; + } + + public MockTargetFactory setConnectable(Boolean connectable) { + this.connectable = connectable; + return this; + } + + public Boolean getReady() { + return ready; + } + + public MockTargetFactory setReady(Boolean ready) { + this.ready = ready; + return this; + } + + public Map getAttributeValues() { + return attributeValues; + } + + public MockTargetFactory setAttributeValues(Map attributeValues) { + this.attributeValues = attributeValues; + return this; + } + + public Map getOperationReturnValues() { + return operationReturnValues; + } + + public MockTargetFactory setOperationReturnValues(Map operationReturnValues) { + this.operationReturnValues = operationReturnValues; + return this; + } + + public List getCreatedTargets() { + return createdTargets; + } + + @Override + public Target createTarget(TransportConfiguration connector, String nodeID) { + MockTarget target = new MockTarget(connector, nodeID); + + target.setUsername(getUsername()); + target.setPassword(getPassword()); + + createdTargets.add(target); + + if (connectable != null) { + target.setConnectable(connectable); + } + + if (ready != null) { + target.setReady(ready); + } + + if (attributeValues != null) { + target.setAttributeValues(attributeValues); + } + + if (operationReturnValues != null) { + target.setOperationReturnValues(operationReturnValues); + } + + return target; + } +} diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/targets/MockTargetProbe.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/targets/MockTargetProbe.java new file mode 100644 index 0000000000..d8c3aa2e48 --- /dev/null +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/targets/MockTargetProbe.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.activemq.artemis.core.server.balancing.targets; + +import java.util.HashMap; +import java.util.Map; + +public class MockTargetProbe extends TargetProbe { + private final Map targetExecutions = new HashMap<>(); + + private boolean checked; + + public boolean isChecked() { + return checked; + } + + public void setChecked(boolean checked) { + this.checked = checked; + } + + public MockTargetProbe(String name, boolean checked) { + super(name); + + this.checked = checked; + } + + public int getTargetExecutions(Target target) { + Integer executions = targetExecutions.get(target); + return executions != null ? executions : 0; + } + + public int setTargetExecutions(Target target, int executions) { + return targetExecutions.put(target, executions); + } + + public void clearTargetExecutions() { + targetExecutions.clear(); + } + + @Override + public boolean check(Target target) { + targetExecutions.compute(target, (t, e) -> e == null ? 1 : e++); + + return checked; + } +} diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/targets/TargetKeyResolverTest.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/targets/TargetKeyResolverTest.java new file mode 100644 index 0000000000..59336969ba --- /dev/null +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/balancing/targets/TargetKeyResolverTest.java @@ -0,0 +1,110 @@ +/** + * 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.activemq.artemis.core.server.balancing.targets; + +import org.apache.activemq.artemis.spi.core.remoting.Connection; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +public class TargetKeyResolverTest { + + @Test + public void testClientIDKey() { + testClientIDKey("TEST", "TEST", null); + } + + @Test + public void testClientIDKeyWithFilter() { + testClientIDKey("TEST", "TEST1234", "^.{4}"); + } + + private void testClientIDKey(String expected, String clientID, String filter) { + TargetKeyResolver targetKeyResolver = new TargetKeyResolver(TargetKey.CLIENT_ID, filter); + + Assert.assertEquals(expected, targetKeyResolver.resolve(null, clientID, null)); + + Assert.assertEquals(TargetKeyResolver.DEFAULT_KEY_VALUE, targetKeyResolver.resolve(null, null, null)); + } + + @Test + public void testSNIHostKey() { + testSNIHostKey("TEST", "TEST", null); + } + + @Test + public void testSNIHostKeyWithFilter() { + testSNIHostKey("TEST", "TEST1234", "^.{4}"); + } + + private void testSNIHostKey(String expected, String sniHost, String filter) { + Connection connection = Mockito.mock(Connection.class); + + TargetKeyResolver targetKeyResolver = new TargetKeyResolver(TargetKey.SNI_HOST, filter); + + Mockito.when(connection.getSNIHostName()).thenReturn(sniHost); + Assert.assertEquals(expected, targetKeyResolver.resolve(connection, null, null)); + + Assert.assertEquals(TargetKeyResolver.DEFAULT_KEY_VALUE, targetKeyResolver.resolve(null, null, null)); + + Mockito.when(connection.getSNIHostName()).thenReturn(null); + Assert.assertEquals(TargetKeyResolver.DEFAULT_KEY_VALUE, targetKeyResolver.resolve(null, null, null)); + } + + @Test + public void testSourceIPKey() { + testSourceIPKey("10.0.0.1", "10.0.0.1:12345", null); + } + + @Test + public void testSourceIPKeyWithFilter() { + testSourceIPKey("10", "10.0.0.1:12345", "^[^.]+"); + } + + private void testSourceIPKey(String expected, String remoteAddress, String filter) { + Connection connection = Mockito.mock(Connection.class); + + TargetKeyResolver targetKeyResolver = new TargetKeyResolver(TargetKey.SOURCE_IP, filter); + + Mockito.when(connection.getRemoteAddress()).thenReturn(remoteAddress); + Assert.assertEquals(expected, targetKeyResolver.resolve(connection, null, null)); + + Assert.assertEquals(TargetKeyResolver.DEFAULT_KEY_VALUE, targetKeyResolver.resolve(null, null, null)); + + Mockito.when(connection.getRemoteAddress()).thenReturn(null); + Assert.assertEquals(TargetKeyResolver.DEFAULT_KEY_VALUE, targetKeyResolver.resolve(null, null, null)); + } + + @Test + public void testUserNameKey() { + testUserNameKey("TEST", "TEST", null); + } + + @Test + public void testUserNameKeyWithFilter() { + testUserNameKey("TEST", "TEST1234", "^.{4}"); + } + + private void testUserNameKey(String expected, String username, String filter) { + TargetKeyResolver targetKeyResolver = new TargetKeyResolver(TargetKey.USER_NAME, filter); + + Assert.assertEquals(expected, targetKeyResolver.resolve(null, null, username)); + + Assert.assertEquals(TargetKeyResolver.DEFAULT_KEY_VALUE, targetKeyResolver.resolve(null, null, null)); + } +} diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/group/impl/ClusteredResetMockTest.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/group/impl/ClusteredResetMockTest.java index 9b6fce21e4..1234a08dd6 100644 --- a/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/group/impl/ClusteredResetMockTest.java +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/server/group/impl/ClusteredResetMockTest.java @@ -46,6 +46,7 @@ import org.apache.activemq.artemis.core.server.ActiveMQServer; import org.apache.activemq.artemis.core.server.Divert; import org.apache.activemq.artemis.core.server.Queue; import org.apache.activemq.artemis.core.server.QueueFactory; +import org.apache.activemq.artemis.core.server.balancing.BrokerBalancer; import org.apache.activemq.artemis.core.server.cluster.Bridge; import org.apache.activemq.artemis.core.server.cluster.BroadcastGroup; import org.apache.activemq.artemis.core.server.cluster.ClusterConnection; @@ -325,6 +326,16 @@ public class ClusteredResetMockTest extends ActiveMQTestBase { } + @Override + public void registerBrokerBalancer(BrokerBalancer balancer) throws Exception { + + } + + @Override + public void unregisterBrokerBalancer(String name) throws Exception { + + } + @Override public Object getResource(String resourceName) { return null; @@ -350,6 +361,16 @@ public class ClusteredResetMockTest extends ActiveMQTestBase { } + @Override + public Object getAttribute(String resourceName, String attribute) { + return null; + } + + @Override + public Object invokeOperation(String resourceName, String operation, Object[] params) throws Exception { + return null; + } + @Override public void start() throws Exception { diff --git a/artemis-server/src/test/resources/ConfigurationTest-full-config.xml b/artemis-server/src/test/resources/ConfigurationTest-full-config.xml index 75efb9dc29..c9e9232550 100644 --- a/artemis-server/src/test/resources/ConfigurationTest-full-config.xml +++ b/artemis-server/src/test/resources/ConfigurationTest-full-config.xml @@ -152,6 +152,38 @@ false + + + USER_NAME + + + + connector1 + + + + + SNI_HOST + ^[^.]+ + DEFAULT + + + 1000 + true + + + + + 60000 + + + 3000 + 2 + 1000 + + + + true diff --git a/artemis-server/src/test/resources/ConfigurationTest-xinclude-config.xml b/artemis-server/src/test/resources/ConfigurationTest-xinclude-config.xml index 540973eed5..0a55f8462b 100644 --- a/artemis-server/src/test/resources/ConfigurationTest-xinclude-config.xml +++ b/artemis-server/src/test/resources/ConfigurationTest-xinclude-config.xml @@ -143,6 +143,38 @@ false + + + USER_NAME + + + + connector1 + + + + + SNI_HOST + ^[^.]+ + DEFAULT + + + 1000 + true + + + + + 60000 + + + 3000 + 2 + 1000 + + + + true diff --git a/docs/user-manual/en/SUMMARY.md b/docs/user-manual/en/SUMMARY.md index b5f2dd3850..d269a734a5 100644 --- a/docs/user-manual/en/SUMMARY.md +++ b/docs/user-manual/en/SUMMARY.md @@ -64,6 +64,7 @@ * [Address Federation](federation-address.md) * [Queue Federation](federation-queue.md) * [High Availability and Failover](ha.md) +* [Broker Balancers](broker-balancers.md) * [Graceful Server Shutdown](graceful-shutdown.md) * [Libaio Native Libraries](libaio.md) * [Thread management](thread-pooling.md) diff --git a/docs/user-manual/en/broker-balancers.md b/docs/user-manual/en/broker-balancers.md new file mode 100644 index 0000000000..36a7907a06 --- /dev/null +++ b/docs/user-manual/en/broker-balancers.md @@ -0,0 +1,191 @@ +# Broker Balancers +Apache ActiveMQ Artemis broker balancers allow incoming client connections to be distributed across multiple [target brokers](target-brokers). +The target brokers are grouped in [pools](#pools) and the broker balancers use a [target key](#target-key) +to select a target broker from a pool of brokers according to a [policy](#policies). + +### This feature is still **EXPERIMENTAL** and not meant to be run in production yet. Furthermore, its configuration can change until declared as **officially stable**. + +## Target Broker +Target broker is a broker that can accept incoming client connections and is local or remote. +The local target is a special target that represents the same broker hosting the broker balancer. +The remote target is another reachable broker. + +## Target Key +The broker balancer uses a target key to select a target broker. +It is a string retrieved from an incoming client connection, the supported values are: +* `CLIENT_ID` is the JMS client ID; +* `SNI_HOST` is the hostname indicated by the client in the SNI extension of the TLS protocol; +* `SOURCE_IP` is the source IP address of the client; +* `USER_NAME` is the username indicated by the client. + +## Pools +The pool is a group of target brokers and checks periodically their state. +It provides a list of ready target brokers to distribute incoming client connections only when it is active. +A pool becomes active when the minimum number of ready target brokers defined by the `quorum-size` parameter is reached. +When it is not active, it doesn't provide any target avoiding weird distribution at startup or after a restart. +Including the local broker in the target pool allows broker hosting the balancer to accept incoming client connections as well. +By default, a pool doesn't include the local broker, to include it as a target the `local-target-enabled` parameter must be `true`. +There are two pool types: [discovery pool](#discovery-pool) and [static pool](#static-pool). + +### Cluster Pool +The cluster pool uses a [cluster connection](clusters.md#configuring-cluster-connections) to get the target brokers to add. +Let's take a look at a cluster pool example from broker.xml that uses a cluster connection: +```xml + + cluster1 + +``` + +### Discovery Pool +The discovery pool uses a [discovery group](clusters.md#discovery-groups) to discover the target brokers to add. +Let's take a look at a discovery pool example from broker.xml that uses a discovery group: +```xml + + + +``` + +### Static Pool +The static pool uses a list of static connectors to define the target brokers to add. +Let's take a look at a static pool example from broker.xml that uses a list of static connectors: +```xml + + + connector1 + connector2 + connector3 + + +``` + +### Defining pools +A pool is defined by the `pool` element that includes the following items: +* the `username` element defines the username to connect to the target broker; +* the `password` element defines the password to connect to the target broker; +* the `check-period` element defines how often to check the target broker, measured in milliseconds, default is `5000`; +* the `quorum-size` element defines the minimum number of ready targets to activate the pool, default is `1`; +* the `quorum-timeout` element defines the timeout to get the minimum number of ready targets, measured in milliseconds, default is `3000`; +* the `local-target-enabled` element defines whether the pool has to include a local target, default is `false`; +* the `cluster-connection` element defines the [cluster connection](clusters.md#configuring-cluster-connections) used by the [cluster pool](#cluster-pool). +* the `static-connectors` element defines a list of static connectors used by the [static pool](#static-pool); +* the `discovery-group` element defines the [discovery group](clusters.md#discovery-groups) used by the [discovery pool](#discovery-pool). + +Let's take a look at a pool example from broker.xml: +```xml + + 2 + 1000 + true + + connector1 + connector2 + connector3 + + +``` + +## Policies +The policy define how to select a broker from a pool. The included policies are: +* `FIRST_ELEMENT` to select the first target broker from the pool which is ready. It is useful to select the ready target brokers + according to the priority defined with their sequence order, ie supposing there are 2 target brokers + this policy selects the second target broker only when the first target broker isn't ready. +* `ROUND_ROBIN` to select a target sequentially from a pool, this policy is useful to evenly distribute; +* `CONSISTENT_HASH` to select a target by a key. This policy always selects the same target broker for the same key until it is removed from the pool. +* `LEAST_CONNECTIONS` to select the targets with the fewest active connections. This policy helps you maintain an equal distribution of active connections with the target brokers. + +A policy is defined by the `policy` element. Let's take a look at a policy example from broker.xml: +```xml + +``` + +## Cache +The broker balancer provides a cache with a timeout to improve the stickiness of the target broker selected, +returning the same target broker for a target key as long as it is present in the cache and is ready. +So a broker balancer with the cache enabled doesn't strictly follow the configured policy. +By default, the cache is enabled, to disable the cache the `cache-timeout` parameter must be `0`. + +## Defining broker balancers +A broker balancer is defined by `broker-balancer` element, it includes the following items: +* the `name` attribute defines the name of the broker balancer; +* the `target-key` element defines what key to select a target broker, the supported values are: `CLIENT_ID`, `SNI_HOST`, `SOURCE_IP`, `USER_NAME`, default is `SOURCE_IP`, see [target key](#target-key) for further details; +* the `target-key-filter` element defines a regular expression to filter the resolved keys; +* the `local-target-filter` element defines a regular expression to match the keys that have to return a local target; +* the `cache-timeout` element is the time period for a target broker to remain in the cache, measured in milliseconds, setting `0` will disable the cache, default is `-1`, meaning no expiration; +* the `pool` element defines the pool to group the target brokers, see [pools](#pools). +* the `policy` element defines the policy used to select the target brokers, see [policies](#policies); + +Let's take a look at some broker balancer examples from broker.xml: +```xml + + + + + + connector1 + connector2 + connector3 + + + + + USER_NAME + admin + + + true + + + + + + CLIENT_ID + ^.{3} + + + guest + guest + + + + +``` + +## Broker Balancer Workflow +The broker balancer workflow include the following steps: +* Retrieve the target key from the incoming connection; +* Return the local target broker if the target key matches the local filter; +* Return the cached target broker if it is ready; +* Get ready target brokers from the pool; +* Select one target broker using the policy; +* Add the selected broker in the cache; +* Return the selected broker. + +Let's take a look at flowchart of the broker balancer workflow: +![Broker Balancer Workflow](images/broker_balancer_workflow.png) + + +## Redirection +Apache ActiveMQ Artemis provides a native redirection for supported clients and a new management API for other clients. +The native redirection can be enabled per acceptor and is supported only for AMQP, CORE and OPENWIRE clients. +The acceptor with the `redirect-to` url parameter will redirect the incoming connections. +The `redirect-to` url parameter specifies the name of the broker balancer to use, +ie the following acceptor will redirect the incoming CORE client connections using the broker balancer with the name `simple-balancer`: + +```xml +tcp://0.0.0.0:61616?redirect-to=simple-balancer;protocols=CORE +``` +### Native Redirect Sequence + +The clients supporting the native redirection connect to the acceptor with the redirection enabled. +The acceptor sends to the client the target broker to redirect if it is ready and closes the connection. +The client connects to the target broker if it has received one before getting disconnected +otherwise it connected again to the acceptor with the redirection enabled. + +![Native Redirect Sequence](images/native_redirect_sequence.png) + +### Management API Redirect Sequence +The clients not supporting the native redirection queries the management API of broker balancer +to get the target broker to redirect. If the API returns a target broker the client connects to it +otherwise the client queries again the API. + +![Management API Redirect Sequence](images/management_api_redirect_sequence.png) diff --git a/docs/user-manual/en/images/broker_balancer_workflow.png b/docs/user-manual/en/images/broker_balancer_workflow.png new file mode 100644 index 0000000000000000000000000000000000000000..97560b6bd83f68d6e18d53d342d6cdd8db540427 GIT binary patch literal 96089 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

    aDdC?2CivvPKG5?l!ypfWK^6~eKXwPjinCwS# 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 literal 0 HcmV?d00001 diff --git a/docs/user-manual/en/images/native_redirect_sequence.png b/docs/user-manual/en/images/native_redirect_sequence.png new file mode 100644 index 0000000000000000000000000000000000000000..d7466da344e50f12f7d4d954f8f0e192044a7446 GIT binary patch literal 11169 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 literal 0 HcmV?d00001 diff --git a/examples/features/broker-balancer/evenly-redirect/pom.xml b/examples/features/broker-balancer/evenly-redirect/pom.xml new file mode 100644 index 0000000000..d92571bb64 --- /dev/null +++ b/examples/features/broker-balancer/evenly-redirect/pom.xml @@ -0,0 +1,207 @@ + + + + + 4.0.0 + + + org.apache.activemq.examples + broker-balancer + 2.18.0-SNAPSHOT + + + evenly-redirect + jar + evenly-redirect + + + ${project.basedir}/../../../.. + + + + + org.apache.activemq + artemis-jms-client + ${project.version} + + + + + + + org.apache.activemq + artemis-maven-plugin + + + create0 + + create + + + ${noServer} + ${basedir}/target/server0 + true + ${basedir}/target/classes/activemq/server0 + + -Djava.net.preferIPv4Stack=true + + + + create1 + + create + + + ${noServer} + ${basedir}/target/server1 + true + ${basedir}/target/classes/activemq/server1 + + -Djava.net.preferIPv4Stack=true + + + + create2 + + create + + + ${noServer} + ${basedir}/target/server2 + true + ${basedir}/target/classes/activemq/server2 + + -Djava.net.preferIPv4Stack=true + + + + start1 + + cli + + + ${noServer} + true + ${basedir}/target/server1 + tcp://localhost:61617 + + run + + server1 + + + + start2 + + cli + + + ${noServer} + true + ${basedir}/target/server2 + tcp://localhost:61618 + + run + + server1 + + + + start0 + + cli + + + true + ${noServer} + ${basedir}/target/server0 + tcp://localhost:61616 + + run + + server0 + + + + runClient + + runClient + + + + org.apache.activemq.artemis.jms.example.EvenlyRedirectExample + + + + stop0 + + cli + + + ${noServer} + ${basedir}/target/server0 + + stop + + + + + stop1 + + cli + + + ${noServer} + ${basedir}/target/server1 + + stop + + + + + stop2 + + cli + + + ${noServer} + ${basedir}/target/server2 + + stop + + + + + + + org.apache.activemq.examples + evenly-redirect + ${project.version} + + + + + org.apache.maven.plugins + maven-clean-plugin + + + + diff --git a/examples/features/broker-balancer/evenly-redirect/readme.md b/examples/features/broker-balancer/evenly-redirect/readme.md new file mode 100644 index 0000000000..05011d9b76 --- /dev/null +++ b/examples/features/broker-balancer/evenly-redirect/readme.md @@ -0,0 +1,8 @@ +# Evenly Redirect Example + +To run the example, simply type **mvn verify** from this directory, or **mvn -PnoServer verify** if you want to create and start the broker manually. + +This example demonstrates how incoming client connections are evenly redirected across two brokers +using a third broker with a balancer to redirect incoming client connections, +based on a least-connections policy and caching on a filtered prefix of the connection ClientID. + diff --git a/examples/features/broker-balancer/evenly-redirect/src/main/java/org/apache/activemq/artemis/jms/example/EvenlyRedirectExample.java b/examples/features/broker-balancer/evenly-redirect/src/main/java/org/apache/activemq/artemis/jms/example/EvenlyRedirectExample.java new file mode 100644 index 0000000000..40a870641e --- /dev/null +++ b/examples/features/broker-balancer/evenly-redirect/src/main/java/org/apache/activemq/artemis/jms/example/EvenlyRedirectExample.java @@ -0,0 +1,106 @@ +/* + * 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.activemq.artemis.jms.example; + +import javax.jms.Connection; +import javax.jms.ConnectionFactory; +import javax.jms.MessageConsumer; +import javax.jms.MessageProducer; +import javax.jms.Queue; +import javax.jms.Session; +import javax.jms.TextMessage; + +import org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory; + +/** + * This example demonstrates how incoming client connections are evenly redirected across two brokers + * using a third broker with a broker balancer to redirect incoming client connections. + */ +public class EvenlyRedirectExample { + + public static void main(final String[] args) throws Exception { + + /** + * Step 1. Create a connection for producer0 and producer1, and send a few messages. + * the server0 will redirect the connection of each producer to a different target broker. + */ + ConnectionFactory connectionFactoryProducer0 = new ActiveMQConnectionFactory("tcp://localhost:61616?ha=true&reconnectAttempts=30&clientID=FOO_PRODUCER"); + ConnectionFactory connectionFactoryProducer1 = new ActiveMQConnectionFactory("tcp://localhost:61616?ha=true&reconnectAttempts=30&clientID=BAR_PRODUCER"); + + Connection connectionProducer0 = null; + Connection connectionProducer1 = null; + + try { + connectionProducer0 = connectionFactoryProducer0.createConnection(); + connectionProducer1 = connectionFactoryProducer1.createConnection(); + + for (Connection connectionProducer : new Connection[] {connectionProducer0, connectionProducer1}) { + Session session = connectionProducer.createSession(false, Session.AUTO_ACKNOWLEDGE); + + Queue queue = session.createQueue("exampleQueue"); + MessageProducer sender = session.createProducer(queue); + for (int i = 0; i < 100; i++) { + sender.send(session.createTextMessage("Hello world n" + i + " - " + connectionProducer.getClientID())); + } + } + } finally { + if (connectionProducer0 != null) { + connectionProducer0.close(); + } + + if (connectionProducer1 != null) { + connectionProducer1.close(); + } + } + + /** + * Step 2. create a connection for consumer0 and consumer1, and receive a few messages. + * the server0 will redirect the connection to the same target broker of the respective producer + * because the consumer and the producer connections have the same clientID prefix, which + * the balancer configuration filters the target key on and caches the target broker the policy selects. + */ + ConnectionFactory connectionFactoryConsumer0 = new ActiveMQConnectionFactory("tcp://localhost:61616?ha=true&reconnectAttempts=30&clientID=BAR_CONSUMER"); + ConnectionFactory connectionFactoryConsumer1 = new ActiveMQConnectionFactory("tcp://localhost:61616?ha=true&reconnectAttempts=30&clientID=FOO_CONSUMER"); + + Connection connectionConsumer0 = null; + Connection connectionConsumer1 = null; + + try { + connectionConsumer0 = connectionFactoryConsumer0.createConnection(); + connectionConsumer1 = connectionFactoryConsumer1.createConnection(); + + for (Connection connectionConsumer : new Connection[] {connectionConsumer0, connectionConsumer1}) { + connectionConsumer.start(); + Session session = connectionConsumer.createSession(false, Session.AUTO_ACKNOWLEDGE); + Queue queue = session.createQueue("exampleQueue"); + MessageConsumer consumer = session.createConsumer(queue); + for (int i = 0; i < 100; i++) { + TextMessage message = (TextMessage) consumer.receive(5000); + System.out.println("Received message " + message.getText() + "/" + connectionConsumer.getClientID()); + } + } + } finally { + if (connectionConsumer0 != null) { + connectionConsumer0.close(); + } + + if (connectionConsumer1 != null) { + connectionConsumer1.close(); + } + } + } +} diff --git a/examples/features/broker-balancer/evenly-redirect/src/main/resources/activemq/server0/broker.xml b/examples/features/broker-balancer/evenly-redirect/src/main/resources/activemq/server0/broker.xml new file mode 100644 index 0000000000..a303f42979 --- /dev/null +++ b/examples/features/broker-balancer/evenly-redirect/src/main/resources/activemq/server0/broker.xml @@ -0,0 +1,135 @@ + + + + + + + + 0.0.0.0 + + + false + + NIO + + + true + + 120000 + + 60000 + + HALT + + 60000 + + + + tcp://0.0.0.0:61616?redirect-to=evenly-balancer;tcpSendBufferSize=1048576;tcpReceiveBufferSize=1048576;amqpMinLargeMessageSize=102400;protocols=CORE,AMQP,STOMP,HORNETQ,MQTT,OPENWIRE;useEpoll=true;amqpCredits=1000;amqpLowCredits=300;amqpDuplicateDetection=true + + + + tcp://localhost:61617 + tcp://localhost:61618 + + + + + CLIENT_ID + ^.{3} + DEFAULT + + + guest + guest + + server1 + server2 + + + + + + + + + + + + + + + + + + + + + + + + DLQ + ExpiryQueue + 0 + + -1 + 10 + PAGE + true + true + true + true + + + + DLQ + ExpiryQueue + 0 + + -1 + 10 + PAGE + true + true + true + true + + + + +

    + + + +
    +
    + + + +
    + + + + + diff --git a/examples/features/broker-balancer/evenly-redirect/src/main/resources/activemq/server1/broker.xml b/examples/features/broker-balancer/evenly-redirect/src/main/resources/activemq/server1/broker.xml new file mode 100644 index 0000000000..a9800a8fe5 --- /dev/null +++ b/examples/features/broker-balancer/evenly-redirect/src/main/resources/activemq/server1/broker.xml @@ -0,0 +1,113 @@ + + + + + + + + 0.0.0.0 + + + false + + NIO + + + true + + 120000 + + 60000 + + HALT + + 60000 + + + + tcp://0.0.0.0:61617?tcpSendBufferSize=1048576;tcpReceiveBufferSize=1048576;amqpMinLargeMessageSize=102400;protocols=CORE,AMQP,STOMP,HORNETQ,MQTT,OPENWIRE;useEpoll=true;amqpCredits=1000;amqpLowCredits=300;amqpDuplicateDetection=true + + + + + + + + + + + + + + + + + + + + + DLQ + ExpiryQueue + 0 + + -1 + 10 + PAGE + true + true + true + true + + + + DLQ + ExpiryQueue + 0 + + -1 + 10 + PAGE + true + true + true + true + + + + +
    + + + +
    +
    + + + +
    + +
    + +
    +
    diff --git a/examples/features/broker-balancer/evenly-redirect/src/main/resources/activemq/server2/broker.xml b/examples/features/broker-balancer/evenly-redirect/src/main/resources/activemq/server2/broker.xml new file mode 100644 index 0000000000..ed92197af3 --- /dev/null +++ b/examples/features/broker-balancer/evenly-redirect/src/main/resources/activemq/server2/broker.xml @@ -0,0 +1,113 @@ + + + + + + + + 0.0.0.0 + + + false + + NIO + + + true + + 120000 + + 60000 + + HALT + + 60000 + + + + tcp://0.0.0.0:61618?tcpSendBufferSize=1048576;tcpReceiveBufferSize=1048576;amqpMinLargeMessageSize=102400;protocols=CORE,AMQP,STOMP,HORNETQ,MQTT,OPENWIRE;useEpoll=true;amqpCredits=1000;amqpLowCredits=300;amqpDuplicateDetection=true + + + + + + + + + + + + + + + + + + + + + DLQ + ExpiryQueue + 0 + + -1 + 10 + PAGE + true + true + true + true + + + + DLQ + ExpiryQueue + 0 + + -1 + 10 + PAGE + true + true + true + true + + + + +
    + + + +
    +
    + + + +
    + +
    + +
    +
    diff --git a/examples/features/broker-balancer/pom.xml b/examples/features/broker-balancer/pom.xml new file mode 100644 index 0000000000..682307b1b3 --- /dev/null +++ b/examples/features/broker-balancer/pom.xml @@ -0,0 +1,56 @@ + + + + + 4.0.0 + + + org.apache.activemq.examples.clustered + broker-features + 2.18.0-SNAPSHOT + + + org.apache.activemq.examples + broker-balancer + pom + ActiveMQ Artemis Broker Balancer Examples + + + + ${project.basedir}/../../.. + + + + + examples + + evenly-redirect + symmetric-redirect + + + + release + + evenly-redirect + symmetric-redirect + + + + diff --git a/examples/features/broker-balancer/symmetric-redirect/pom.xml b/examples/features/broker-balancer/symmetric-redirect/pom.xml new file mode 100644 index 0000000000..04319d8cbc --- /dev/null +++ b/examples/features/broker-balancer/symmetric-redirect/pom.xml @@ -0,0 +1,164 @@ + + + + + 4.0.0 + + + org.apache.activemq.examples + broker-balancer + 2.18.0-SNAPSHOT + + + symmetric-redirect + jar + symmetric-redirect + + + ${project.basedir}/../../../.. + + + + + org.apache.activemq + artemis-jms-client + ${project.version} + + + + + + + org.apache.activemq + artemis-maven-plugin + + + create0 + + create + + + ${noServer} + ${basedir}/target/server0 + true + ${basedir}/target/classes/activemq/server0 + + -Djava.net.preferIPv4Stack=true + + + + create1 + + create + + + ${noServer} + ${basedir}/target/server1 + true + ${basedir}/target/classes/activemq/server1 + + -Djava.net.preferIPv4Stack=true + + + + start0 + + cli + + + true + ${noServer} + ${basedir}/target/server0 + tcp://localhost:61616 + + run + + server0 + + + + start1 + + cli + + + ${noServer} + true + ${basedir}/target/server1 + tcp://localhost:61617 + + run + + server1 + + + + runClient + + runClient + + + + org.apache.activemq.artemis.jms.example.SymmetricRedirectExample + + + + stop0 + + cli + + + ${noServer} + ${basedir}/target/server0 + + stop + + + + + stop1 + + cli + + + ${noServer} + ${basedir}/target/server1 + + stop + + + + + + + org.apache.activemq.examples + symmetric-redirect + ${project.version} + + + + + org.apache.maven.plugins + maven-clean-plugin + + + + diff --git a/examples/features/broker-balancer/symmetric-redirect/readme.md b/examples/features/broker-balancer/symmetric-redirect/readme.md new file mode 100644 index 0000000000..616dd8b3fa --- /dev/null +++ b/examples/features/broker-balancer/symmetric-redirect/readme.md @@ -0,0 +1,9 @@ +# Symmetric Redirect Example + +To run the example, simply type **mvn verify** from this directory, or **mvn -PnoServer verify** if you want to create and start the broker manually. + +This example demonstrates how incoming client connections are distributed across two brokers +using a symmetric architecture. In this architecture both brokers have two roles: balancer and target. +So they can redirect or accept the incoming client connection according to the consistent hash algorithm. +Both brokers use the same consistent hash algorithm to select the target broker so for the same key +if the first broker redirects an incoming client connection the second accepts it and vice versa. \ No newline at end of file diff --git a/examples/features/broker-balancer/symmetric-redirect/src/main/java/org/apache/activemq/artemis/jms/example/SymmetricRedirectExample.java b/examples/features/broker-balancer/symmetric-redirect/src/main/java/org/apache/activemq/artemis/jms/example/SymmetricRedirectExample.java new file mode 100644 index 0000000000..ca3abe59d2 --- /dev/null +++ b/examples/features/broker-balancer/symmetric-redirect/src/main/java/org/apache/activemq/artemis/jms/example/SymmetricRedirectExample.java @@ -0,0 +1,107 @@ +/* + * 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.activemq.artemis.jms.example; + +import javax.jms.Connection; +import javax.jms.ConnectionFactory; +import javax.jms.MessageConsumer; +import javax.jms.MessageProducer; +import javax.jms.Queue; +import javax.jms.Session; +import javax.jms.TextMessage; + +import org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory; + +/** + * This example demonstrates how incoming client connections are distributed across two brokers + * using a symmetric architecture. + */ +public class SymmetricRedirectExample { + + public static void main(final String[] args) throws Exception { + + /** + * Step 1. Create a connection for producer0 and producer1, and send a few messages. + * the server0 will redirect the connection of each producer to a different target brokers. + */ + ConnectionFactory connectionFactory0Server0 = new ActiveMQConnectionFactory("tcp://localhost:61616?ha=true&reconnectAttempts=30&clientID=FOO_PRODUCER"); + ConnectionFactory connectionFactory1Server0 = new ActiveMQConnectionFactory("tcp://localhost:61616?ha=true&reconnectAttempts=30&clientID=BAR_PRODUCER"); + + Connection connectionProducer0 = null; + Connection connectionProducer1 = null; + + try { + connectionProducer0 = connectionFactory0Server0.createConnection(); + connectionProducer1 = connectionFactory1Server0.createConnection(); + + for (Connection connectionProducer : new Connection[] {connectionProducer0, connectionProducer1}) { + Session session = connectionProducer.createSession(false, Session.AUTO_ACKNOWLEDGE); + + Queue queue = session.createQueue("exampleQueue" + connectionProducer.getClientID().substring(0, 3)); + MessageProducer sender = session.createProducer(queue); + for (int i = 0; i < 100; i++) { + TextMessage message = session.createTextMessage("Hello world n" + i + " - " + connectionProducer.getClientID().substring(0, 3)); + System.out.println("Sending message " + message.getText() + "/" + connectionProducer.getClientID()); + sender.send(message); + } + } + } finally { + if (connectionProducer0 != null) { + connectionProducer0.close(); + } + + if (connectionProducer1 != null) { + connectionProducer1.close(); + } + } + + /** + * Step 2. create a connection for consumer0 and consumer1, and receive a few messages. + * the server1 will redirect the connection to the same target broker of the respective producer + * from earlier as the new consumer connection uses the same ClientID prefix. + */ + ConnectionFactory connectionFactory0Server1 = new ActiveMQConnectionFactory("tcp://localhost:61617?ha=true&reconnectAttempts=30&clientID=BAR_CONSUMER"); + ConnectionFactory connectionFactory1Server1 = new ActiveMQConnectionFactory("tcp://localhost:61617?ha=true&reconnectAttempts=30&clientID=FOO_CONSUMER"); + + Connection connectionConsumer0 = null; + Connection connectionConsumer1 = null; + + try { + connectionConsumer0 = connectionFactory0Server1.createConnection(); + connectionConsumer1 = connectionFactory1Server1.createConnection(); + + for (Connection connectionConsumer : new Connection[] {connectionConsumer0, connectionConsumer1}) { + connectionConsumer.start(); + Session session = connectionConsumer.createSession(false, Session.AUTO_ACKNOWLEDGE); + Queue queue = session.createQueue("exampleQueue" + connectionConsumer.getClientID().substring(0, 3)); + MessageConsumer consumer = session.createConsumer(queue); + for (int i = 0; i < 100; i++) { + TextMessage message = (TextMessage) consumer.receive(5000); + System.out.println("Received message " + message.getText() + "/" + connectionConsumer.getClientID()); + } + } + } finally { + if (connectionConsumer0 != null) { + connectionConsumer0.close(); + } + + if (connectionConsumer1 != null) { + connectionConsumer1.close(); + } + } + } +} diff --git a/examples/features/broker-balancer/symmetric-redirect/src/main/resources/activemq/server0/broker.xml b/examples/features/broker-balancer/symmetric-redirect/src/main/resources/activemq/server0/broker.xml new file mode 100644 index 0000000000..9316bfbe1b --- /dev/null +++ b/examples/features/broker-balancer/symmetric-redirect/src/main/resources/activemq/server0/broker.xml @@ -0,0 +1,150 @@ + + + + + + + + 0.0.0.0 + + + false + + NIO + + + true + + 120000 + + 60000 + + HALT + + 60000 + + + + tcp://0.0.0.0:61616?redirect-to=symmetric-balancer;tcpSendBufferSize=1048576;tcpReceiveBufferSize=1048576;amqpMinLargeMessageSize=102400;protocols=CORE,AMQP,STOMP,HORNETQ,MQTT,OPENWIRE;useEpoll=true;amqpCredits=1000;amqpLowCredits=300;amqpDuplicateDetection=true + + + + tcp://localhost:61616 + + + + + ${udp-address:231.7.7.7} + 9876 + 100 + netty-connector + + + + + + ${udp-address:231.7.7.7} + 9876 + 10000 + + + + + + CLIENT_ID + ^.{3} + DEFAULT + + + guest + guest + 2 + true + + + + + + + + + + + + + + + + + + + + + + + + DLQ + ExpiryQueue + 0 + + -1 + 10 + PAGE + true + true + true + true + + + + DLQ + ExpiryQueue + 0 + + -1 + 10 + PAGE + true + true + true + true + + + + +
    + + + +
    +
    + + + +
    + +
    + +
    +
    diff --git a/examples/features/broker-balancer/symmetric-redirect/src/main/resources/activemq/server1/broker.xml b/examples/features/broker-balancer/symmetric-redirect/src/main/resources/activemq/server1/broker.xml new file mode 100644 index 0000000000..b3fa125b8c --- /dev/null +++ b/examples/features/broker-balancer/symmetric-redirect/src/main/resources/activemq/server1/broker.xml @@ -0,0 +1,150 @@ + + + + + + + + 0.0.0.0 + + + false + + NIO + + + true + + 120000 + + 60000 + + HALT + + 60000 + + + + tcp://0.0.0.0:61617?redirect-to=symmetric-balancer;tcpSendBufferSize=1048576;tcpReceiveBufferSize=1048576;amqpMinLargeMessageSize=102400;protocols=CORE,AMQP,STOMP,HORNETQ,MQTT,OPENWIRE;useEpoll=true;amqpCredits=1000;amqpLowCredits=300;amqpDuplicateDetection=true + + + + tcp://localhost:61617 + + + + + ${udp-address:231.7.7.7} + 9876 + 100 + netty-connector + + + + + + ${udp-address:231.7.7.7} + 9876 + 10000 + + + + + + CLIENT_ID + ^.{3} + DEFAULT + + + guest + guest + 2 + true + + + + + + + + + + + + + + + + + + + + + + + + DLQ + ExpiryQueue + 0 + + -1 + 10 + PAGE + true + true + true + true + + + + DLQ + ExpiryQueue + 0 + + -1 + 10 + PAGE + true + true + true + true + + + + +
    + + + +
    +
    + + + +
    + +
    + +
    +
    diff --git a/pom.xml b/pom.xml index 5e59e4999c..c6d5384f83 100644 --- a/pom.xml +++ b/pom.xml @@ -163,7 +163,7 @@ 1 0 0 - 130,129,128,127,126,125,124,123,122 + 131,130,129,128,127,126,125,124,123,122 ${project.version} ${project.version}(${activemq.version.incrementingVersion}) diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/balancing/BalancingTestBase.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/balancing/BalancingTestBase.java new file mode 100644 index 0000000000..db7a849277 --- /dev/null +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/balancing/BalancingTestBase.java @@ -0,0 +1,246 @@ +/** + * 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.activemq.artemis.tests.integration.balancing; + +import javax.jms.ConnectionFactory; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.apache.activemq.artemis.api.core.TransportConfiguration; +import org.apache.activemq.artemis.core.config.Configuration; +import org.apache.activemq.artemis.core.config.balancing.BrokerBalancerConfiguration; +import org.apache.activemq.artemis.core.config.balancing.PolicyConfiguration; +import org.apache.activemq.artemis.core.config.balancing.PoolConfiguration; +import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants; +import org.apache.activemq.artemis.core.server.balancing.targets.TargetKey; +import org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory; +import org.apache.activemq.artemis.tests.integration.cluster.distribution.ClusterTestBase; +import org.apache.activemq.artemis.tests.util.ActiveMQTestBase; +import org.apache.qpid.jms.JmsConnectionFactory; + +public class BalancingTestBase extends ClusterTestBase { + protected static final String AMQP_PROTOCOL = "AMQP"; + protected static final String CORE_PROTOCOL = "CORE"; + protected static final String OPENWIRE_PROTOCOL = "OPENWIRE"; + + protected static final String CLUSTER_POOL = "CLUSTER"; + protected static final String DISCOVERY_POOL = "DISCOVERY"; + protected static final String STATIC_POOL = "STATIC"; + + protected static final String BROKER_BALANCER_NAME = "bb1"; + + protected static final String DEFAULT_CONNECTOR_NAME = "DEFAULT"; + + protected static final String GROUP_ADDRESS = ActiveMQTestBase.getUDPDiscoveryAddress(); + + protected static final int GROUP_PORT = ActiveMQTestBase.getUDPDiscoveryPort(); + + protected static final int MULTIPLE_TARGETS = 3; + + + protected TransportConfiguration getDefaultServerAcceptor(final int node) { + return getServer(node).getConfiguration().getAcceptorConfigurations().stream().findFirst().get(); + } + + protected TransportConfiguration getDefaultServerConnector(final int node) { + Map connectorConfigurations = getServer(node).getConfiguration().getConnectorConfigurations(); + TransportConfiguration connector = connectorConfigurations.get(DEFAULT_CONNECTOR_NAME); + return connector != null ? connector : connectorConfigurations.values().stream().findFirst().get(); + } + + protected TransportConfiguration setupDefaultServerConnector(final int node) { + TransportConfiguration defaultServerConnector = getDefaultServerConnector(node); + + if (!defaultServerConnector.getName().equals(DEFAULT_CONNECTOR_NAME)) { + defaultServerConnector = new TransportConfiguration(defaultServerConnector.getFactoryClassName(), + defaultServerConnector.getParams(), DEFAULT_CONNECTOR_NAME, defaultServerConnector.getExtraParams()); + + getServer(node).getConfiguration().getConnectorConfigurations().put(DEFAULT_CONNECTOR_NAME, defaultServerConnector); + } + + return defaultServerConnector; + } + + protected void setupBalancerServerWithCluster(final int node, final TargetKey targetKey, final String policyName, final Map properties, final boolean localTargetEnabled, final String localTargetFilter, final int quorumSize, String clusterConnection) { + Configuration configuration = getServer(node).getConfiguration(); + BrokerBalancerConfiguration brokerBalancerConfiguration = new BrokerBalancerConfiguration().setName(BROKER_BALANCER_NAME); + + setupDefaultServerConnector(node); + + brokerBalancerConfiguration.setTargetKey(targetKey).setLocalTargetFilter(localTargetFilter) + .setPoolConfiguration(new PoolConfiguration().setCheckPeriod(1000).setQuorumSize(quorumSize) + .setLocalTargetEnabled(localTargetEnabled).setClusterConnection(clusterConnection)) + .setPolicyConfiguration(new PolicyConfiguration().setName(policyName).setProperties(properties)); + + configuration.setBalancerConfigurations(Collections.singletonList(brokerBalancerConfiguration)); + + TransportConfiguration acceptor = getDefaultServerAcceptor(node); + acceptor.getParams().put("redirect-to", BROKER_BALANCER_NAME); + } + + protected void setupBalancerServerWithDiscovery(final int node, final TargetKey targetKey, final String policyName, final Map properties, final boolean localTargetEnabled, final String localTargetFilter, final int quorumSize) { + Configuration configuration = getServer(node).getConfiguration(); + BrokerBalancerConfiguration brokerBalancerConfiguration = new BrokerBalancerConfiguration().setName(BROKER_BALANCER_NAME); + + setupDefaultServerConnector(node); + + brokerBalancerConfiguration.setTargetKey(targetKey).setLocalTargetFilter(localTargetFilter) + .setPoolConfiguration(new PoolConfiguration().setCheckPeriod(1000).setQuorumSize(quorumSize) + .setLocalTargetEnabled(localTargetEnabled).setDiscoveryGroupName("dg1")) + .setPolicyConfiguration(new PolicyConfiguration().setName(policyName).setProperties(properties)); + + configuration.setBalancerConfigurations(Collections.singletonList(brokerBalancerConfiguration)); + + TransportConfiguration acceptor = getDefaultServerAcceptor(node); + acceptor.getParams().put("redirect-to", BROKER_BALANCER_NAME); + } + + protected void setupBalancerServerWithStaticConnectors(final int node, final TargetKey targetKey, final String policyName, final Map properties, final boolean localTargetEnabled, final String localTargetFilter, final int quorumSize, final int... targetNodes) { + Configuration configuration = getServer(node).getConfiguration(); + BrokerBalancerConfiguration brokerBalancerConfiguration = new BrokerBalancerConfiguration().setName(BROKER_BALANCER_NAME); + + setupDefaultServerConnector(node); + + List staticConnectors = new ArrayList<>(); + for (int targetNode : targetNodes) { + TransportConfiguration connector = getDefaultServerConnector(targetNode); + configuration.getConnectorConfigurations().put(connector.getName(), connector); + staticConnectors.add(connector.getName()); + } + + brokerBalancerConfiguration.setTargetKey(targetKey).setLocalTargetFilter(localTargetFilter) + .setPoolConfiguration(new PoolConfiguration().setCheckPeriod(1000).setQuorumSize(quorumSize) + .setLocalTargetEnabled(localTargetEnabled).setStaticConnectors(staticConnectors)) + .setPolicyConfiguration(new PolicyConfiguration().setName(policyName).setProperties(properties)); + + configuration.setBalancerConfigurations(Collections.singletonList(brokerBalancerConfiguration)); + + TransportConfiguration acceptor = getDefaultServerAcceptor(node); + acceptor.getParams().put("redirect-to", BROKER_BALANCER_NAME); + } + + protected ConnectionFactory createFactory(String protocol, boolean sslEnabled, String host, int port, String clientID, String user, String password) throws Exception { + switch (protocol) { + case CORE_PROTOCOL: { + StringBuilder urlBuilder = new StringBuilder(); + + urlBuilder.append("tcp://"); + urlBuilder.append(host); + urlBuilder.append(":"); + urlBuilder.append(port); + urlBuilder.append("?ha=true&reconnectAttempts=30"); + + urlBuilder.append("&sniHost="); + urlBuilder.append(host); + + if (clientID != null) { + urlBuilder.append("&clientID="); + urlBuilder.append(clientID); + } + + if (sslEnabled) { + urlBuilder.append("&"); + urlBuilder.append(TransportConstants.SSL_ENABLED_PROP_NAME); + urlBuilder.append("="); + urlBuilder.append(true); + + urlBuilder.append("&"); + urlBuilder.append(TransportConstants.TRUSTSTORE_PATH_PROP_NAME); + urlBuilder.append("="); + urlBuilder.append("server-ca-truststore.jks"); + + urlBuilder.append("&"); + urlBuilder.append(TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME); + urlBuilder.append("="); + urlBuilder.append("securepass"); + } + + return new ActiveMQConnectionFactory(urlBuilder.toString(), user, password); + } + case AMQP_PROTOCOL: { + StringBuilder urlBuilder = new StringBuilder(); + + urlBuilder.append("failover:("); + + if (sslEnabled) { + urlBuilder.append("amqps://"); + urlBuilder.append(host); + urlBuilder.append(":"); + urlBuilder.append(port); + + urlBuilder.append("?transport.trustStoreLocation="); + urlBuilder.append(getClass().getClassLoader().getResource("server-ca-truststore.jks").getFile()); + urlBuilder.append("&transport.trustStorePassword=securepass)"); + } else { + urlBuilder.append("amqp://"); + urlBuilder.append(host); + urlBuilder.append(":"); + urlBuilder.append(port); + urlBuilder.append(")"); + } + + if (clientID != null) { + urlBuilder.append("?jms.clientID="); + urlBuilder.append(clientID); + } + + return new JmsConnectionFactory(user, password, urlBuilder.toString()); + } + case OPENWIRE_PROTOCOL: { + StringBuilder urlBuilder = new StringBuilder(); + + urlBuilder.append("failover:("); + + if (sslEnabled) { + urlBuilder.append("ssl://"); + urlBuilder.append(host); + urlBuilder.append(":"); + urlBuilder.append(port); + urlBuilder.append(")"); + } else { + urlBuilder.append("tcp://"); + urlBuilder.append(host); + urlBuilder.append(":"); + urlBuilder.append(port); + urlBuilder.append(")"); + } + + if (clientID != null) { + urlBuilder.append("?jms.clientID="); + urlBuilder.append(clientID); + } + + if (sslEnabled) { + org.apache.activemq.ActiveMQSslConnectionFactory sslConnectionFactory = new org.apache.activemq.ActiveMQSslConnectionFactory(urlBuilder.toString()); + sslConnectionFactory.setUserName(user); + sslConnectionFactory.setPassword(password); + sslConnectionFactory.setTrustStore("server-ca-truststore.jks"); + sslConnectionFactory.setTrustStorePassword("securepass"); + return sslConnectionFactory; + } else { + return new org.apache.activemq.ActiveMQConnectionFactory(user, password, urlBuilder.toString()); + } + } + default: + throw new IllegalStateException("Unexpected value: " + protocol); + } + } +} diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/balancing/MQTTRedirectTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/balancing/MQTTRedirectTest.java new file mode 100644 index 0000000000..acef93ce74 --- /dev/null +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/balancing/MQTTRedirectTest.java @@ -0,0 +1,125 @@ +/** + * 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.activemq.artemis.tests.integration.balancing; + +import io.netty.handler.codec.mqtt.MqttConnectReturnCode; +import org.apache.activemq.artemis.api.core.QueueConfiguration; +import org.apache.activemq.artemis.api.core.RoutingType; +import org.apache.activemq.artemis.api.core.management.BrokerBalancerControl; +import org.apache.activemq.artemis.api.core.management.QueueControl; +import org.apache.activemq.artemis.api.core.management.ResourceNames; +import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants; +import org.apache.activemq.artemis.core.server.balancing.policies.FirstElementPolicy; +import org.apache.activemq.artemis.core.server.balancing.targets.TargetKey; +import org.apache.activemq.artemis.utils.Wait; +import org.eclipse.paho.client.mqttv3.MqttClient; +import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; +import org.junit.Assert; +import org.junit.Test; + +import javax.management.openmbean.CompositeData; +import javax.management.openmbean.TabularData; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class MQTTRedirectTest extends BalancingTestBase { + + private final boolean discovery = true; + + @Test + public void testSimpleRedirect() throws Exception { + final String topicName = "RedirectTestTopic"; + + setupLiveServerWithDiscovery(0, GROUP_ADDRESS, GROUP_PORT, true, true, false); + setupLiveServerWithDiscovery(1, GROUP_ADDRESS, GROUP_PORT, true, true, false); + if (discovery) { + setupBalancerServerWithDiscovery(0, TargetKey.USER_NAME, FirstElementPolicy.NAME, null, false, null, 1); + } else { + setupBalancerServerWithStaticConnectors(0, TargetKey.USER_NAME, FirstElementPolicy.NAME, null, false, null, 1, 1); + } + + startServers(0, 1); + + getServer(0).createQueue(new QueueConfiguration(topicName).setRoutingType(RoutingType.ANYCAST)); + getServer(1).createQueue(new QueueConfiguration(topicName).setRoutingType(RoutingType.ANYCAST)); + + QueueControl queueControl0 = (QueueControl)getServer(0).getManagementService() + .getResource(ResourceNames.QUEUE + topicName); + QueueControl queueControl1 = (QueueControl)getServer(1).getManagementService() + .getResource(ResourceNames.QUEUE + topicName); + + Assert.assertEquals(0, queueControl0.countMessages()); + Assert.assertEquals(0, queueControl1.countMessages()); + + MqttConnectOptions connOpts = new MqttConnectOptions(); + connOpts.setCleanSession(true); + connOpts.setUserName("admin"); + connOpts.setPassword("admin".toCharArray()); + + MqttClient client0 = new MqttClient("tcp://" + TransportConstants.DEFAULT_HOST + ":" + TransportConstants.DEFAULT_PORT, "TEST", new MemoryPersistence()); + try { + client0.connect(connOpts); + Assert.fail(); + } catch (MqttException e) { + Assert.assertEquals(MqttConnectReturnCode.CONNECTION_REFUSED_USE_ANOTHER_SERVER, MqttConnectReturnCode.valueOf((byte) e.getReasonCode())); + } + client0.close(); + + BrokerBalancerControl brokerBalancerControl = (BrokerBalancerControl)getServer(0).getManagementService() + .getResource(ResourceNames.BROKER_BALANCER + BROKER_BALANCER_NAME); + + CompositeData targetData = brokerBalancerControl.getTarget("admin"); + CompositeData targetConnectorData = (CompositeData)targetData.get("connector"); + TabularData targetConnectorParams = (TabularData)targetConnectorData.get("params"); + CompositeData hostData = targetConnectorParams.get(new Object[]{TransportConstants.HOST_PROP_NAME}); + CompositeData portData = targetConnectorParams.get(new Object[]{TransportConstants.PORT_PROP_NAME}); + String host = hostData != null ? (String)hostData.get("value") : TransportConstants.DEFAULT_HOST; + int port = portData != null ? Integer.valueOf((String)portData.get("value")) : TransportConstants.DEFAULT_PORT; + + CountDownLatch latch = new CountDownLatch(1); + List messages = new ArrayList<>(); + + MqttClient client1 = new MqttClient("tcp://" + host + ":" + port, "TEST", new MemoryPersistence()); + client1.connect(connOpts); + + Assert.assertEquals(0, queueControl0.countMessages()); + Assert.assertEquals(0, queueControl1.countMessages()); + + client1.subscribe(topicName, (s, mqttMessage) -> { + messages.add(mqttMessage); + latch.countDown(); + }); + + client1.publish(topicName, new MqttMessage("TEST".getBytes())); + + Assert.assertTrue(latch.await(3000, TimeUnit.MILLISECONDS)); + Assert.assertEquals("TEST", new String(messages.get(0).getPayload())); + + client1.disconnect(); + client1.close(); + + Assert.assertEquals(0, queueControl0.countMessages()); + Wait.assertEquals(0, () -> queueControl1.countMessages()); + } +} + diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/balancing/RedirectTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/balancing/RedirectTest.java new file mode 100644 index 0000000000..185c552e1c --- /dev/null +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/balancing/RedirectTest.java @@ -0,0 +1,399 @@ +/** + * 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.activemq.artemis.tests.integration.balancing; + +import javax.jms.Connection; +import javax.jms.ConnectionFactory; +import javax.jms.MessageConsumer; +import javax.jms.MessageProducer; +import javax.jms.Session; +import javax.jms.TextMessage; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +import org.apache.activemq.artemis.api.core.QueueConfiguration; +import org.apache.activemq.artemis.api.core.RoutingType; +import org.apache.activemq.artemis.api.core.management.QueueControl; +import org.apache.activemq.artemis.api.core.management.ResourceNames; +import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants; +import org.apache.activemq.artemis.core.server.balancing.policies.ConsistentHashPolicy; +import org.apache.activemq.artemis.core.server.balancing.policies.FirstElementPolicy; +import org.apache.activemq.artemis.core.server.balancing.policies.LeastConnectionsPolicy; +import org.apache.activemq.artemis.core.server.balancing.policies.RoundRobinPolicy; +import org.apache.activemq.artemis.core.server.balancing.targets.TargetKey; +import org.apache.activemq.artemis.core.server.cluster.impl.MessageLoadBalancingType; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class RedirectTest extends BalancingTestBase { + + @Parameterized.Parameters(name = "protocol: {0}, pool: {1}") + public static Collection data() { + final String[] protocols = new String[] {AMQP_PROTOCOL, CORE_PROTOCOL, OPENWIRE_PROTOCOL}; + final String[] pools = new String[] {CLUSTER_POOL, DISCOVERY_POOL, STATIC_POOL}; + Collection data = new ArrayList<>(); + + for (String protocol : Arrays.asList(protocols)) { + for (String pool : Arrays.asList(pools)) { + data.add(new Object[] {protocol, pool}); + } + } + + return data; + } + + + private final String protocol; + + private final String pool; + + + public RedirectTest(String protocol, String pool) { + this.protocol = protocol; + + this.pool = pool; + } + + @Test + public void testSimpleRedirect() throws Exception { + final String queueName = "RedirectTestQueue"; + + setupLiveServerWithDiscovery(0, GROUP_ADDRESS, GROUP_PORT, true, true, false); + setupLiveServerWithDiscovery(1, GROUP_ADDRESS, GROUP_PORT, true, true, false); + if (CLUSTER_POOL.equals(pool)) { + setupDiscoveryClusterConnection("cluster0", 0, "dg1", "queues", MessageLoadBalancingType.OFF, 1, true); + setupDiscoveryClusterConnection("cluster1", 1, "dg1", "queues", MessageLoadBalancingType.OFF, 1, true); + setupBalancerServerWithCluster(0, TargetKey.USER_NAME, FirstElementPolicy.NAME, null, false, "ACTIVEMQ.CLUSTER.ADMIN.USER", 1, "cluster0"); + } else if (DISCOVERY_POOL.equals(pool)) { + setupBalancerServerWithDiscovery(0, TargetKey.USER_NAME, FirstElementPolicy.NAME, null, false, null, 1); + } else { + setupBalancerServerWithStaticConnectors(0, TargetKey.USER_NAME, FirstElementPolicy.NAME, null, false, null, 1, 1); + } + + startServers(0, 1); + + getServer(0).createQueue(new QueueConfiguration(queueName).setRoutingType(RoutingType.ANYCAST)); + getServer(1).createQueue(new QueueConfiguration(queueName).setRoutingType(RoutingType.ANYCAST)); + + QueueControl queueControl0 = (QueueControl)getServer(0).getManagementService() + .getResource(ResourceNames.QUEUE + queueName); + QueueControl queueControl1 = (QueueControl)getServer(1).getManagementService() + .getResource(ResourceNames.QUEUE + queueName); + + Assert.assertEquals(0, queueControl0.countMessages()); + Assert.assertEquals(0, queueControl1.countMessages()); + + ConnectionFactory connectionFactory = createFactory(protocol, false, TransportConstants.DEFAULT_HOST, + TransportConstants.DEFAULT_PORT + 0, null, "admin", "admin"); + + + try (Connection connection = connectionFactory.createConnection()) { + connection.start(); + try (Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE)) { + javax.jms.Queue queue = session.createQueue(queueName); + try (MessageProducer producer = session.createProducer(queue)) { + producer.send(session.createTextMessage("TEST")); + } + } + } + + Assert.assertEquals(0, queueControl0.countMessages()); + Assert.assertEquals(1, queueControl1.countMessages()); + + try (Connection connection = connectionFactory.createConnection()) { + connection.start(); + try (Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE)) { + try (MessageConsumer consumer = session.createConsumer(session.createQueue(queueName))) { + TextMessage message = (TextMessage) consumer.receive(1000); + Assert.assertNotNull(message); + Assert.assertEquals("TEST", message.getText()); + } + } + } + + Assert.assertEquals(0, queueControl0.countMessages()); + Assert.assertEquals(0, queueControl1.countMessages()); + + stopServers(0, 1); + } + + @Test + public void testRoundRobinRedirect() throws Exception { + testEvenlyRedirect(RoundRobinPolicy.NAME, null); + } + + @Test + public void testLeastConnectionsRedirect() throws Exception { + testEvenlyRedirect(LeastConnectionsPolicy.NAME, Collections.singletonMap(LeastConnectionsPolicy.CONNECTION_COUNT_THRESHOLD, String.valueOf(30))); + } + + private void testEvenlyRedirect(final String policyName, final Map properties) throws Exception { + final String queueName = "RedirectTestQueue"; + final int targets = MULTIPLE_TARGETS; + int[] nodes = new int[targets + 1]; + int[] targetNodes = new int[targets]; + QueueControl[] queueControls = new QueueControl[targets + 1]; + + nodes[0] = 0; + setupLiveServerWithDiscovery(0, GROUP_ADDRESS, GROUP_PORT, true, true, false); + for (int i = 0; i < targets; i++) { + nodes[i + 1] = i + 1; + targetNodes[i] = i + 1; + setupLiveServerWithDiscovery(i + 1, GROUP_ADDRESS, GROUP_PORT, true, true, false); + } + + if (CLUSTER_POOL.equals(pool)) { + for (int node : nodes) { + setupDiscoveryClusterConnection("cluster" + node, node, "dg1", "queues", MessageLoadBalancingType.OFF, 1, true); + } + setupBalancerServerWithCluster(0, TargetKey.USER_NAME, policyName, properties, false, "ACTIVEMQ.CLUSTER.ADMIN.USER", targets, "cluster0"); + } else if (DISCOVERY_POOL.equals(pool)) { + setupBalancerServerWithDiscovery(0, TargetKey.USER_NAME, policyName, properties, false, null, targets); + } else { + setupBalancerServerWithStaticConnectors(0, TargetKey.USER_NAME, policyName, properties, false, null, targets, 1, 2, 3); + } + + startServers(nodes); + + for (int node : nodes) { + getServer(node).createQueue(new QueueConfiguration(queueName).setRoutingType(RoutingType.ANYCAST)); + + queueControls[node] = (QueueControl)getServer(node).getManagementService() + .getResource(ResourceNames.QUEUE + queueName); + + Assert.assertEquals(0, queueControls[node].countMessages()); + } + + + ConnectionFactory[] connectionFactories = new ConnectionFactory[targets]; + Connection[] connections = new Connection[targets]; + Session[] sessions = new Session[targets]; + + for (int i = 0; i < targets; i++) { + connectionFactories[i] = createFactory(protocol, false, TransportConstants.DEFAULT_HOST, + TransportConstants.DEFAULT_PORT + 0, null, "user" + i, "user" + i); + + connections[i] = connectionFactories[i].createConnection(); + connections[i].start(); + + sessions[i] = connections[i].createSession(false, Session.AUTO_ACKNOWLEDGE); + } + + for (int i = 0; i < targets; i++) { + try (MessageProducer producer = sessions[i].createProducer(sessions[i].createQueue(queueName))) { + producer.send(sessions[i].createTextMessage("TEST" + i)); + } + + sessions[i].close(); + connections[i].close(); + } + + Assert.assertEquals(0, queueControls[0].countMessages()); + for (int targetNode : targetNodes) { + Assert.assertEquals("Messages of node " + targetNode, 1, queueControls[targetNode].countMessages()); + } + + for (int i = 0; i < targets; i++) { + try (Connection connection = connectionFactories[i].createConnection()) { + connection.start(); + try (Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE)) { + try (MessageConsumer consumer = session.createConsumer(session.createQueue(queueName))) { + TextMessage message = (TextMessage) consumer.receive(1000); + Assert.assertNotNull(message); + Assert.assertEquals("TEST" + i, message.getText()); + } + } + } + } + + for (int node : nodes) { + Assert.assertEquals(0, queueControls[node].countMessages()); + } + + stopServers(nodes); + } + + @Test + public void testSymmetricRedirect() throws Exception { + final String queueName = "RedirectTestQueue"; + + setupLiveServerWithDiscovery(0, GROUP_ADDRESS, GROUP_PORT, true, true, false); + setupLiveServerWithDiscovery(1, GROUP_ADDRESS, GROUP_PORT, true, true, false); + if (CLUSTER_POOL.equals(pool)) { + setupDiscoveryClusterConnection("cluster0", 0, "dg1", "queues", MessageLoadBalancingType.OFF, 1, true); + setupDiscoveryClusterConnection("cluster1", 1, "dg1", "queues", MessageLoadBalancingType.OFF, 1, true); + setupBalancerServerWithCluster(0, TargetKey.USER_NAME, ConsistentHashPolicy.NAME, null, true, "ACTIVEMQ.CLUSTER.ADMIN.USER", 2, "cluster0"); + setupBalancerServerWithCluster(1, TargetKey.USER_NAME, ConsistentHashPolicy.NAME, null, true, "ACTIVEMQ.CLUSTER.ADMIN.USER", 2, "cluster1"); + } else if (DISCOVERY_POOL.equals(pool)) { + setupBalancerServerWithDiscovery(0, TargetKey.USER_NAME, ConsistentHashPolicy.NAME, null, true, null, 2); + setupBalancerServerWithDiscovery(1, TargetKey.USER_NAME, ConsistentHashPolicy.NAME, null, true, null, 2); + } else { + setupBalancerServerWithStaticConnectors(0, TargetKey.USER_NAME, ConsistentHashPolicy.NAME, null, true, null, 2, 1); + setupBalancerServerWithStaticConnectors(1, TargetKey.USER_NAME, ConsistentHashPolicy.NAME, null, true, null, 2, 0); + } + + startServers(0, 1); + + Assert.assertTrue(getServer(0).getNodeID() != getServer(1).getNodeID()); + + getServer(0).createQueue(new QueueConfiguration(queueName).setRoutingType(RoutingType.ANYCAST)); + getServer(1).createQueue(new QueueConfiguration(queueName).setRoutingType(RoutingType.ANYCAST)); + + QueueControl queueControl0 = (QueueControl)getServer(0).getManagementService() + .getResource(ResourceNames.QUEUE + queueName); + QueueControl queueControl1 = (QueueControl)getServer(1).getManagementService() + .getResource(ResourceNames.QUEUE + queueName); + + Assert.assertEquals(0, queueControl0.countMessages()); + Assert.assertEquals(0, queueControl1.countMessages()); + + ConnectionFactory connectionFactory0 = createFactory(protocol, false, TransportConstants.DEFAULT_HOST, + TransportConstants.DEFAULT_PORT + 0, null, "admin", "admin"); + + + try (Connection connection = connectionFactory0.createConnection()) { + connection.start(); + try (Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE)) { + javax.jms.Queue queue = session.createQueue(queueName); + try (MessageProducer producer = session.createProducer(queue)) { + producer.send(session.createTextMessage("TEST")); + } + } + } + + Assert.assertTrue((queueControl0.countMessages() == 0 && queueControl1.countMessages() == 1) || + (queueControl0.countMessages() == 1 && queueControl1.countMessages() == 0)); + + Assert.assertTrue(getServer(0).getNodeID() != getServer(1).getNodeID()); + + ConnectionFactory connectionFactory1 = createFactory(protocol, false, TransportConstants.DEFAULT_HOST, + TransportConstants.DEFAULT_PORT + 1, null, "admin", "admin"); + + try (Connection connection = connectionFactory1.createConnection()) { + connection.start(); + try (Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE)) { + try (MessageConsumer consumer = session.createConsumer(session.createQueue(queueName))) { + TextMessage message = (TextMessage) consumer.receive(1000); + Assert.assertNotNull(message); + Assert.assertEquals("TEST", message.getText()); + } + } + } + + Assert.assertEquals(0, queueControl0.countMessages()); + Assert.assertEquals(0, queueControl1.countMessages()); + + stopServers(0, 1); + } + + @Test + public void testRedirectAfterFailure() throws Exception { + final String queueName = "RedirectTestQueue"; + + setupLiveServerWithDiscovery(0, GROUP_ADDRESS, GROUP_PORT, true, true, false); + setupLiveServerWithDiscovery(1, GROUP_ADDRESS, GROUP_PORT, true, true, false); + setupLiveServerWithDiscovery(2, GROUP_ADDRESS, GROUP_PORT, true, true, false); + if (CLUSTER_POOL.equals(pool)) { + setupDiscoveryClusterConnection("cluster0", 0, "dg1", "queues", MessageLoadBalancingType.OFF, 1, true); + setupDiscoveryClusterConnection("cluster1", 1, "dg1", "queues", MessageLoadBalancingType.OFF, 1, true); + setupDiscoveryClusterConnection("cluster2", 2, "dg1", "queues", MessageLoadBalancingType.OFF, 1, true); + setupBalancerServerWithCluster(0, TargetKey.USER_NAME, FirstElementPolicy.NAME, null, false, "ACTIVEMQ.CLUSTER.ADMIN.USER", 1, "cluster0"); + } else if (DISCOVERY_POOL.equals(pool)) { + setupBalancerServerWithDiscovery(0, TargetKey.USER_NAME, FirstElementPolicy.NAME, null, false, null, 1); + } else { + setupBalancerServerWithStaticConnectors(0, TargetKey.USER_NAME, FirstElementPolicy.NAME, null, false, null, 1, 1, 2); + } + + startServers(0, 1, 2); + + getServer(0).createQueue(new QueueConfiguration(queueName).setRoutingType(RoutingType.ANYCAST)); + getServer(1).createQueue(new QueueConfiguration(queueName).setRoutingType(RoutingType.ANYCAST)); + getServer(2).createQueue(new QueueConfiguration(queueName).setRoutingType(RoutingType.ANYCAST)); + + QueueControl queueControl0 = (QueueControl)getServer(0).getManagementService() + .getResource(ResourceNames.QUEUE + queueName); + QueueControl queueControl1 = (QueueControl)getServer(1).getManagementService() + .getResource(ResourceNames.QUEUE + queueName); + QueueControl queueControl2 = (QueueControl)getServer(2).getManagementService() + .getResource(ResourceNames.QUEUE + queueName); + + Assert.assertEquals(0, queueControl0.countMessages()); + Assert.assertEquals(0, queueControl1.countMessages()); + Assert.assertEquals(0, queueControl2.countMessages()); + + int failedNode; + ConnectionFactory connectionFactory = createFactory(protocol, false, TransportConstants.DEFAULT_HOST, + TransportConstants.DEFAULT_PORT + 0, null, "admin", "admin"); + + + try (Connection connection = connectionFactory.createConnection()) { + connection.start(); + try (Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE)) { + javax.jms.Queue queue = session.createQueue(queueName); + try (MessageProducer producer = session.createProducer(queue)) { + producer.send(session.createTextMessage("TEST_BEFORE_FAILURE")); + + if (queueControl1.countMessages() > 0) { + failedNode = 1; + } else { + failedNode = 2; + } + + stopServers(failedNode); + + producer.send(session.createTextMessage("TEST_AFTER_FAILURE")); + } + } + } + + startServers(failedNode); + + Assert.assertEquals(0, queueControl0.countMessages()); + Assert.assertEquals(1, queueControl1.countMessages()); + Assert.assertEquals(1, queueControl2.countMessages()); + + try (Connection connection = connectionFactory.createConnection()) { + connection.start(); + try (Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE)) { + try (MessageConsumer consumer = session.createConsumer(session.createQueue(queueName))) { + TextMessage message = (TextMessage) consumer.receive(1000); + Assert.assertNotNull(message); + Assert.assertEquals("TEST_AFTER_FAILURE", message.getText()); + } + } + } + + Assert.assertEquals(0, queueControl0.countMessages()); + if (failedNode == 1) { + Assert.assertEquals(1, queueControl1.countMessages()); + Assert.assertEquals(0, queueControl2.countMessages()); + } else { + Assert.assertEquals(0, queueControl1.countMessages()); + Assert.assertEquals(1, queueControl2.countMessages()); + } + + stopServers(0, 1, 2); + } +} diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/balancing/TargetKeyTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/balancing/TargetKeyTest.java new file mode 100644 index 0000000000..901941b720 --- /dev/null +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/balancing/TargetKeyTest.java @@ -0,0 +1,184 @@ +/** + * 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.activemq.artemis.tests.integration.balancing; + +import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants; +import org.apache.activemq.artemis.core.server.balancing.policies.FirstElementPolicy; +import org.apache.activemq.artemis.core.server.balancing.policies.Policy; +import org.apache.activemq.artemis.core.server.balancing.policies.PolicyFactory; +import org.apache.activemq.artemis.core.server.balancing.policies.PolicyFactoryResolver; +import org.apache.activemq.artemis.core.server.balancing.targets.Target; +import org.apache.activemq.artemis.core.server.balancing.targets.TargetKey; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import javax.jms.Connection; +import javax.jms.ConnectionFactory; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +@RunWith(Parameterized.class) +public class TargetKeyTest extends BalancingTestBase { + + private static final String MOCK_POLICY_NAME = "MOCK_POLICY"; + + @Parameterized.Parameters(name = "protocol: {0}") + public static Collection data() { + Collection data = new ArrayList<>(); + + for (String protocol : Arrays.asList(new String[] {AMQP_PROTOCOL, CORE_PROTOCOL, OPENWIRE_PROTOCOL})) { + data.add(new Object[] {protocol}); + } + + return data; + } + + + private final String protocol; + + private final List keys = new ArrayList<>(); + + + public TargetKeyTest(String protocol) { + this.protocol = protocol; + } + + @Before + public void setup() throws Exception { + PolicyFactoryResolver.getInstance().registerPolicyFactory( + new PolicyFactory() { + @Override + public String[] getSupportedPolicies() { + return new String[] {MOCK_POLICY_NAME}; + } + + @Override + public Policy createPolicy(String policyName) { + return new FirstElementPolicy(MOCK_POLICY_NAME) { + @Override + public Target selectTarget(List targets, String key) { + keys.add(key); + return super.selectTarget(targets, key); + } + }; + } + }); + } + + @Test + public void testClientIDKey() throws Exception { + setupLiveServerWithDiscovery(0, GROUP_ADDRESS, GROUP_PORT, true, true, false); + setupBalancerServerWithDiscovery(0, TargetKey.CLIENT_ID, MOCK_POLICY_NAME, null, true, null, 1); + startServers(0); + + ConnectionFactory connectionFactory = createFactory(protocol, false, TransportConstants.DEFAULT_HOST, + TransportConstants.DEFAULT_PORT + 0, "test", null, null); + + keys.clear(); + + try (Connection connection = connectionFactory.createConnection()) { + connection.start(); + } + + Assert.assertEquals(1, keys.size()); + Assert.assertEquals("test", keys.get(0)); + } + + @Test + public void testSNIHostKey() throws Exception { + String localHostname = "localhost.localdomain"; + + if (!checkLocalHostname(localHostname)) { + localHostname = "artemis.localtest.me"; + + if (!checkLocalHostname(localHostname)) { + localHostname = "localhost"; + + Assume.assumeTrue(CORE_PROTOCOL.equals(protocol) && checkLocalHostname(localHostname)); + } + } + + setupLiveServerWithDiscovery(0, GROUP_ADDRESS, GROUP_PORT, true, true, false); + getDefaultServerAcceptor(0).getParams().put(TransportConstants.SSL_ENABLED_PROP_NAME, true); + getDefaultServerAcceptor(0).getParams().put(TransportConstants.KEYSTORE_PATH_PROP_NAME, "server-keystore.jks"); + getDefaultServerAcceptor(0).getParams().put(TransportConstants.KEYSTORE_PASSWORD_PROP_NAME, "securepass"); + + setupBalancerServerWithDiscovery(0, TargetKey.SNI_HOST, MOCK_POLICY_NAME, null, true, null, 1); + startServers(0); + + ConnectionFactory connectionFactory = createFactory(protocol, true, localHostname, + TransportConstants.DEFAULT_PORT + 0, null, null, null); + + try (Connection connection = connectionFactory.createConnection()) { + connection.start(); + } + + Assert.assertEquals(1, keys.size()); + Assert.assertEquals(localHostname, keys.get(0)); + } + + @Test + public void testSourceIPKey() throws Exception { + setupLiveServerWithDiscovery(0, GROUP_ADDRESS, GROUP_PORT, true, true, false); + setupBalancerServerWithDiscovery(0, TargetKey.SOURCE_IP, MOCK_POLICY_NAME, null, true, null, 1); + startServers(0); + + ConnectionFactory connectionFactory = createFactory(protocol, false, TransportConstants.DEFAULT_HOST, + TransportConstants.DEFAULT_PORT + 0, null, null, null); + + try (Connection connection = connectionFactory.createConnection()) { + connection.start(); + } + + Assert.assertEquals(1, keys.size()); + Assert.assertEquals(InetAddress.getLoopbackAddress().getHostAddress(), keys.get(0)); + } + + @Test + public void testUserNameKey() throws Exception { + setupLiveServerWithDiscovery(0, GROUP_ADDRESS, GROUP_PORT, true, true, false); + setupBalancerServerWithDiscovery(0, TargetKey.USER_NAME, MOCK_POLICY_NAME, null, true, null, 1); + startServers(0); + + ConnectionFactory connectionFactory = createFactory(protocol, false, TransportConstants.DEFAULT_HOST, + TransportConstants.DEFAULT_PORT + 0, null, "admin", "admin"); + + try (Connection connection = connectionFactory.createConnection()) { + connection.start(); + } + + Assert.assertEquals(1, keys.size()); + Assert.assertEquals("admin", keys.get(0)); + } + + private boolean checkLocalHostname(String host) { + try { + return InetAddress.getByName(host).isLoopbackAddress(); + } catch (UnknownHostException ignore) { + return false; + } + } +} diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/cluster/failover/FailoverTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/cluster/failover/FailoverTest.java index 43756f1b58..2cbbb3307c 100644 --- a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/cluster/failover/FailoverTest.java +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/cluster/failover/FailoverTest.java @@ -691,7 +691,7 @@ public class FailoverTest extends FailoverTestBase { waitForBackupConfig(sf); TransportConfiguration initialLive = getFieldFromSF(sf, "currentConnectorConfig"); - TransportConfiguration initialBackup = getFieldFromSF(sf, "backupConfig"); + TransportConfiguration initialBackup = getFieldFromSF(sf, "backupConnectorConfig"); instanceLog.debug("initlive: " + initialLive); instanceLog.debug("initback: " + initialBackup); @@ -745,7 +745,7 @@ public class FailoverTest extends FailoverTestBase { assertTrue(current.isSameParams(initialLive)); //now manually corrupt the backup in sf - setSFFieldValue(sf, "backupConfig", null); + setSFFieldValue(sf, "backupConnectorConfig", null); //crash 2 crash(); @@ -759,12 +759,12 @@ public class FailoverTest extends FailoverTestBase { } protected void waitForBackupConfig(ClientSessionFactoryInternal sf) throws NoSuchFieldException, IllegalAccessException, InterruptedException { - TransportConfiguration initialBackup = getFieldFromSF(sf, "backupConfig"); + TransportConfiguration initialBackup = getFieldFromSF(sf, "backupConnectorConfig"); int cnt = 50; while (initialBackup == null && cnt > 0) { cnt--; Thread.sleep(200); - initialBackup = getFieldFromSF(sf, "backupConfig"); + initialBackup = getFieldFromSF(sf, "backupConnectorConfig"); } } diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/jms/client/SessionMetadataAddExceptionTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/jms/client/SessionMetadataAddExceptionTest.java index 8194de2a48..39a7cc013c 100644 --- a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/jms/client/SessionMetadataAddExceptionTest.java +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/jms/client/SessionMetadataAddExceptionTest.java @@ -23,10 +23,10 @@ import javax.jms.InvalidClientIDException; import javax.jms.JMSException; import org.apache.activemq.artemis.api.core.ActiveMQException; +import org.apache.activemq.artemis.api.core.client.ClientSession; import org.apache.activemq.artemis.core.config.Configuration; import org.apache.activemq.artemis.core.server.ServerSession; import org.apache.activemq.artemis.core.server.plugin.ActiveMQServerPlugin; -import org.apache.activemq.artemis.jms.client.ActiveMQConnection; import org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory; import org.apache.activemq.artemis.tests.util.JMSTestBase; import org.junit.Test; @@ -49,7 +49,7 @@ public class SessionMetadataAddExceptionTest extends JMSTestBase { public void beforeSessionMetadataAdded(ServerSession session, String key, String data) throws ActiveMQException { - if (ActiveMQConnection.JMS_SESSION_CLIENT_ID_PROPERTY.equals(key)) { + if (ClientSession.JMS_SESSION_CLIENT_ID_PROPERTY.equals(key)) { if ("invalid".equals(data)) { throw new ActiveMQException("Invalid clientId"); } diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/management/ActiveMQServerControlTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/management/ActiveMQServerControlTest.java index 9430649f4b..1e1057508f 100644 --- a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/management/ActiveMQServerControlTest.java +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/management/ActiveMQServerControlTest.java @@ -100,8 +100,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; -import static org.apache.activemq.artemis.jms.client.ActiveMQConnection.JMS_SESSION_CLIENT_ID_PROPERTY; - @RunWith(Parameterized.class) public class ActiveMQServerControlTest extends ManagementTestBase { @@ -2757,7 +2755,7 @@ public class ActiveMQServerControlTest extends ManagementTestBase { JsonArray array = JsonUtil.readJsonArray(jsonString); Assert.assertEquals(1 + (usingCore() ? 1 : 0), array.size()); JsonObject obj = lookupSession(array, ((ActiveMQConnection)con).getInitialSession()); - Assert.assertEquals(obj.getJsonObject("metadata").getJsonString(ActiveMQConnection.JMS_SESSION_CLIENT_ID_PROPERTY).getString(), clientID); + Assert.assertEquals(obj.getJsonObject("metadata").getJsonString(ClientSession.JMS_SESSION_CLIENT_ID_PROPERTY).getString(), clientID); Assert.assertNotNull(obj.getJsonObject("metadata").getJsonString(ClientSession.JMS_SESSION_IDENTIFIER_PROPERTY)); } @@ -3782,7 +3780,7 @@ public class ActiveMQServerControlTest extends ManagementTestBase { ClientSession session1_c1 = csf.createSession(); ClientSession session2_c1 = csf.createSession(); session1_c1.addMetaData(ClientSession.JMS_SESSION_IDENTIFIER_PROPERTY, ""); - session1_c1.addMetaData(JMS_SESSION_CLIENT_ID_PROPERTY, "MYClientID"); + session1_c1.addMetaData(ClientSession.JMS_SESSION_CLIENT_ID_PROPERTY, "MYClientID"); String filterString = createJsonFilter("SESSION_COUNT", "GREATER_THAN", "1"); String connectionsAsJsonString = serverControl.listConnections(filterString, 1, 50); diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/management/BrokerBalancerControlTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/management/BrokerBalancerControlTest.java new file mode 100644 index 0000000000..08716ca0c5 --- /dev/null +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/management/BrokerBalancerControlTest.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.activemq.artemis.tests.integration.management; + +import org.apache.activemq.artemis.api.core.JsonUtil; +import org.apache.activemq.artemis.api.core.TransportConfiguration; +import org.apache.activemq.artemis.api.core.management.BrokerBalancerControl; +import org.apache.activemq.artemis.core.server.balancing.policies.FirstElementPolicy; +import org.apache.activemq.artemis.core.server.balancing.targets.TargetKey; +import org.apache.activemq.artemis.tests.integration.balancing.BalancingTestBase; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import javax.json.JsonObject; +import javax.json.JsonValue; +import javax.management.MBeanServer; +import javax.management.MBeanServerFactory; +import javax.management.openmbean.CompositeData; +import javax.management.openmbean.TabularData; +import java.util.Map; + +public class BrokerBalancerControlTest extends BalancingTestBase { + + private MBeanServer mbeanServer; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + + mbeanServer = MBeanServerFactory.createMBeanServer(); + } + + @Override + @After + public void tearDown() throws Exception { + super.tearDown(); + + MBeanServerFactory.releaseMBeanServer(mbeanServer); + } + + + @Test + public void testGetTarget() throws Exception { + BrokerBalancerControl brokerBalancerControl = getBrokerBalancerControlForTarget(); + + CompositeData targetData = brokerBalancerControl.getTarget("admin"); + Assert.assertNotNull(targetData); + + String nodeID = (String)targetData.get("nodeID"); + Assert.assertEquals(getServer(1).getNodeID().toString(), nodeID); + + Boolean local = (Boolean)targetData.get("local"); + Assert.assertEquals(false, local); + + CompositeData connectorData = (CompositeData)targetData.get("connector"); + Assert.assertNotNull(connectorData); + + TransportConfiguration connector = getDefaultServerConnector(1); + + String connectorName = (String)connectorData.get("name"); + Assert.assertEquals(connector.getName(), connectorName); + + String connectorFactoryClassName = (String)connectorData.get("factoryClassName"); + Assert.assertEquals(connector.getFactoryClassName(), connectorFactoryClassName); + + TabularData connectorParams = (TabularData)connectorData.get("params"); + Assert.assertNotNull(connectorParams); + + for (Map.Entry param : connector.getParams().entrySet()) { + CompositeData paramData = connectorParams.get(new Object[]{param.getKey()}); + Assert.assertEquals(String.valueOf(param.getValue()), paramData.get("value")); + } + } + @Test + public void testGetTargetAsJSON() throws Exception { + BrokerBalancerControl brokerBalancerControl = getBrokerBalancerControlForTarget(); + + String targetJSON = brokerBalancerControl.getTargetAsJSON("admin"); + Assert.assertNotNull(targetJSON); + + JsonObject targetData = JsonUtil.readJsonObject(targetJSON); + Assert.assertNotNull(targetData); + + String nodeID = targetData.getString("nodeID"); + Assert.assertEquals(getServer(1).getNodeID().toString(), nodeID); + + Boolean local = targetData.getBoolean("local"); + Assert.assertEquals(false, local); + + JsonObject connectorData = targetData.getJsonObject("connector"); + Assert.assertNotNull(connectorData); + + TransportConfiguration connector = getDefaultServerConnector(1); + + String connectorName = connectorData.getString("name"); + Assert.assertEquals(connector.getName(), connectorName); + + String connectorFactoryClassName = connectorData.getString("factoryClassName"); + Assert.assertEquals(connector.getFactoryClassName(), connectorFactoryClassName); + + JsonObject connectorParams = connectorData.getJsonObject("params"); + Assert.assertNotNull(connectorParams); + + for (Map.Entry param : connector.getParams().entrySet()) { + JsonValue paramData = connectorParams.get(param.getKey()); + Assert.assertEquals(String.valueOf(param.getValue()), paramData.toString()); + } + } + + + @Test + public void testGetLocalTarget() throws Exception { + BrokerBalancerControl brokerBalancerControl = getBrokerBalancerControlForLocalTarget(); + + CompositeData targetData = brokerBalancerControl.getTarget("admin"); + Assert.assertNotNull(targetData); + + String nodeID = (String)targetData.get("nodeID"); + Assert.assertEquals(getServer(0).getNodeID().toString(), nodeID); + + Boolean local = (Boolean)targetData.get("local"); + Assert.assertEquals(true, local); + + CompositeData connectorData = (CompositeData)targetData.get("connector"); + Assert.assertNull(connectorData); + } + + @Test + public void testGetLocalTargetAsJSON() throws Exception { + BrokerBalancerControl brokerBalancerControl = getBrokerBalancerControlForLocalTarget(); + + String targetJSON = brokerBalancerControl.getTargetAsJSON("admin"); + Assert.assertNotNull(targetJSON); + + JsonObject targetData = JsonUtil.readJsonObject(targetJSON); + Assert.assertNotNull(targetData); + + String nodeID = targetData.getString("nodeID"); + Assert.assertEquals(getServer(0).getNodeID().toString(), nodeID); + + Boolean local = targetData.getBoolean("local"); + Assert.assertEquals(true, local); + + Assert.assertTrue(targetData.isNull("connector")); + } + + private BrokerBalancerControl getBrokerBalancerControlForTarget() throws Exception { + setupLiveServerWithDiscovery(0, GROUP_ADDRESS, GROUP_PORT, true, true, false); + setupBalancerServerWithDiscovery(0, TargetKey.USER_NAME, FirstElementPolicy.NAME, null, false, null, 1); + getServer(0).setMBeanServer(mbeanServer); + + setupLiveServerWithDiscovery(1, GROUP_ADDRESS, GROUP_PORT, true, true, false); + + startServers(0, 1); + + return ManagementControlHelper.createBrokerBalancerControl(BROKER_BALANCER_NAME, mbeanServer); + } + + private BrokerBalancerControl getBrokerBalancerControlForLocalTarget() throws Exception { + setupLiveServerWithDiscovery(0, GROUP_ADDRESS, GROUP_PORT, true, true, false); + setupBalancerServerWithDiscovery(0, TargetKey.USER_NAME, FirstElementPolicy.NAME, null, true, null, 1); + getServer(0).setMBeanServer(mbeanServer); + + startServers(0); + + return ManagementControlHelper.createBrokerBalancerControl(BROKER_BALANCER_NAME, mbeanServer); + } +} diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/management/ManagementControlHelper.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/management/ManagementControlHelper.java index be7aa9e219..4c6b3516c7 100644 --- a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/management/ManagementControlHelper.java +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/management/ManagementControlHelper.java @@ -27,6 +27,7 @@ import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl; import org.apache.activemq.artemis.api.core.management.AddressControl; import org.apache.activemq.artemis.api.core.management.BridgeControl; import org.apache.activemq.artemis.api.core.management.BroadcastGroupControl; +import org.apache.activemq.artemis.api.core.management.BrokerBalancerControl; import org.apache.activemq.artemis.api.core.management.ClusterConnectionControl; import org.apache.activemq.artemis.api.core.management.DivertControl; import org.apache.activemq.artemis.api.core.management.JGroupsChannelBroadcastGroupControl; @@ -98,6 +99,12 @@ public class ManagementControlHelper { return (AddressControl) ManagementControlHelper.createProxy(ObjectNameBuilder.DEFAULT.getAddressObjectName(address), AddressControl.class, mbeanServer); } + + public static BrokerBalancerControl createBrokerBalancerControl(final String name, + final MBeanServer mbeanServer) throws Exception { + return (BrokerBalancerControl) ManagementControlHelper.createProxy(ObjectNameBuilder.DEFAULT.getBrokerBalancerObjectName(name), BrokerBalancerControl.class, mbeanServer); + } + // Constructors -------------------------------------------------- // Public -------------------------------------------------------- diff --git a/tests/security-resources/build.sh b/tests/security-resources/build.sh index 82484a4f3c..dec0ffd4dd 100755 --- a/tests/security-resources/build.sh +++ b/tests/security-resources/build.sh @@ -24,6 +24,8 @@ KEY_PASS=securepass STORE_PASS=securepass CA_VALIDITY=365000 VALIDITY=36500 +CLIENT_NAMES="san=dns:localhost,ip:127.0.0.1" +SERVER_NAMES="san=dns:localhost,dns:localhost.localdomain,dns:artemis.localtest.me,ip:127.0.0.1" # Clean up existing files # ----------------------- @@ -43,10 +45,10 @@ keytool -importkeystore -srckeystore server-ca-truststore.p12 -destkeystore serv # Create a key pair for the server, and sign it with the CA: # ---------------------------------------------------------- -keytool -storetype pkcs12 -keystore server-keystore.p12 -storepass $STORE_PASS -keypass $KEY_PASS -alias server -genkey -keyalg "RSA" -keysize 2048 -dname "CN=ActiveMQ Artemis Server, OU=Artemis, O=ActiveMQ, L=AMQ, S=AMQ, C=AMQ" -validity $VALIDITY -ext bc=ca:false -ext eku=sA -ext san=dns:localhost,ip:127.0.0.1 +keytool -storetype pkcs12 -keystore server-keystore.p12 -storepass $STORE_PASS -keypass $KEY_PASS -alias server -genkey -keyalg "RSA" -keysize 2048 -dname "CN=ActiveMQ Artemis Server, OU=Artemis, O=ActiveMQ, L=AMQ, S=AMQ, C=AMQ" -validity $VALIDITY -ext bc=ca:false -ext eku=sA -ext $SERVER_NAMES keytool -storetype pkcs12 -keystore server-keystore.p12 -storepass $STORE_PASS -alias server -certreq -file server.csr -keytool -storetype pkcs12 -keystore server-ca-keystore.p12 -storepass $STORE_PASS -alias server-ca -gencert -rfc -infile server.csr -outfile server.crt -validity $VALIDITY -ext bc=ca:false -ext san=dns:localhost,ip:127.0.0.1 +keytool -storetype pkcs12 -keystore server-ca-keystore.p12 -storepass $STORE_PASS -alias server-ca -gencert -rfc -infile server.csr -outfile server.crt -validity $VALIDITY -ext bc=ca:false -ext $SERVER_NAMES keytool -storetype pkcs12 -keystore server-keystore.p12 -storepass $STORE_PASS -keypass $KEY_PASS -importcert -alias server-ca -file server-ca.crt -noprompt keytool -storetype pkcs12 -keystore server-keystore.p12 -storepass $STORE_PASS -keypass $KEY_PASS -importcert -alias server -file server.crt @@ -56,10 +58,10 @@ keytool -importkeystore -srckeystore server-keystore.p12 -destkeystore server-ke # Create a key pair for the other server, and sign it with the CA: # ---------------------------------------------------------- -keytool -storetype pkcs12 -keystore other-server-keystore.p12 -storepass $STORE_PASS -keypass $KEY_PASS -alias other-server -genkey -keyalg "RSA" -keysize 2048 -dname "CN=ActiveMQ Artemis Other Server, OU=Artemis, O=ActiveMQ, L=AMQ, S=AMQ, C=AMQ" -validity $VALIDITY -ext bc=ca:false -ext san=dns:localhost,ip:127.0.0.1 +keytool -storetype pkcs12 -keystore other-server-keystore.p12 -storepass $STORE_PASS -keypass $KEY_PASS -alias other-server -genkey -keyalg "RSA" -keysize 2048 -dname "CN=ActiveMQ Artemis Other Server, OU=Artemis, O=ActiveMQ, L=AMQ, S=AMQ, C=AMQ" -validity $VALIDITY -ext bc=ca:false -ext $SERVER_NAMES keytool -storetype pkcs12 -keystore other-server-keystore.p12 -storepass $STORE_PASS -alias other-server -certreq -file other-server.csr -keytool -storetype pkcs12 -keystore server-ca-keystore.p12 -storepass $STORE_PASS -alias server-ca -gencert -rfc -infile other-server.csr -outfile other-server.crt -validity $VALIDITY -ext bc=ca:false -ext eku=sA -ext san=dns:localhost,ip:127.0.0.1 +keytool -storetype pkcs12 -keystore server-ca-keystore.p12 -storepass $STORE_PASS -alias server-ca -gencert -rfc -infile other-server.csr -outfile other-server.crt -validity $VALIDITY -ext bc=ca:false -ext eku=sA -ext $SERVER_NAMES keytool -storetype pkcs12 -keystore other-server-keystore.p12 -storepass $STORE_PASS -keypass $KEY_PASS -importcert -alias server-ca -file server-ca.crt -noprompt keytool -storetype pkcs12 -keystore other-server-keystore.p12 -storepass $STORE_PASS -keypass $KEY_PASS -importcert -alias other-server -file other-server.crt @@ -107,10 +109,10 @@ keytool -importkeystore -srckeystore client-ca-truststore.p12 -destkeystore clie # Create a key pair for the client, and sign it with the CA: # ---------------------------------------------------------- -keytool -storetype pkcs12 -keystore client-keystore.p12 -storepass $STORE_PASS -keypass $KEY_PASS -alias client -genkey -keyalg "RSA" -keysize 2048 -dname "CN=ActiveMQ Artemis Client, OU=Artemis, O=ActiveMQ, L=AMQ, S=AMQ, C=AMQ" -validity $VALIDITY -ext bc=ca:false -ext eku=cA -ext san=dns:localhost,ip:127.0.0.1 +keytool -storetype pkcs12 -keystore client-keystore.p12 -storepass $STORE_PASS -keypass $KEY_PASS -alias client -genkey -keyalg "RSA" -keysize 2048 -dname "CN=ActiveMQ Artemis Client, OU=Artemis, O=ActiveMQ, L=AMQ, S=AMQ, C=AMQ" -validity $VALIDITY -ext bc=ca:false -ext eku=cA -ext $CLIENT_NAMES keytool -storetype pkcs12 -keystore client-keystore.p12 -storepass $STORE_PASS -alias client -certreq -file client.csr -keytool -storetype pkcs12 -keystore client-ca-keystore.p12 -storepass $STORE_PASS -alias client-ca -gencert -rfc -infile client.csr -outfile client.crt -validity $VALIDITY -ext bc=ca:false -ext eku=cA -ext san=dns:localhost,ip:127.0.0.1 +keytool -storetype pkcs12 -keystore client-ca-keystore.p12 -storepass $STORE_PASS -alias client-ca -gencert -rfc -infile client.csr -outfile client.crt -validity $VALIDITY -ext bc=ca:false -ext eku=cA -ext $CLIENT_NAMES keytool -storetype pkcs12 -keystore client-keystore.p12 -storepass $STORE_PASS -keypass $KEY_PASS -importcert -alias client-ca -file client-ca.crt -noprompt keytool -storetype pkcs12 -keystore client-keystore.p12 -storepass $STORE_PASS -keypass $KEY_PASS -importcert -alias client -file client.crt @@ -120,10 +122,10 @@ keytool -importkeystore -srckeystore client-keystore.p12 -destkeystore client-ke # Create a key pair for the other client, and sign it with the CA: # ---------------------------------------------------------- -keytool -storetype pkcs12 -keystore other-client-keystore.p12 -storepass $STORE_PASS -keypass $KEY_PASS -alias other-client -genkey -keyalg "RSA" -keysize 2048 -dname "CN=ActiveMQ Artemis Other Client, OU=Artemis, O=ActiveMQ, L=AMQ, S=AMQ, C=AMQ" -validity $VALIDITY -ext bc=ca:false -ext eku=cA -ext san=dns:localhost,ip:127.0.0.1 +keytool -storetype pkcs12 -keystore other-client-keystore.p12 -storepass $STORE_PASS -keypass $KEY_PASS -alias other-client -genkey -keyalg "RSA" -keysize 2048 -dname "CN=ActiveMQ Artemis Other Client, OU=Artemis, O=ActiveMQ, L=AMQ, S=AMQ, C=AMQ" -validity $VALIDITY -ext bc=ca:false -ext eku=cA -ext $CLIENT_NAMES keytool -storetype pkcs12 -keystore other-client-keystore.p12 -storepass $STORE_PASS -alias other-client -certreq -file other-client.csr -keytool -storetype pkcs12 -keystore client-ca-keystore.p12 -storepass $STORE_PASS -alias client-ca -gencert -rfc -infile other-client.csr -outfile other-client.crt -validity $VALIDITY -ext bc=ca:false -ext eku=cA -ext san=dns:localhost,ip:127.0.0.1 +keytool -storetype pkcs12 -keystore client-ca-keystore.p12 -storepass $STORE_PASS -alias client-ca -gencert -rfc -infile other-client.csr -outfile other-client.crt -validity $VALIDITY -ext bc=ca:false -ext eku=cA -ext $CLIENT_NAMES keytool -storetype pkcs12 -keystore other-client-keystore.p12 -storepass $STORE_PASS -keypass $KEY_PASS -importcert -alias client-ca -file client-ca.crt -noprompt keytool -storetype pkcs12 -keystore other-client-keystore.p12 -storepass $STORE_PASS -keypass $KEY_PASS -importcert -alias other-client -file other-client.crt diff --git a/tests/security-resources/client-ca-keystore.p12 b/tests/security-resources/client-ca-keystore.p12 index 7c6fae79e7dc90da5837da734db100451745ecf5..14693945150f2299c87413a0413c9691c562d22b 100644 GIT binary patch delta 2415 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 delta 2415 zcmV-#36S=k6rB{1Xn)>_**~k%PNZq#T`WIQV>?$GZ6Gu6nGW7n>z#48Lr2GmIe9e_EwYl5lJcY zJ#T^~WLREYJgpSBUM8jBk?Fk{#?6?SS9K2}VIcZ6p&IWZ@qgmtwLAIvmvn~7)Xj%> zPqADq;;pDcXq`I%*W8hO2JEJ}^sm9KVgeMdWbDK*I^j~LGgXE$D8 z&c_e1KyY?oaeoRK|0rbp^fmc=gvd^YL{ZNalOXM4dx;jlpugt0xP z)u{i(_Vg*fMPrs+Kn{Y)M5i)FJN&~l(-=9gc~%6(vVW(`Dn1XX3(oK7c-(xtnK%f~ zW3_c}DW#Kmu!1#Saou}cZLQPNA4SB`9uGyVh8jNTqxVZ&OTM41)f+bY&xG}Lc>|aA zn&Y-YYPg~PAvZv7?Sd_0slZPoBQfZD|L`a!47wP)BQ!DtAQv| z{(l-CNPkNFJ9{Mamb~zPrB%~;tYcE5IRvJ9Vw3{v{_!SA{~MV?ht)w6Y2Dj%&ow48 zh;!V10`iZUsz6@TXin<5F?}=9|D%}D+XRF_f(nbn0Dw#aV|m#%okl0hA&*E3Cr0I1 zq}Gwz@AF~%fROYnMV!*8cuU4CoxbuX861D71%G)CEJXNVlre`x%;|+)c>tLaF6DDf z3%zLaLbr+335zqQ5o+YaJsu-b{${0$*sTXpn0%f%I|z?cUwt&7#{KB4_bekzsMhIc zkqH#2lHTkNS?9QIdpx&d6)<@i|*@O6>z;_0H0?ejuv@9Ru(ejY{d_Lh+DiNty zc9EGbQ2b9kJJ$loA??R}Lb~GA>9Ub5kvI&5Su8QVmH%sJN@~R1)=kR7(yX1y%6}V4 z6v6qf(LUZ;>sv_dH-g1)K&$zbxJO*gjT~6Jd2Uj|;5s3N{cE2luC2s5&nL{CM6wGCQ^>rD z>`XG$;XZoxYNgNH6%&=l%8^X$nt>&mF-9;U1_>&LNQU0x2p8%)EK0)dkvMlhZXrumS@B!%%>N1MoTi<`%pX<{$pSM0(vf8;CyWoX0qZ{3UjyjX-^t`!f_VGAViO;s@92XRcUi^aJkH?_v$u#gIi zImB9CA1yZIa-SK}Xcba0ZqTg-2PDO_z?+3fI%HJ?iv_w-%KbAs@Na(gC%Al%xvb1Z zZhefI{&6!H8;lt{-34AWMZ@oFF7tNn@3P}4s7x1%f8%LXiMi!TM{QTX9z&XCbWH!F zpj{;JfAW&3>!xU~q)LYx2||SC&lbc{Q&?F)_^#1VAqgf)41&&5S1_cedqS&O*%7hG z0m`czDMu{Bi*!iL6@G|4QSV}=f5QV?-IP#SRA@&x(L1v=OHmHnIXzG6L4iFS;RBD& z$D;Xue+1HU*7iPQE1Bdd5y|lWTFl>nd1#y__v^rRTGAV@1KD(pd!$TGz`=sI2lwp} z-}A54d*e}iC0pG!hY^d;FHy>xK5Y#`U#-KW0mN#ZTNUNi>a%iX?2F8n_BN5#WH7_# z_ipA_WYJpmK=vDD9_`h{6^Y>WZFS&c*s+)On!9*!)yL=@YwyhB1!0nq1DPDGg40RIl~ zxDcm*p7<1-r*y_g&*X!i9$EwYAvPAgn65dAZx=Wt ze;O}`)Lgp${syYbzHC_{RR5}4dk(Q{G@EvmI|LIJ3UTu`YN>)F!7ZW|LhSD_bsR8JRi^OePsHWtJt7fnv){;}be}oryQYJByr^eJO=Xv~@yoPb&)_u9WtLoOQ zdyl{*c2yx9rAz<- diff --git a/tests/security-resources/client-ca-truststore.jceks b/tests/security-resources/client-ca-truststore.jceks index 8c7f939c6c8310dc6c18e8ddd3842a2f75e03350..45501ac55fbd57670455df657e7f966373748c29 100644 GIT binary patch delta 659 zcmV;E0&M-Z2et>0A0ilk-z5MASS~d%IRF3ybTEPga4>=bSfB#}0RjR9cXmE3kwJMG zH8L_ZH!(L_7!NZrGB7bRFg7wWG&eCfT9HspQD7olxWHI__Dy27dyxcyk+V(4$K&fSzy`4bIS;JA2L2lejDm7bte zu+|bzG+IKHL;Dk3fB3pz#qB2rw5nNG``fo4-+nN5fIpU$%p z2@Xs?e?#eW)eqh22d|y@0lS~dYxbD7CboR{Zn^Rw4u8~ax4r-aXFJUGH4kWTPDvZck%9FNp_U2}Lq>Jp3LK(yRD$YH!Oo7MC-ZCDtGeWzW8m3_e zBo~Gj(IARfpe>46RXfGq6%mc9vfAC_V}7j40>#;rRRSe{!BueVnaYvfSAjlFvmq&@1GcQQp^GfK~ahnh{UkB7lR&es;D&Z zsPu8HpvNtGeyl2PJe_l=g;FQYe~t=B1|Bix<9lamBNYpor&RqsaTw^!7IMY>3U~2W ztkqa6!Te}{0xDZI#<`gZ-t5|o{$82a1sR5^XMPd>KxJaXxv4PfX>KF%+1!$QwYp4W z^?E70{O)#DLkL*P!f^4MZrVu`yGQtO@_@q0>Ca{F;4y6~@UpxJ={*AMml2eTmgJJK zWAvgIlQk665|hxDioOGjb>|U-7_0A0iAKGQa=@SS~d%IRF3ybTEPga4>=bSfB#}0RjR9cHf5vkwJMG zGchwTGBr3_7!NZrGB7bRFf=hUFfuhbT9Hs@{lK>U_lCO4dk~*OlLP|ytN^A?ci_~mnVfniv&@dYPjRDbXnwl13uip3&d51 z_Rm@^fW2=n68L)LZ=a7R`6WLi0sGI_VFkZiy|D<$))mL-DA(>&f<`=Riwyke)a-&I z)jM=NfA*Q7f@C8*LP55PY{VvSS3tu{yl)zrhq>8bP;_<4*uc~4I$$(7P=kTNOTP)h z-68-a*C>`iP7VXToLx83{LGtvnkt8+-hyF>Vqf?pAz%r!}Zb8i_bh+U* zMJfe@V}?}wsPB1F+A1yyac>n*bjUG)ZPKMg0)gd|RRSe{W`AESr0rJbBF^eM3w6}> z)SJ1sFbxI?Duzgg_YDC73k3iJf&l>lo|1nls+NxX8^qwd4b0B2$L> zU1%KOkGermrAW`Q6J~{F9)S^{V)RJk)oc<^LfxTDcUJ>~8+^;eubHZgGNNQ^i-(R; zr`=5h?`4pG^t+*uqOVQas}8=efW85Ol)Qb(+w72S#0D*1)Xo!b@ERU5x5#cRZ>F~L z=}M>%7nyJUAD*`$FG(R~i<;7fRq@Dcz-=d1QUIeA(pEl>PDl%@w7p@Lr>Jt>ostA- z9M1mklh1oYkw8c?dqJ({=B`>_|GS)6@?23Vus@eRJMD#6DC(ulHPDVH7V1AE@FQcm tP!w;bzfXyqk;~$bACw^h8!F;>AG0cHKknPzT*zAW4z^Dv!%Q;{%Lj*KD#!o; diff --git a/tests/security-resources/client-ca-truststore.jks b/tests/security-resources/client-ca-truststore.jks index e2dfeff87dba7532471486ccbbd18c4660fb7b72..49e2bc14cf450d50e2f6543a9a71e0bd7ecb2b01 100644 GIT binary patch delta 659 zcmV;E0&M-Z2et>0A0ilk;GzHpSS~d%IRF3ybTEPga4>=bSfB#}0RjR9cXmE3kwJMG zH8L_ZH!(L_7!NZrGB7bRFg7wWG&eCfT9HspQD7olxWHI__Dy27dyxcyk+V(4$K&fSzy`4bIS;JA2L2lejDm7bte zu+|bzG+IKHL;Dk3fB3pz#qB2rw5nNG``fo4-+nN5fIpU$%p z2@Xs?e?#eW)eqh22d|y@0lS~dYxbD7CboR{Zn^Rw4u8~ax4r-aXFJUGH4kWTPDvZck%9FNp_U2}Lq>Jp3LK(yRD$YH!Oo7MC-ZCDtGeWzW8m3_e zBo~Gj(IARfpe>46RXfGq6%mc9vfAC_V}7j40>#;rRRSe{!BueVnaYvfSAjlFvmq&@1GcQQp^GfK~ahnh{UkB7lR&es;D&Z zsPu8HpvNtGeyl2PJe_l=g;FQYe~t=B1|Bix<9lamBNYpor&RqsaTw^!7IMY>3U~2W ztkqa6!Te}{0xDZI#<`gZ-t5|o{$82a1sR5^XMPd>KxJaXxv4PfX>KF%+1!$QwYp4W z^?E70{O)#DLkL*P!f^4MZrVu`yGQtO@_@q0>Ca{F;4y6~@UpxJ={*AMml2eTmgJJK zWAvgIlQk665|hxDioOGjb>|U-7_0A0iAKG-3b+SS~d%IRF3ybTEPga4>=bSfB#}0RjR9cHf5vkwJMG zGchwTGBr3_7!NZrGB7bRFf=hUFfuhbT9Hs@{lK>U_lCO4dk~*OlLP|ytN^A?ci_~mnVfniv&@dYPjRDbXnwl13uip3&d51 z_Rm@^fW2=n68L)LZ=a7R`6WLi0sGI_VFkZiy|D<$))mL-DA(>&f<`=Riwyke)a-&I z)jM=NfA*Q7f@C8*LP55PY{VvSS3tu{yl)zrhq>8bP;_<4*uc~4I$$(7P=kTNOTP)h z-68-a*C>`iP7VXToLx83{LGtvnkt8+-hyF>Vqf?pAz%r!}Zb8i_bh+U* zMJfe@V}?}wsPB1F+A1yyac>n*bjUG)ZPKMg0)gd|RRSe{W`AESr0rJbBF^eM3w6}> z)SJ1sFbxI?Duzgg_YDC73k3iJf&l>lo|1nls+NxX8^qwd4b0B2$L> zU1%KOkGermrAW`Q6J~{F9)S^{V)RJk)oc<^LfxTDcUJ>~8+^;eubHZgGNNQ^i-(R; zr`=5h?`4pG^t+*uqOVQas}8=efW85Ol)Qb(+w72S#0D*1)Xo!b@ERU5x5#cRZ>F~L z=}M>%7nyJUAD*`$FG(R~i<;7fRq@Dcz-=d1QUIeA(pEl>PDl%@w7p@Lr>Jt>ostA- z9M1mklh1oYkw8c?dqJ({=B`>_|GS)6@?23Vus@eRJMD#6DC(ulHPDVH7V1AE@FQcm tP!w;bzfXyqk;~$bACw^h8!F;>AF~XMZV*PD^0s zqJik}#AAoW3xBo%zyM@}YQ;<$B&k_3pu~a>*(cu*?faeIF6Qdx1`+U5akxf#8wYal6 zm7|#5G{o+B1~+B|@+!7r?Z^ya*hd}n?3s(e^81mVDSv4&UeGtkxu1}Qx=NKTFH5WE zEwaP82v|b52@r&tVk&a+c8>HCe z#2*+6GJj7q?=JPkkw5C=&99XI=q*n%gxTV?Q)M-OU)b{QTauHf+6&a&oi`W#VZi6% zT%e=T5;cE@vR=i@*lQ2Oz@}3A8uQKo^2cEHIVAWUXVp9a=1iZyu?F!~<}Cr>R54N5 z;K#$Z-%4?~g|zfC-pcKP_;cGFtG?iI9At=j`hWL}a9B>Zfc=b7Uq`~rfD z>oe-@7{%jIaS~G(*;7>`fF}kv@MiA8;eXpC=FD_Yb`SE(oGAW$H`PxI#g0L&LqIsV z7xp7htc#X=Xr7(GE$b(?X583z9Kb#39#sXtQA4u7LxRpVG^^>mpB%e}hZG{KN}iZb zW?4-PR0Ok@z7g#EfHJwdyY2G@`0Z|~FAut%jSg!>xKIu6$o+{!&Buxu?#gdF8BJ~h ziA#-qurVZ$wj#|iJ}@CL2?hl#4g&%j1povT3Ap$!jWQG8tKQZ&d-5s~i;yYW1Qb2b aT?#+@R8o0-8YY6x(?GUjIL*NEHo3`!Dq*$gnO~(w^#)3GtW~4_bSQd2VmqH z$MgI=cq-2Eo_`@OSh2gzd8iMMgPUU(V&$bLMoO2zZqB!p7a%kb)72F;H|Ized3RIN zRLDABWcAcniX9A0v&YW&>Nm~t0{U(Fp%f$~9Ps(e;?(%2g~9;OA+L6CF7!1=q9sDdJj#8+pJR3mD$}zd{2PCz01b_HEMxH60ly^tPzR4Drkz|)@ zDFyOyFQwO5%+`HP*#l|`SP~A5oc%Prw`OFr4$w7De>iH6Be-W1h{8P$Q5@RF7u34~ zH;WKq;#0J6BGAaYk_>0s*?Qsxwg28bf+tO)0%{h!P5cvwVW(oimo+V zM{F@yMgoTrU2%!k(~h7;Zz$e0B>wTTaJ+Xs?LakVj|qvDjd9j(1Ig=?T@~q|91#)% zVsX%#S9wc^`|a4cOB(I}OQqBwKs2Hq`wwb}&wpPNX0uoNc@06@xO_F?KG=o^Eq8ue zqbRSY(~&}V^};i*aS=Y1OGpitx3c@Rm_gPmh|W~w3oNw7A}-|{p1|!l^!3SE1=`vU ze}g}beMNYtU+qR^NjT=FCmryF7^9g|NqzM#6Wfo8chZG$RfeZ;dv2l5KGVZAqqImx z7=JWT?JrcuZ@U7O#I@^St-%APA2zQKEo*A$ZDh8*E;3yIMjwak_mg_7+T=;jGwS6P zR}98WSus$10*BqP3yR&GED9Ggct^xvPsL|KgUgiorXEydJky5lI;Ncn7I28yKc_ zj0~dgoiAYUPutnC#1v$}E%pa?&$;r1u9K3>6c7JMM2f -----BEGIN PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCteE6eFAkH+r0S -3xmAZW5j+sqa8RGh+/KQJ6BgQQpsDeS3RkxnN2e8tSF27eBwFZcnhUKLBFGbarjj -sHRZ5HwDPty/C8RVhvbPWi2AvW8uEvh65G+fjyf5JT8jAfvP12EFv1u9sQjI1hXH -6CjX7lOCRjxriwz86NTsgiPVO3Q89pmhgmQjO0JBtolsxCZvV0DDS7xvGpmHudlf -UHR1ydjA0+s6YDQ4UIOBwUu/CcHdIgAk1yiWQE4OA72cXTfR/Mybfpoqh6TegnSk -ONJY1/iNgmujU5nOwDKlEG5BzSd0ueE1RSoFg2OGVPuo73lS2iouCXFvFU90yDGA -bdKlRIHlAgMBAAECggEAdbXYay4fPrnnSQH4tQafFNreVqtUkr17SFSLYCViZBY9 -aBwcxkFzdDrY3XHnRUdxTVEA6YJhuft+QIrBOSpw+GbUthLPBFZT7jo7/EsPQY1/ -7SxLjlM/BbI/mIrFC7ET1imWoC6cTmPvXbps1LGVGyZ742H0yz1XFrHsjMoOQzrW -itL29T09CYfZrB+/uo2ozfAjTDKVUALhrd4qN/uiJsHTfZPOwIv/qgZTSUHDsfZP -SbUjJjWoEWJBhIewosCeyFaGOYN4JmHUQG597Xp8PS+cAvfLWMpBcSsX1ULClY2Q -PSv0PKVprZdIfeOtQHmRk56lwhW2QV7PhwstKdqjNQKBgQDvbfFlYkCq6HwMcPQJ -h2hBIUFHm7rBVflw72LKEYE5oiouSflMVRujujPUWIHkF9TRBZ0f4B+J9sUXTyPY -wAlbRTAaG5JGLjF6JxLjkw5MiPooJk8YcHPaadpOgT/vLall3mhdQG+hEshtysHP -jdagK93joWVc0aTdj2NFkJUFFwKBgQC5ebxmnkb2PyzH2oatZNfMLWnLjs8GFoWe -NHbJTzLAadl/sVTVhaWHYDjvbtZPq+0ynzLGnNQ7HPtuSqNiG2bY3/eedWdruPIO -Dcztr05YUzDX5pItoUucu19V0k0sWSOeKBD5mTVdUHgCLxd0GyZ4ODkS63ItjiBM -78m5q8MGYwKBgCed7X91DnY5Ga2FUxvwh9OfCQosPm6XJzsEoTgGRXef2ZLnMpTq -0DP7L3BHZNa1CsW7RBBuKUnOxzXgJnJK9EFh5V+siDuMkStBI+L8BjWrxJi4HgZR -NRpCwZiT0lxlFc6BSouDifUBAqEIF6GcOpMuLvznS7pcBgeTHj34em/pAoGAW8kS -ovXQyCubTYum+kfdQv12TXXunWSn2xK7dgPraaz4JWjsQn5Q3B2SD2saQ3Mhftup -lQAnRtmg04O8NuC4lLrBH3maJITxxGKv9y+55ZvFoBJKZKpdcMKI+z+HUVsLdUj+ -nYZkEjmwKeSEBsEo2HV6SRKa/lBHS8ueWHPXn2ECgYBn/WeTob0JMmoF5dIhISpP -bA/j/gj2r7aTR7/o9bpmJjj0f71zuPvJRIo5L1qs/UvsZIoU8DuZwSx8KyzS6g+J -VB5gE3JBKUhshy8TnMNIR+ZzJBFYtYc1TbB2OSsWP6sIilFN8KQKU9RMpmo6yiZZ -us6gZcNh399Hz894wYKyog== +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCf8s5tfHnara0G +c0qtw7VdaQjXxBhfH6AQ/XbTOzHMF6FtTzWWTEq6wqoUuP1Y4WLob67O3ZRQ+RMN +V+C4eFgH9e7LlZ6gU7DWEk40WkKVQ/sTW3/4ul/F7ScFtyTvZ4zvNa6Qr0np4rB+ +SJlOy5yBZlNgmU2H+J/OsxEJDkw+Q+lz1Q/d6QevnfgBu5/Ka/aYtia2fPduufIf +Dn/UbLe+AANnO8z1NQ9ocOSgCcuocOmCssSZNGFXtf8qjMhZypO2c/bmZnuki+yQ +QhnD+yrOPbJMgcfL3jIesjNCu5kapmEGJBeGFtEgilegLYpYVTvF3RURjaqy2t3j +Y36sysXZAgMBAAECggEAHIzIj/5x+biac9ZMdCvEycGf1HOqLgCFH8M+XIHqZ5Wo +OMy0sfk4NZHdrgk/H4hLkVWuDDq86J8s6WrQL907SWB6mVhBkjieDfgCgZHC5MXX +oSLp+sm5oqisGHcSXrFLFL+uQyEmvlq6SjBHPStW6joLk+iJUEXusISB++3TaiGI +2LnHu0AZR+TNGk9tf+K48mlfpRimqq+Bugh7Sgh7uis9bwXn1p1Nz5hZDbBLGVMv +wgsC6xvvZJwNrUW7mgmVuytcCivR1Q/Tt1d8cim1bt+a/ZfRD6utexcfaZ47l5+3 +WPFb7UkMlXioZzrxDjlKO8J8qBcxcd8brqp5mX3KKQKBgQDRM0I18K4QCsw1zZFa +Zu7DjmLdxsgWSmpvmHVY2iFyUDI5NVcx7HU0ACd0lRU9IFnAIDzlNPnjyaShRQxW +IrPG+zFifmwyqNVRJfZdrm1ZkLba5CgJ+vVQbZF+lEh3M/mKID1CfLk4O/xZKeho +xXn3Nika/BqHNJlFJsvytoLdrwKBgQDDuu+K0KdILWsDTnVLOY3mjXKfJjw3P5MI +/dyoWFtW4M4xQ172aSVNZ11AHaiaDQ1KRXgVcUvmMVXQXXB+01DA/DArreDmREnT +gBwenTApjT/lblRBkcei6VtM5BLQ46uXueYzTJJ419WwYc0GaXG7lnipcz+L0C2a +thEsMrG+9wKBgAjJr3FWn+k6muNztDRo+ISseYi5bfRJwfjYHa3S0+7aYZG3pOcK ++M1raDzkelTsA/knIYe7Vvfzo3/Gx8LiiEzGhoeNqfvizbsv7g53Yk6N3rCJPwlU +SnPLdn4runOPcl8UBZ7CYIF1O59/PC0ShpIU61sf1flyAzI9c/nJIuwvAoGAWG6b +T9KZ4ehzUxkdsZEdZa8+vF0gE64rloJsMbtJ+WS0hFl2DErRSbmLzi4YQRHokUf1 +y2pW6ngb13qAGy0KbUcD1JhI5oCwAlj9W2+VlRB2cAh7FOzyj85zK7hYL/zNSE37 +je3ot6R8raZiZaU6d5Cyj4y8h0TVdfMQqzF0UV0CgYEAhpNPBVxNfwX/hFKdFQKA +1yDHTQiLhC8N0xfkI4FRQUdviVmy/LLRHincjtievo7bpE0sn87q990LK4B4L4dJ +y6024cPPCxrz1p2i43B0CuS7JwCL7UxaDQnHc/zwLYjZZJhYnZmfdjzThIturKEw +pUfEMSr5rFelUKg2aZjVAqM= -----END PRIVATE KEY----- diff --git a/tests/security-resources/client-keystore.jceks b/tests/security-resources/client-keystore.jceks index a0ac1a3dad13328141399bcd331793130ddbcaba..8efa04690a38efbb4e12e3392dd76e39f55f9853 100644 GIT binary patch literal 4124 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 WdPlV<`r literal 4144 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 diff --git a/tests/security-resources/client-keystore.p12 b/tests/security-resources/client-keystore.p12 index 13b246823dc606038f60a97a05b73a71aa44cfe0..7fa3d66372a6d83443c765e5bf6c204950f7e3f2 100644 GIT binary patch delta 4608 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 delta 4608 zcmV+b694U&C6^_TXn&>o=?JX>2rYHIyIn#)gKF~pOhy6&0K-rOf&|EZ`PQdSH0vhl zt|>+RV-`v9@UP^RetKqG)W|?2@P9j72Ox{%7fh5Qp5!X+p}@;v|^kti@y zbpbF)wP==5jO+QjLWwYS5PtOrjXICTd8=e+lPQqe=rrlrui;bZ4|YE&H91ypVWOWiy}1n&E(_hb7ePW{mhDZj07fQtTUlf3!pb zOJvE)GS@Pu{(+&Q7UuFP@L)>tE_DoW!jzcvmU@^#?WJK9}=O|v2~Kz9e~ zM{NOnJAZ|$1Afb-kWWd3!hryEF$NQd85>N6(lE`blr+nwM!8-EpJnGTY=dqne6%L9 zS?4G=YRM|HSE|$$3978ZW=E9y4jFSp5MJFA$>9(9-bR{bX?C)|?wWM)bPxEyg3^06 zS#~$gIaT3e;^weIR)1`Yky-2$?2YTeSmqIJM1R=Y5NwkgKQh)>O-UE%{_PGS1>w6b zecFhfdwSZCY)dt|BwIP+I|BT;H{S4oO^x0Tn7a1`Mb;VUY;HkxIW>QHsmQgl8Bsgmm^W?1*1$)#gUgFEg^WOg&tJ;hD>4fC!5Z=(*in#`Pe7@{ zZQ7LNVh=LpODN-Q&8&n5A#1V+l4>N_sDGgk6Jn2&mq-Psbx$giBAk%lvPEXu!k`J+ zU>$zdoOY)KgSdX+U}9sFq{h6jJ!x-K0YP@wq9vN3o*L-GN^W*)*t_{3dc~J3*CAoB z7(--|tnr-{f6oXAX@MsY;e*{oJaU@kRWZjHRv128*s7BBCBh>UP$L}43?u;M5P!u( z;T-iF>_(*e5jJ-?(8)`n-%PjS>!Zj?zFGg z!vbWVg``c--Jw=xU9c0efG&}VJNez(qhB3Mqo>N78=BSi zt>_2~juHEseKg8|qR5j$0K*=ux_=P9^vs2S19NLVclnt6RG(9s_pg{#l(ue!fvKB? z$p}fb;KlLjkMz7Lo;lUNo0a1CQ6SuDI<;N z16N$GnR#2K4(}zBocmHZZm03@Hi;rgvz~g#F+eaI1_>&LNQUEe~r)#Ko%bj3-w+{tj6g=zdTTxVgdsI!%%>N3~=L((aj%#9<84q zA`T)wbvxT>SM;X8WV+^Y99VXTe74`9~S=e+=54F6?gvdd1ieHGxaC z%2B;h_NB6u8I{rjjy5AcM5j&%L?WC0O!n~B5#i4-|F6U-;b>H#h^t%*Gb9FVP^()m zI(^wbsUxTC0z4>~ZJrd-0YhaMMBxK-P*?DPh>0YcI4gmtGJL4cdX5)D><9Xo5JtNc z`O``feQ88)elXpg7!elp!P*l)x5cz!SDFRLhRCyYS4ipyXfB71?ov0N!77R?7kxBJvU>5zn zPgeXNZg|m3<=>Y(TR*W8s6cP&7(-X2$@~bnyq5HT87M_`sc`5C^kkESTalP66$&MG zvQZgHkHn(AUO{0nX@q10kwj#CP;0Xcq|oXVp%`h_;nYnTh`zI`xKldaZ@Ot9^RD=^ zeuB$Ze+H*H;H$LS;J?BSdcB^^CIHD-G-zx$BoZx-Krx+~v8lS{Y>$Xh%ja0~T_W}= z9W(>;wHe|1mJMu(gr^C-yNC?{7H6|1DQqtoj-GA|xq`lO#7EJs>5sSz?IZ2pBi+=ixs zPrABy?T8*S~x-)#pl&1EO5_-ioc3Vex0;^(Inq><%_Qg*2T`WU4JW z;1M;fn-3E+x1_&9%Sx&tCJ3@Q7c9JRUvnh+V5E0ftDRz)JB8-c8 zNC-r<`g?49LqHtB{|$>GLEQ5;MA`PSf4+=zf^xMo5yhXe1i^}K0clyu1kQDXe+cH* zIP1+CGJsM`hj-(WXUo^SoJsrp(5u6SjQs$T)Cr7+J@l5qmQ)nuxxHcd%2P}e|6;nu zC;nt&QjDDHBt!g=OZ@+EpG?FGZ2bS}7JoGjZv~CJqt3ipU8>szh9UZ!D#4JIe*y}z zGKG28a6{dRxaYICO`PTn-@4~zy$dfm>B9j}su6AX{3x^sbQML`QposZ3Ck&Amk}Rm z+s-#0fDr6-e`PU87;e|+q?&>pQPrZjmjp6al1cxJmyt>{ z(>cQde)c^!&nt1_m{kp{f!lu9voYk(W8$#*%M3X)g1=uMd-yh@!LCJOIzyebZE!p> zEu&LxAgY{rkTGck1l=cmsyHk)qm$=woK5qCq}X~|joklFe{4`}HYY9me^Q=rLHVe} zbp@jehr)C5s9Jc|bQSPV*<=O_o4|$B*FYGf#FFYllN&Ph@i$LXZ!))H)?drkcf9D@ z&}88B`gS^zmH2)TIHYp}u;GrgtB9BpeE>1T(Xf3QjoqF`tWMkx1G?URPSx29pC)bY zLP{!L%wp-Px;gq7@zAtjf1y>u0903??I`Mdqq{9*8aiVR_T$~;hlo(qjJw%=Fof2YTAJEpglGsivke$(<>BXT_hc31I}u=lrnKKQZE>H{yi+8efbdFtl7ttz=nYiFBy+rd@AIMEpG2u6|Nf1sD{I|XSsO>}ZF(63Y- zaEv10osZEh>%HK&L^OWX!Ko#8;{oy^U0#+M4P3@K)olfgVfvfpT^m=%Gtj(Oowk%} z@z_{%!`s@&9ehiTWW-iiGqiKr9_79=8VT!X}0|n&H>X8}+F>VS`u8NT4JJ~KP1xWv*2^bf;%|eCD zy?-n#5S(rJ0{}GrxZr&g?_nEX1v~7cGG*Yj-()p=dCIRoGU^=Lakgq+e4$O?;6{PI zE9s9$f8ru7o9PiTBrKUUQaq@9Yr*5hM?|k}rkH?7+`C4H7^qFCOi$pKCU2!tVwG}S z`DU_hVj8lR!za-18wBdzcf+zG!DJdizGeJM&hGylxBt3YA~pG`7#mBXnU0FLX(0q4 z%Wm-;S*)0FwQt=(C~geu>+3WosiMIyCs(g^f4ou9V(6-e7$aJ)QYOqR8gugL)%YdT zRPp?rA+m?{<;wT1Lnwq7PM}{xLO`oMye6Qhn9%*P{lC1D+8SLs1F~)!mI;b#dLmBy zep#8Xqt8XsAr+2|O4Ms0)i#X-IOg3dpi3evLr&XPfB=ex~C ze;BvT6YV5rDzQ^r3=6XbqV)SZmu|n&l%t=s*B{qEZ~&Vu-XJi91L-s`hDKTV+dA51 zp~K~K8xeCd4w?Q=uUGfz?ss3RsDk~cKTl6S&y@p(XBU%iB4nD7=dp$6i1~BV2a#9Z z3REsg=1H%>!-Ne6flm`K6;vKt@9V@zfACh`YDy>oXAeL9PZO+_6Ju4kClrxi<3L% zT?eGdXOZ5jVezCP`m3$((I(%r{dJqQwwwQR1M2{z08^t919`%R9xEyEZ@!K?e~i@^ zl*dhD#ZRqF6isdhhg_n@6wED)X+~|R{{5^F-T^pG@q2_y{?GLL=mpePZ*!6+hNe;w z@Lbo+W9^p=p{Zx{I8`a!Cy$*O5TBJBR03g{=FV`*VumA51yG)a0!1N9y2A>-t(pML zToZm1WqH{UORx+j)c)3hYE?c>e=^$d`uNVNG7`2h=c!Qa-sTLpq*;4A<#K?-yOJ^A zj(u>xzWYHn7vxPTEUgsL^RJ4Omjv>B^JkZ}tD;_-w8sE3|3_=bjVbx*eX^rgu1dNK ze*jJzr!Qwq)IX`ZZg5#9zTe`O@ACywO)Hu|s`J6M|EFr@RWK{8m7vQvefkIG(<-@b{L>-*>Rs`~SRr zdsU~HpD>lmkaK?P%rA;3Sj9{BPaeZp@$55s654l(U~p7jya?L5kl7ia+PJc*u?RUD z3TqSiT4DugNibIlRSGw5=DyP8K6*$mEhPG@Y9mx1UfJNqu<2pjW=-aCgYW^lfdcUY zzz((^n$yu=+QPOVA6t!cCi!c?ve@Ho-wTs4J}@CL2?hl#4g&%j1povTCEW+u=T1J( qj-P#|=uyn5r>Pdg1QZ`=rO07i{YXKSs8?pV`EkWFdPM>Q0fwNM`Q!Hh diff --git a/tests/security-resources/other-client-crl.pem b/tests/security-resources/other-client-crl.pem index 45f51abe29..9c6610bc81 100644 --- a/tests/security-resources/other-client-crl.pem +++ b/tests/security-resources/other-client-crl.pem @@ -1,12 +1,12 @@ -----BEGIN X509 CRL----- MIIB0zCBvAIBATANBgkqhkiG9w0BAQsFADBfMREwDwYDVQQKEwhBY3RpdmVNUTEQ MA4GA1UECxMHQXJ0ZW1pczE4MDYGA1UEAxMvQWN0aXZlTVEgQXJ0ZW1pcyBDbGll -bnQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkXDTIxMDgwMzEzMDMwN1oYDzIxMjEw -NzEwMTMwMzA3WjAXMBUCBH493qkXDTIxMDgwMzEzMDMwN1qgDjAMMAoGA1UdFAQD -AgEAMA0GCSqGSIb3DQEBCwUAA4IBAQBzM0YCos5sHRAN4pPzNWCAonqezX6FfcY+ -SuufVcxD583O2Vnuwmz9i9PhGJJbWxGuCtXwS1JNldm7/rXhpZOd539W1BJQprGb -nwooQWTBBU8qTaXmUVWiPsMlL/IcMUTB/DVgWsRuwjA7wtVAseIoa2Z/geZZAOwO -vgp7RAtWW9M1Vr7/XWNsJqIOoPnPqGhg8Nve2sFfySQmJQZP8LnnDgC6pv51TnRa -VrOmHtralj2d0U3z78nRZW26S1XMxA0wb5yTc4T8lxCZ969vwtiWOQRCoKL/EFWe -Yy2oBbRjTHEZWYyhYHCMcGP2JSGcDnSZmc+d7ydgx4Gq7nHy3FCM +bnQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkXDTIxMDgwNTIyNDcyNloYDzIxMjEw +NzEyMjI0NzI2WjAXMBUCBHRbRY4XDTIxMDgwNTIyNDcyNlqgDjAMMAoGA1UdFAQD +AgEAMA0GCSqGSIb3DQEBCwUAA4IBAQA2w9pF0q2Lvrw2VxXNVJLyZNnV46kmlVTT +ao9bMK3pIZWNALmzc2FhW7yUZXmiIOvLHqdh08sVtz9sOmxhJcqKG6jmseygapRJ +lFcujq6E2V4XGcNIBXgE+3TNB65wvRQHL//NWVFp6pFdsW0CcsIFHUQetrkVkW1r +HlL/CKoXvVrt7XeJW4PPiq5sFKqNs5cOdpqTmoaeG823uE0BvdOEJIr1aIowBA2b +qeqACPH1VjVb+/JcHZlYmYor7RzUEdMmqF/e38QAF3bveiXfsFg2n1xKlUyBz4RR +6UxlLv4txWyAO6er/90olhQWvBbQDivKVF+n5XkRJbAbbNqxvy6h -----END X509 CRL----- diff --git a/tests/security-resources/other-client-keystore.jceks b/tests/security-resources/other-client-keystore.jceks index 0cf978c275b8447fd4f9a1a59faf58ca566b79a8..596996ca8553c23a5daeaf1639146d64cb311895 100644 GIT binary patch literal 4136 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 literal 4136 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 literal 4136 zcmeH~c{r5)8pmg|jLDv{XB$h@cxEg~c9NzvDqDjhMur(929>o@Q4HBiL~2NuAz4GR z7Ow~gMfR;QdM(LIsY5yEdf)e)>pJJ3bN+h&c&_KUzrWx0Jok0q&*%PLzmATM4iE?g z*}lM_9aNeJ(RZ&O(KmqT3j%?Ic1VfFff%tb0>oeW)fa5nYSao)OuOBI4t82I(zgKU1_Ro`(!DG zijvPe*3QWLZ4%;k=zwF3u+sU++&ovcJ2FSd)oO&AQxz9`<1lX<#~xO=%NO8Z&{@rI zMIVDp3lcmlFFXh>%zf-u95gZ`mFbzXzcpK-8Z4$`MLoP{$kq;PvW{3Ar_Y_r9qfSF{@2TB7mHAYHvDbfvdg)7g3_vV@r!7#~~E zL;9GC_PW~?SCn(Vxv9=*LhbQ5I#Tc8t9+Zgx7)ZWLvvYad8m&rh@&rDQ1HRty{i7l zUk^t2{W3HKn~n`SbYgLkOR3iV6=_jmB|gJ(vQ8tz){$55FmJOj(Sa#JtM@WvpcnR} z%Xn$-D-cR!SF`25E#NaQ%w_ueeH-&M&611WNL$6cwC8UPJNu=U4DMS!xRwgsd(^C<+Z$2lSpy+r^vT7K1AXnXV2$I+sPUVCHhg9L2|BKRpnY zc4FqS)=kax61@|!*2l-8IoBO)PLfXIFYw;(y-}GW6*OIc@0dt;yJ|mwtXf%C2rkO{ zRy}4I$!N+~Eo*Zt>U)61JPPI)9~oE?Jj-}RtEJjJrJYjejryX1e1r2FeN}WsJpC#k zvFa(xFUtTOl0Qbz_8eJ6&4m=@Dal=0f@m|T?5QzO8qdDSR+B6({Uv`C;%>D|<`XwN z>YRvnwsg>GQ~lfJCS$?=ppJfQRKT>T&@S+NY}MoYxbHd3eKr<`N;wrOopCE>8=i?e z7yV@HSZb#!yg_uP$GpJ$GG#a00wp54{IlL|Z&q<0-rC5#rwC0f}~P+1>uPgIn;fR#R}h$9+@8fG-o@Ngoz=gtk)G zUPPdW)c~_+vk{UTGL^^~Z0}2`R$THhWld<;+Pawkggb<90g?bZUsooH&DF$suhKT& z^&`Z7A?U}J*m9X#t>Mk=D4I`aD?IPR>Kk(vlIS*awo!c6mig&OPfWAZw5IFXiRk$d z!sW3RoWL>Vr7~|3`ZF&cOpCG)YL7}MQlev5n^shz(%Mib84F4JF%LQH(OSTM5D5Af zphJ5AI;18O3IRhPFo&|G8vrjST=km8wN+j)7{LJo&R_%qeom+jj7ON;*p)^KAX*;B z2mpLr5<-~E*q276ko+(@fcBOG6;}VpCfV&J*^@tJuB<81mqv0Yxw_CuRBu^hf0_r? zmqfb=$cpk}F@O%Bfzbf4S{Q;DKNhno?85-t0uYGV0q|_Sol_WUy!rL*i2Dx_*U#v` zN>&L#{Y+N+Uy{Xd*~E6r039s<`*MIeKu|iEAGE3SKC0B*(+0T zf?Jyl3X`w?BujT%l|W-sP!5r&*owi1jyYe0l1nf2mXCx)@n$(LY^3NV(4%gJcd5Lz zC?AQ^SoUvxQf#;{ra50(o8b^PeomrOTCmiwJcZJf^Ue*uyG?OVyw9X=ov5%kWbsUZ z1KQb|ZBRh^nrUSdyS{Axe)nOVnnby0GMPU3T{%%g7Tz z-&rtE);CfzpPqszcK^nX8cNGOe;$BuZ4ib*01zO&^{5Ej>LT~1>vC{#LBW8;mM91l z1_bGFGS$_E>_PRTMZ&^1ts0QtTH}KuVLSC2Et(e{^lbGehRz9^`%*Cd#*)A9(B==j z*ɗ@NopP&-0XFm<=6h9mipnAtaNfkh=dnwrT0i z=ktQ#eHn+Jgx1Osbqj7f-$141taT&0{e?zbJtxgOuJO*vXV(;nDAuot5ET59EtZx} zXFTY?ZEN4t9%p$}&6_-2%h9xd?A;=9l!m)=sY`+@n)xAAS}Nn+{6Jcrzb4z{6~|RP z`uV^clUdy5$;bY`a&=v{dvV(3ZyE!;%@CiiJfkS?dz-&=h`DpLnrZr#ae)gt;OOxb z#0~l<;(wIyI|arFhb&BIPrGGh1g?`;g|HIyOFp;{d_xvxU&rZv*kTxY_Fl!eKV> zyGH`0O1^O)X(JehTX%)njn1@-2O0Q4FQ&kO6*h>q8 z!QWvV0Ce;AJN*7F*?;tlZT#d{luM&87F$9W$&Mpo@VsVejfI3pNN)6(huv{biS2!| zm9HIaN$=Vx*6q6# zwv$xyN7-rg-^Fa NJA}pZ{%SBK_II@wtG)mL diff --git a/tests/security-resources/other-server-keystore.jks b/tests/security-resources/other-server-keystore.jks index 4661445cc7a59ea100b481c11994b037cc9c9403..58a752e78584cfaf612945fb244d8b8c8688ea7b 100644 GIT binary patch literal 4202 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 Peua-|sx7M6y`X;p&V%U6 literal 4155 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 diff --git a/tests/security-resources/other-server-keystore.p12 b/tests/security-resources/other-server-keystore.p12 index e21a4e11ff042e0d12a139cb911dd861b2ef52d2..7023c405c0ad0e1517796e291f393036f9db9a1d 100644 GIT binary patch delta 4739 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 Rwu>;z?WoSaFaiSshM;j+Eu;Vd delta 4690 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 UoE!1Sy0RjR9V@D@PkwkeI zH8L_ZH!(0;7!NWrGBGeWF)}hTG&eCYk=2`jj-qyEYtQxFQ>H*OcA`jZWW4+#p|;0) zTUpy-o&taxd(kh8;ymQ*X$6zJTY$K_cI9u<06+7m$tAlLM_;aUO`0y6wX5fJr%^wW zU!ZW$JPDIdfF51MC1(V!45zo5W0VKufkH=FnIEio7M`rwo7w8#wzOLlRWg&+P8 z#qa<#ngr$FUb8byFeCWx`5r7OxPzuH!b1WB0RRD`fvhlrsW2Y~163U(1Q;+DfE0t; zS#Kp<9^c#3#~txOa;f`Mruvhb0wI4#1_MXm4|Lf)#9UV_|G) zZ*z1mY;R*>Y-De3VQFrH6k&37Wo>D5E^KdOVQh3|b963kWrqZR0003n9R>qc9S#H* z1QaPctiU`;o5mCj-0%3D3(`xb5REVm1_>&LNQU?L_`h0-BaYtz+Xbk;76wcA03DBvCvK*CA%lnNN322$Bu-@<;z-w+4t&ljS zR&jtWvhVgDEhLPD)CdiH)s~9#fp44~EV5$IweoRFeN$^8f$< delta 734 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 delta 1279 zcmV(3YrcD=YL)0&q!3cRL0s{cU zP=JC2P;>jEY>%$gdtgG%5_stu@l5u6ort>bNo1hA<5y^j+?xO`rn4uA-&=BA{CC*< zDk||iSgQj6#>4pN7En9{wA06b|I}vVNceG_UH$b|VXYG7w*Y#^wH1F0e}K{KN77JVk>XcVB z5W2(qM91UmK#)*jgu&Ecw@pCscJ|P>5s11wUcF2&eG)(xYEwt9Xu2Smboy zrV5Y8O3+a~J}vjdo~Mt0kL3G1;ySyDZ!|l0(e`Yln`#|z^&tl=h3i29{*cs}T}N5~ z0{}*ogTD(1hYDWs&=?qZl+Koamk!fAW5y0Y`4b8olw%(mYDTzkbJegU=Pw2MUKbYbbO&X|wmnXco&iVu# zS!DqIpH+0t3yF8(#feU)wZu|+5k}8~8{*ZN4A0W-%5fe7llN_2_v}IJll|8oyhqIYR*C zk6+JQFS98kO#`vOZE&l9Ar40NZj}SoBr&FeTaOY1`Y;(MeQ|PTZ>OjuMXtX>#84rK9)J0Mj@IkP5Rb2xtyP$NrsM=D25@i0CxAutIB1uG6V0|FWa00b0(=^gU8g{t9K pJz9{3rqL4MrgO#w6y-^LL(P`t{aO*POmWhn4b`TeCISNihM>`uW5@si diff --git a/tests/security-resources/server-ca-keystore.p12 b/tests/security-resources/server-ca-keystore.p12 index fb54141cf8d34989d19258a996fa9259864ae67f..c9552beeb18f79e69b6ee8a11c9c68c2fa8e2533 100644 GIT binary patch delta 2415 zcmV-#36S=k6rB{1Xn%RM=aU8@R8S|SNsYq*-uSi3(**(p0K-rOf&|DkD5KO_&&+fH zw;s}@6_JyLUH8J>qDe5@5|0|~z6y(}TR)FV?dCNCNZwE3w2T{R1ZqT_j_u-dbi)@ zhKo|`eU$C4Tpy1B5fQ3&+A|WNYrc8`(tG7kKRpU|3@PvlW$v~dzA0Xku4-+C!ufWd zFg9@kNXQsdt)vD_PFX%jG)31@C5 z>i$GUJcOy>Ie+}swRI=i%Nf(uU+uV1j&W7z1Eo*NNB^=7;tVodyv#~1sh+~hK;~+i zsDb}kTpGwtGb6uta;_<(oq4rG?sYSOv05Lp+LEVRot>L_g~rEpG8`uf;#6XLOAGuX z2TlNqWSW!=3k$7bvgw`PQV<*eFgyu@8pKjeTd*Jj1AmMIGdIY6-bLm$Z^JLHBVCR(^vcEKTU zC=+8g;3R>y)e`rJk&DT@%hI%`4^J-H5;Z9D-!?BU!MZCgSy-uL%b)1Or53riBW2^v z1O7(?_J7N{k`utP&4rtXC8K3V-Q{!+Lkt@@WW=uH;3x#h1%g~Be$Gk`0ps4hVOLxG zphA*z&#qkh*y)I|9u{>g#-gs88{Z#rM3g&1AmWw=D^2s91o01xg(h87t08HXvwML$|jMn574Kt$EgJT)`MD`6%cM&j;c4K<$5C|8LiB^5&wpUY6$!SFN>`2pqH3bL+ux7e2YkkFn-%Sg zKlTGA8e1%Y=zpseJE_j=AP$jv;(9oUaC`kB%IK!Toe>@sn)(WmFo_iXiw-Fw8{Bug zW5?uBJi)w4SD)RHiX^8|Q0Y(OXAj>Ltr_bY=zn>X zF}}zu?u~yZCU`NA#9T6!&|NDAktlq73zcq{msf$;Pz&Y}&<7&S?45oG)cm7WEL&PC0LoITs)*6eEiQT zMM)9o{xTk~?>-4@{|YN4=gVZnJzIMXdSs&tnj9B*k^^3z1|g%b2(egDA31c$>mOo`L$y-sYD z^N`kHAhPT=1@YpYS)NMgb|zJIgv+GulRBg{F-9;U1_>&LNQUN1MoMyRtw)rK%XY_ zT2<}%wC6r%U$JW`(+c}Xb53&33)$Sfq|}B(%F8Fmc9N?*0rnVIv&Is|sr=3jHafyH zp*}AHgYLm?N1?t?r$MB4>VQnkBd$O;8tiW@qz1lk*lTn>Qa~)H(u(>c;qaIcm(ssY3Sl)Fn5{le z`;7v@lzE>3fAi%Rn)^oAU7zsPN*e;zutpF7%5u_QA)NcEV{1p97Cy14Xh%Ia7%L^1 zS-A-BC8XBQr@!g<8VC~?piik>zN(1|^~_U>#ee;FjGH5Cm#9~Ysk6-+7Jke5|( zJ0p8tI*GYQ4S7M>7M<(^YAkw(%EjO|Z%YyOGOom=fr ze|9x*sxc3~oILp}=Uq^;n~W+g0NKn(WB@SY*4lQpEVq|W{dU!=HPE7-f3=oXqTqT( zNw!E2f7`>;o_YP;jtN55!j34d^b&y9^4w)9a zLy)PM*U~tq;R8+v#+Q+&mK0LNn0vpFQ!%R?fALk@1 z zpjc%yV*Zp__c3VncUQBg9g1DpzggZ43AyN$>fC&}gf{VPAsq!bCuX_{v4at17ZOQ7H; z@dvMFLTtckeDl8TA5b)^Uj$okj8I`4&zG|#D|x>bjU^{nusg~V=2nv2h9X5Z0O;PM z!(`)0dF81mLRhysNYGfXj-)HfjI?IdcKqTN@>h3%ay6mg^qfRe>0v zT!$*@+OFUmaH8O|VPK)0x>qnhFd;Ar1_dh)0|FWa00a~cBaCt}uJ0-V!~?<7m{`_7 h5zEm86y~sDRM2t~fFa6YZ;jg6Ua51aMFIl>hM+(qeJTI| delta 2415 zcmV-#36S=k6rB{1Xn$Ff>5Q76x5{TEP{YduS&Qva%JTZTwy0Su;{wYc@YwL_E^3jh-6COTLvw-2?fyMkUEqZxR5Ie&`+wCn4z*t=7EW`X-p95J zj@^^iLCOV7UKq@T%f0=`#e8LVPi_|-u_e)re&8bS566zL9$^P~Qw1}ID6_kt2wJjn z6Hvr(3U#}*GnM&`8{bUF%u$6}EJ$;d$@3PfbrzQQ!UY_ghWI{MiVMj z`9xEv`hQmFpD!Ywtw&KI6A`)!@65589CK<6Y9UZCa;6JGM%fLd3Mbt{D`i}*%fVkL zAavJ)AB!|Juu^o9ai3SC(k$`IS*{XUWcSr-dL<$iM!C zCi#VT*Ix>>kbY@V^tN!Gdyq8a)>^U*9X>ZYv43g2mFx42^Sv^Q@vu%~H=aJhRj_F5@7p;y6kMa$>=P1wL zh>43#zKFDzcKc3qob2sDt1b10c;GE;LYSK@F-9;U1_>&LNQUN1Mnv5g1Ty8);45e zQZT1b<#kz5XRnraE9b1dj9#EfSN7G$9ZUA@U1kZfYzKITF|1jhILbcJSeZqN^hob1aY#mGdbvRF?4KV0YjI^u_p)sv%^~y?-lGV)QI;GL zekE5VMc7Pne>A&t+aK}BegU6%_CTiF0*7Jzf?p9$zEIT3y~fKbq?|jOg~Gl@dWapu z=SOL82{<_C1`QJrkhvFyU1XRr?R@OvA%s&6`h9nX*>M8axHxaMH&v(Wd5!lwKK)sD zGh*rZ`C;3oi*sz89iYleOR*fT36{f?o-Izrsyt43f86Xyu}_?+-nk+F7}3jwZpe>r zu3=LFAAIx?9WRDIp#IwXV~P}CCN(jw=GptJ41Y-IYJxGUf3Hi{B1a(xoMd5`KZ36{ zo!s3Y^Zjh3jX7JGseASNZH5Cn2AoNC{viJ?wZI`_k`*!yvqBbG)++KkFbwFqFq1@9 z+*_%Ge=5>MS0y?(N;O7BoFS3+O>Ul5M8v%PkUjqj>I40W)zEl;ub`4albWwnWt z`ydDS{Zh9YC!r^lHJxDKugb)7*;g%Vjww-_c^x5XwF#C#Trc>fB0_)5gGe2BXzEd+ z*A;D*U#w0q*V|(#rLzq?_s7ErNdrR{7S8lse`(PfER~lD0lA<8SL<@Zq4jpvuJ^3m zE08Rv8GyZzTi<=*ECn{wRNgby)poZ7TTJ5A=lk?gtTSe6G~aoza|6wb7SAjZMGBqk zIS)s{-fAc8W8Z4^`cU`C^*={L!ZQt_89f$6MElpcy~DPJUoyc_rX|kgBzdw@gKdfu!fn3BU2nR`L65i-KOGPLjxLvQZY;OErN`p=^xF%$>NMk_gU zw@oA)>)x#yx%^r=FOLS3-0(60rg;bQf1JV>?{ifQVc~#$QiZB?jfN!5DOJ;{{bPU% z6-uWeihUT{?zDtfy>@34d;lkJr)_C|587|nI0t!g{t81PmJRU_pGwC6KQ9!>wWA$D z$|JFI?B`8Bdp%YAl^pE4(JUbef5k`EqTdzDrgS33$2=bN$1E{JxUct<-s}l8Re61p z5)O}mu0Kf~Y)T=)y#>N^nKLjxFd;Ar1_dh)0|FWa00a~xS(^3W!-eWiRGqIO3Q={X hiP7!^6b=MpP)*~%%)XJAJ$d8av+3j8umS@ChM>s%hYtV% diff --git a/tests/security-resources/server-ca-truststore.jceks b/tests/security-resources/server-ca-truststore.jceks index 5c2cc80a98fcb5d8c713e26033c35e109e113e79..992f0e4da8eea52fdb49ac9117e772869102a770 100644 GIT binary patch delta 659 zcmV;E0&M-Z2et>0A0ilksi6P`SS~d%IRF3ybTEPga4>=bSfB#}0RjR9R3ojzkwJMG zH8L_ZH!w3=7!NZrGB7bRFg7wWG&e9aT9HsSwyUy#%zWsnmI zw!=6)htY=t|IjT*9THK8v^?%k!EH75QbTgH&1iaeyd|UFYL1jV% zH*ZJ!(BljzQTWA|V_NCLMReDy@B`9-8~LxzL%6cE_1BH6ZSY~uh}}Fs@y5+{*2dT& z1y$01e@eA2QhyT9GCDw0NCj__R85giYIg1V??o2yYTF8X}_xW+p0q5RV6LW zog|`8Qu_cXb*PqYNkOsuvr(%}dFU_!4@g^l3=}o`Xq+E)3zzEe5!jXxo)TttfP-(Bc(3JDrNNy-O>;Q#PQV=Ouo zxn9yyd+;DG0~i$M&5X;@?0JIc6k*GKOtIAR^J7eIT_=CkFpNr3)_Wx0Vj|DE1xUf4ay!Seq58Hy=0{5MO16iuH)Ytg%ObK>_fy0( t>tSofrc(SYHN?v39RwFYR9%mpwllzI3Pt+dv1K&J#eYHe9olOf4@BE2Fi8La delta 659 zcmV;E0&M-Z2et>0A0iAJ{r3O`SS~d%IRF3ybTEPga4>=bSfB#}0RjR9UKs?MkwJMG zGchwTGBh+=7!NZrGB7bRFf=hUFfueWT9Hsy3S?Vvk zB~w+mnD!6F>^SN+M>Lth>`#3KJ6cfoND_tq+L20+>WMxRe^m*bOLrWz{s>FUT0ekP z(S29o>CG8?K5zqhn;>ZOtx|I5`-agkZK zjof~bf3|@Y*|9z}Ykq~-r2ZjRw28VebQf*oCk7dJ4n^H5ImDA}>YEEfgAwN)a*V|A zce>mP23jtZd~Cf!IJ4_;AJ++D@o1AERUjM*{dTn?TF-VNGQV1+?MtQIuHda5WsF9f zZhuwibh;>S3J~(d@RRSe{tnF+)Kqf}!lf_ozMC$x)su}uTu1nmsN|heR2bhDv%j|pUpJvv!xLU(Li*_fAyKIP_!k?(Mxv#ogGFDN5 tT+cZAcS&e>Cl$94@W~0!7;H{8mKe&?-&3MczuHL{3)(>-QnuF^wG?4*E(rht diff --git a/tests/security-resources/server-ca-truststore.jks b/tests/security-resources/server-ca-truststore.jks index 3fe0f29ad27fec3158355a5c1d253d6934165d13..bdb6985de8aaf2deeddc414db7198a248ad53a23 100644 GIT binary patch delta 659 zcmV;E0&M-Z2et>0A0ilks|ElCSS~d%IRF3ybTEPga4>=bSfB#}0RjR9R3ojzkwJMG zH8L_ZH!w3=7!NZrGB7bRFg7wWG&e9aT9HsSwyUy#%zWsnmI zw!=6)htY=t|IjT*9THK8v^?%k!EH75QbTgH&1iaeyd|UFYL1jV% zH*ZJ!(BljzQTWA|V_NCLMReDy@B`9-8~LxzL%6cE_1BH6ZSY~uh}}Fs@y5+{*2dT& z1y$01e@eA2QhyT9GCDw0NCj__R85giYIg1V??o2yYTF8X}_xW+p0q5RV6LW zog|`8Qu_cXb*PqYNkOsuvr(%}dFU_!4@g^l3=}o`Xq+E)3zzEe5!jXxo)TttfP-(Bc(3JDrNNy-O>;Q#PQV=Ouo zxn9yyd+;DG0~i$M&5X;@?0JIc6k*GKOtIAR^J7eIT_=CkFpNr3)_Wx0Vj|DE1xUf4ay!Seq58Hy=0{5MO16iuH)Ytg%ObK>_fy0( t>tSofrc(SYHN?v39RwFYR9%mpwlhhpcN>-VH8`(O;{^DGn0A0iAJ|8oEZSS~d%IRF3ybTEPga4>=bSfB#}0RjR9UKs?MkwJMG zGchwTGBh+=7!NZrGB7bRFf=hUFfueWT9Hsy3S?Vvk zB~w+mnD!6F>^SN+M>Lth>`#3KJ6cfoND_tq+L20+>WMxRe^m*bOLrWz{s>FUT0ekP z(S29o>CG8?K5zqhn;>ZOtx|I5`-agkZK zjof~bf3|@Y*|9z}Ykq~-r2ZjRw28VebQf*oCk7dJ4n^H5ImDA}>YEEfgAwN)a*V|A zce>mP23jtZd~Cf!IJ4_;AJ++D@o1AERUjM*{dTn?TF-VNGQV1+?MtQIuHda5WsF9f zZhuwibh;>S3J~(d@RRSe{tnF+)Kqf}!lf_ozMC$x)su}uTu1nmsN|heR2bhDv%j|pUpJvv!xLU(Li*_fAyKIP_!k?(Mxv#ogGFDN5 tT+cZAcS&e>Cl$94@W~0!7;H{8mKg3UtUF&Y7>6H#r-eQdY=2kelG^(mETI4Z diff --git a/tests/security-resources/server-ca-truststore.p12 b/tests/security-resources/server-ca-truststore.p12 index 1fa9c61b533a13fcc74cc63876de0afd7578d7ec..8993e3dc321c9c8cb01dc6da6b4f95606d6f4085 100644 GIT binary patch delta 1108 zcmV-a1gra^38D#*U4Q%Rj!!SpG0`hUOHAJi1h3Ln(VGGT0K-s#f&=I(A=8!golLBe z{OB18y;6{`5U!Z##$K{ZP>VVSqAPw)F?SH|a3(;2s>GaBC`q=4lptPfPUP!t1~@@- zQ(yGD7fHZjBFaqo=F);da6$M>J!ktUTPZbcguo&5((XSwg@0+pK$8*^Rrl5o(gFJ8 z80sF6SPG)B2QR6f#|@_hxarO>K^MBw{VQdXUW8~8W z>}9DK*J&Vrey?jrbul~shxWvsIJY}bn__ANvV9pxk6N!LU8CL-{?Tj9jQvsOL+~gf z+6YXf{C|y|;PmFTs+R{8rrg+jJZB@y;3k^1!tNJ= zZht2)SnD@#b2+1~?P3x2XOLSS=KXB1mg4I=<|USs#8C*9_IBe{? zqPdkd+~FyvK`n`TU(anS5?X7S>oJj*NPkpjxBG5>)9Lu4*=pR*FKO`rai)A(6`Ck- zaVJS~S$=9h#0|xTcyDFtg+nD~qB34)IYCuCQ5QH-daK%G_3y}Y+swxE_A5imEOTSJ zpeCp76UrQ~P^*En54A#siR(7o0<>kBUBtyd=<6W5ihh{=eTBB@x0ElsSjRGLecd7NWgi0KpqEi< zQMI47i_cL5pTKv!mU%cVv?>GtPI9ThxW^C9cd#mUcdCRRq3l6mg_*)Mji5vuht0*3 z7PxOCK7S3#=-X0TMEhG#?o=zNl)UKd5o8wGo}K*xw>6B$b`H<*1tR@L7)M!38+~WA zXryl{r({$xAcM?YaIh~D!bRi3pVoF0CL^!o-snq9be?PhouOlX?I%mu+daM2qZLUGS#SBB*A1 z`NG0B$vN4*TTb;e0y2Lq)}y&KShJ!ORTT@*7nV&Hx7HguK8Bnl(i1?WN76tJ6GLl$ zPk&Z+vZ3Y#FDb&ym*wu#$b&^NnQk@ES}QBEx^+LXWpMU7Ej^|Spsg{il@|H zfg`JmM8}_IQ*HEmesFW9yd~m~1Rj^07A_3eiTtVnbJRU#e)sCt&nfVovVJKxUX>FH zOqK7t(cwfs4EQ?Tj*scKN2t)|t z-R*D8J20}jvVJ@s8T*p7pc!4pX$ -----BEGIN PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCNcCgfsx99PDN6 -+cK7pt1Z6i+6JVNVt5j2D8XsOOo2RzSZwOxPfQU7WlD2SBKF/tqRSo/qiT4Tf1UJ -nEt3HLP+CEvLWj+AVNF9V+DpzRl7PnADeZsgaPOtUnLn+4bRSnwGwsUrCeJaJCQN -drNt3sREpaQ8WizxR1nicLyN3H6RtoEV2bE+NGt+hdek/iFWtIm6L3QXbeMnBhl3 -DkXdKTnEk2zqmwtCgxHnHXKMxPB3utwKBloulHxsvUI4s+twH9cJYvFokyBVIBwJ -/Xa1IlrPdiEyv1qk7Uul3a7grR1ljEabbn9V6HS6KG8KEPLE5Kk97PNKU5LFkwPr -PC/QY8tBAgMBAAECggEARQlqvFZdV26sHimNMLU5NCtIEo8nhx5vriNy02PQhp/o -/+eYMRBwHlFuVVhGmlsUani/mJZW04OCiYddmo1LGgMIpACwID7GZm0fnl97QZnv -aPLRkldIIeCtr6gpXT0DHvWw8doIP0GGy3+WA1oJ6QwFB2RorXjLWej3UDNBIHP/ -UN/DuMvvl82ZVVpgLSAxWWDQxZpDE7Mvwcpd+yms9qhzaH6Sf3/TPxkn6tPrGSN/ -E3O7ez+ixqATQ5L+N4ZsBUWfrX2fPplZB3Zmt8QUSYDZ9IeO0Oga4N6g0PRxQILG -QxJ7MCwu0DAGx3KgKWsQg5f8tLSeHzwEHnz63+1xlQKBgQDO/cm2bJ8bqyIxERTD -s8FekrL2vlzTd+uChZEIX74nCjnG6jWK7TExqq/56khGORz5OFSRXqKR6CkEs5o8 -SzUHduc34OtFsQovyxFSxIY7O8qcbIcpav1CA5S7BtU9zleUr5Av3DsG8hZwyIhk -zDk6Vf/tLTH6PVGPfPe3E5mAxwKBgQCu7Qn7HkjLXcVzgzpp1CXYoBQTmKHZf2fF -wnTASFrRjQTwVN+p0afueTqzn4TutSosKiymtGgVonZoLWmBWSuKbEHLECHXlQcY -wjRAccB6u0Q5NZLcVmFLVjLiKw+kljSNgpQI6vYgPWp4zF6x/9ioRbz0+3wuKzsI -pUkcPg5btwKBgEyWeOFH1aNOMeuHz3AN/dl5XECR9RTFxV1ZAG3hxyD41qH0HPWX -h+FBr7U/65gYH9FS92+GXY6xISQ9NC9lAG0PoMP7M/JobEV81J8UWjpmiDRSr7wy -exzG6Gw/Pf2NcLhyMV6UFT8fqg/3EwiAzBf6pCRk2Z4mvBvkeF/EH8MXAoGAIuzm -6kGQrTIKw1Z3KjwWVlsXxxXZctCSSpTZtK59m4s5aja39XMLwXxo8QYvh22afvjo -s1wfz/oBBCnU/+Nq4xdcR4vwBdgWc6YKwrczhA2xwG5m5SFGCcGrJScN14G5+msQ -3Xr0K1m30WiUm5uGiYprAMrZb2poPgCqST5GpZ8CgYA7dc8QWQWUzaP1gjA6hspC -4qcHecNaYxaNPjhR9kBlzx9VXtVpqk0IyDkHIdJ7nz+GPa9WJTSmkgpYwz7hSWw7 -O8PbsxZ1qY4j9/yNUGcIodjgwUckwj8ULkl8mDGQCZByImZzjqHUfWuezWmhjW43 -sfD8CrHOirVMRbu49FEAVw== +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDYRlaCSUXLtzgJ +Q1+Q1CNlkBMJtsM4PIfRhwH/0C1HHRJRh7Q87k7BbUwL1ZPPcydw2OKnpw9mdDOV +rwS82vV8W/KsQWVCAzdvR/rQ4wwnUfjFl2Na6cJFdNeq8APSgBv5r81DuLK09deN +qm3wYc2I3Tw+8cbNddbG2CEFVdJ+SrUtUn8SzzI6QFNIBW+Mgo4SffbK2TmKJ3Pb +Y0SxU7vw7G5pv6r226pC5lUlLc2dJKJOUvsAKHWolm1JQbH7s1GrTXnoMAEPSFuW +rOlN4X9Vep2zn2bry+sllXf0xGybpNnwVa5uIt+muuYoyyH8NE4NbGEncMYaCaQA +RVCbfykNAgMBAAECggEBAMATV+lF4fLWubGgYFNj1DvzBLVv11kuiQERAjmLTWsG +6qn196DVlKQ8yXkJKcjn4HNca4+x0v+O2/FoCrEfVT4o+xD401EZQjZWmu7Bdp3F +M0do+BhZ2uuMpa1ulDZzCGVsOMSYWD5WyCVM427FraCLu2G7oHAA48qdUFZIrjTx +ySYmXFZqV0Rh+k68Jv2+q8Fy2DP2t6n9wbUilIM6iYi3DM+bym0n5AuG8Nc3pqfx +L4RP82/brYcZo6VX1fNnAZRRUqWP9Xv8Ry8ff/9FiLKXSMT8v9uSL0/tp1PFIe26 +zuS7Vw0xGOOH4nUtctIa9iKjZ9LRy9pYMLs/RR00TpECgYEA/8ekPk6sP5J+zty1 +iaRmL66/s+xlQIGl51Xt63r684tI6tL7Dv+r37ocCygEHNPa324iPvtau+UV3DL6 +x5AwJhLP6kn71c9l9jfjynRurtlenY6X1Vn6H29qeidQ0y/dEuclEI0+H3Sa3MmP +9rhfdy7eYYCdurvxawbl1d5Hf5sCgYEA2HX954wSj6VXFWcCLk/06v6kBbhMRDBL +u3uR16W3bm2fnoM4Hitbst+5Hcj6nspGKDBdhy/buUtF+BCmQcGlGnr5SBar0zsF +VwRKoOwJsdGzaUftj+EV/gTkx6Tloa1h7VsqMjSICuTnn6817RwdkaZ6Y43etu/N +VPwwGWAyCHcCgYAK2ZEiA3xTBgfTBpG80Ph0tVj0bOauodFDyuVYw9K5WgMx0tlL +fZTw3Jgr8PqbrnDuYWGaglcK+WDAAnmY1Yj1VH71VUYVf8K3ew9ymxXG3PmifVX7 +euGdS8CcheZrzu/1yVBNL3CfLPcUvogY0yFZkOdmA2qtbSOEgrplJBSsWQKBgQDT +GNbB/leHoR45MBjvY2id2DHLpj0ybsscjtjfLqyh0+TLqHqM6Ynm+snEY2EOhINA +5FIB6cllfiRBVLNfA2NpXK5JAFsXh8KgZv4Ey8x0juZh8RSbsU5KSSl4DbcoIjeT +S8nt5k8aGLxOfYegsj+f8HQBLLUbQOfFTp/1z1tb5wKBgQDx/1ELg0MCRXyaE7Sm +shZR//UBPldIXGr757oUOXc9hpfotws3QepMu4o342v3V3zvTBYhBf8WIyRYZdkG +gXZG/aGfmuAb7KAgzqdw/3kLbAVnWfMhRVaexauIYLIVkHsygjTvpCbaKyAzZR86 +ZeAzkZ3ivQvbKY7GDjgbJ3GjHg== -----END PRIVATE KEY----- diff --git a/tests/security-resources/server-keystore.jceks b/tests/security-resources/server-keystore.jceks index 57cc472efe029d89ae6f7a73ccf3ce4484ee613b..fba9d8b9e81100e80abfe8af07514ad5366be265 100644 GIT binary patch literal 4150 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 I)(|iL0pakxbN~PV diff --git a/tests/security-resources/server-keystore.p12 b/tests/security-resources/server-keystore.p12 index fe5eab72684d1cda5a45f34cc0981bdb3bf3dc01..0a74923b7e13b95ee95e91c5ec5b4e4ac5ece2fb 100644 GIT binary patch delta 4698 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 delta 4650 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 diff --git a/tests/security-resources/unknown-client-keystore.jceks b/tests/security-resources/unknown-client-keystore.jceks index dde733626e7af45fcc92c8cedd1da901b4cb6b55..41becd8da60fab7f1af10c19c079270084c22b5f 100644 GIT binary patch literal 4112 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

    q(IRpjskr-^ZqD*^t3CYmnkj2YVy?&@52A z?+c2}$NYtBqA1y?UU)e^J}F-B3g*H&rOVmP{zQXMw?o^W5!j{5uHoa)#}PSc^U^tp zx-Mv(v6Fp5Ufuw~nR6f|qP{L}2rU zDXOE&SrmDzDd_LY;?Kp+e@oZDD~rD?i~n0?@nycDg?&cds6zYENaHtp$uc2UiGKjC CkiL)r literal 4112 zcmeI#X*iVa9suxpX2vjMH%Zx-Y$H5FVIm9_y;C<_ySuv~5D4^- z2kGYLO(ap2-CQ6LAc9NO!4JY_tFjx!K_D;+NQDJ}RH$t(3<^M@@Q{}Y2#_0r#KarN zFL46^CmRHG!t#T>2$(e-h33G!QM^NlrWRN}kY_W*iDt(KQHcKD$FN$U#%2VDR{i@R z$)BGj4Zlx7(vTQL@%Hd`bD?;XNs{!7Ug*_2U(kBNLG@-JurF4(s^OJ$A~` zO=J~;FX0p9oD}(GSv?tGwWrU8t>tJ_B?t4lVCP$fzF8~k0Uci&*Vnhb(^Q#f+7zA7 z^gOD&jJ5&3Hp#C;j(k|Z-K3E$OiySSa3WBG@AtPpeZR+fmlh%BRGUc)vTaxzvZGHS zQ{RjS)^yQ9^U2qTjHql(Lcs`?QlQ1M?fY_;>YHyx?`~BqE zS}p!?-0}rR1sO;dG^a4J(f+K$eWzYSVE_ckR|CQCCqWDi0PAoz5Vp~PVjH_I#sl99 z-{Nu7X=lNV&7^eq)E>^@-o;)dH5 znhBVzv{n4FgO}*5lv{Z7LgSuFv_~9K{1kt#>nG#!XMO_a2E%zKp@(5qk;tx&a(XFE z?TWh(Eg{+B>aZ#712Of}?Yz`0<)#DIc$&+jn*nM@bWxny1~ z4jR6c4)+r6UYkAF6JeO9Z>$!9FPogaVdu1d`=afFy@g_hUY7Pu++l^O!mizqljXNg z>B-E=xYZN3kc(Q(lNf1TV<{3DQh*3%EKK6&MrHyKo?wzMi5yB&{*j$OQq%r31OlH0 zsqiUK6oFJi0B`^UMFSu&oWt07H9sKmx*`+?~7b`UVx)4S~jaPO0mgAuKG7&7O&15-o^)l>51p*0+1TQ}QY;tEa<8%aCI1k|_qDcRT+=TPobFdyk+c)Y8l9CFR%_p?;OpYVp`FW+ zT_Ds=59qAQj7*AIel+|Jjod!n0TEi(-e){c%G` zqA@`;&0`swSrip>;x#M!O5hfa0XC)lTiWmzVO0yh4!<$*X?z1bNg@7_{aK>NSuE{s4(7Fe_FRra5gFt!7o#Rkh9u- zE5%l@Cu3kVv=7sqIy4h0g>+n(O{g-_tF88^igg!Y2?49w-eDO58VfC`8ufP9mJcPb zO!_WlRmZzb+)+6pKWSo|FmtU$Bzdhp=A3_hIO${5NPJ9%@i9=(_no`w`}3&ttGmaXiPWJFj0jvLUMmN2(e%Ib@DYz(^{8sxTh zSzZa98e{)OVA+51VENIw6t20)bIX;=n&C+ziwPz|1sS^vGqYZp^N3wN@IW=F)#vRS zf7Lzq>(%lLoPY_3f>)%q`kV9GXAfKE-fi7~sQkl@I}cpjKBS!cVK;n>qU8 zlO;;kpqm0oo zl7CN?!lD^Gr>cSp35NqL1WcV1_d8C@>YIm<+>Q^@4@shu>kG#<2JzwU!tB@72$<1J zDG!l>T~Ye?)+XZ%r?uW_ZpTN+&N$YB8o$vwGOJd+UrJ{_A-!@)nJT$TFX(mrsPazN zjz`EyRm=YQ`p(>@-c*Cp9SQKb0yD19y9UWdQ0?mlg(ha=9bY*IIf@6HsJBE=9%C!K z@BF?&AO`IAFI@x!ZR@lu*B%8CtO$d-idk50JrwD2Y}LK$BjG6#<%>5*+@C~^jD5|% z#kVX)9nfZ~OdeNEJ48J_%MVyZzP|gc@|IDY-hO`BhwOsmhSpBa!&kJXA2hHe$p34EtDXbku zQRi+h``V&*&I6k{zrxs>XqePc~WZP#2DX7^8_#5yW{5(AkM*ZY_F z#S47*4RYUm?yRXDJ41l3m#XaKPdQC|$Y4h7O(QF*2=MH(|1y8#_x8D7zl#oeFm!B+ z1!KfxuSCV^IKGd*-_}h2sx1Co-2A6>{j0M0tFri?RTl06lS9E@PI*2(bJV!hRqn_+ IktN>00RhsWnE(I) diff --git a/tests/security-resources/unknown-client-keystore.jks b/tests/security-resources/unknown-client-keystore.jks index 10c7c434aaf611cd91db4e88432c2abbf18485b0..f3498bacbf96f6ca04aa343deaef6a51b2cec78b 100644 GIT binary patch literal 4131 zcmeI#S5#Bk8UWxls`MZrB1EJ}XeWUPDk{Rz6hUg}#n6HfK!OZKN`s1V(zb4!uZk0}`cOQ17gnxp&=rAMU)(!&&F-|D1m+jkxNrvg&hNl7eB)}vBQ-=TCl7wv?e{wFe!e` zXP|7Qk)*i+w+>}g+tcTznJOd}U2iX93S*1(rbmlIbSVa{so^dr4wMTw80j6Qylh&dn?47Z#2cm`W*uJ<&l zq>-ZRh%MVe-u~ILC86B+)1*sK7I`jK<3Pg(vg1^Vet2{1AX>CUNu!1$obAXgAs8jl zjktx)kiTVNc9K4@pZCsBCGQ8ua3T3E^mh;_7zBQd27s@ba0m_t@4%P<={+DLnbCj020c^PTTr1 z7FP&HhXV<*EFTz>k0hw2`Q}_VmTnJ?Vb^6AMNNzax#ExP-$%(Y-BR-r znraoeLf>TfU&iM5TgblLbb}zRF-=tzw!yjgRzMRM!yRZFT2M4OLWN22VJ$ z^zN7Vpt5Rx+*+Va&E~>YGfh^DmU{2y2ZZ-}@ERS4&q`SRtLaYX#bJ~ua;c=2b@~)! zE}1v+*1hrgV%+K@!2}F{FJD5}y<0RW+3zE7gwQjXaKJW8KE6%-jb8U-jEEJ>aAI=6 z1wcKXL`fDXS((imy!Y@I7QW=fGuiK>9lBmYp0@5*CK$x!N4QMz@+ZiDtIqGG*$4tL zfdLY1Yqz}EWtqV+FcQKG-c3gs2{w&rn@D%@V@j*UK^oZ4_}f8GwWEq7?Ah}JH#X%A z5sw_8skst{UZhg2MbZarlZ&p~T4)O^?2kg+L6evHBQnD#OPM_1kakYd54*QBdvqc! z(x}#`VjA3Xcwsi6&iCs27eyP6kHqC#W4CdB`*-F;J>IEyH~U1YS$}PyX+@sl zsWY_=ZXYK0UI??I{X)M<7uSoiU1b;O4$1mKH?zM0TW`tr)WrR>e&5jWN{7XIbm{}M zxTf$UuaLMd^W!p$!K4$dQ-y^X4w*YrEq2lZV6IFR)ao=v|HfiTaVjRoSJF`)b=ZMf zZ9}S11oc2Vp%(RmMOp4?U1g{m81IM}H7+~&m^!59|3EkfcfCA+1kMyD9?$qdOWgkS z_`WG9Wg{DHe1O7#l%;Avskc9IKTSI5VJmCLrS+AbE*{~vcTMZ_3k`@9RPwTbF6t?r zaI_%Lm?zs{vV*f(va7o|U`!9{wSc3xoY$`HkrbcXAmWdiu_b_U+QDNIPXMW3`?Sf&X>0ZwYXCWx%4Yx&j#w>IX-rq6X6-oGvP12aM!^^?B&7d z>6sFQ(E|2mEn(T3z}0Ipj1zt5i*}ssXC?1u37czpX*%g08|)e9-uG^K>t|lpZn0NQ z39p}|R6gm^>zxnGIY2R*DdW1<6^6b5uNLCjD&k}H%?W7EtvcVFo?p`ilOPjze z=1F71@CnJ!E1Ka z@{?>mulPo!xLrnCy9V;p%x+|g6d9jJ?+=W29*i{hL3GcA-kYEvSF3sFr)wwb7gvUs z%XMYyi{iPj(AHv}u;#NxOsNB3OlKT&PdsE$lnIHnA#1z6dOe!=#Amy6*+=P)MsK^N z1u3M)�}Gu4!bPlHM?e`H%&cbo){3#RO;^J}`Ltgd81Gq*BwLDRo&t3)$@__&g%N z@tAai08?msHo4zPTq4zPpx3qmf+|5`C77qs&$9Y5&n41aD~^_QFQ?S5;p^SYlDf4N zy%lK7e3UK2?by!;wd2|~vc)u>YmnlUJVNw0I@zM_gE9^vyWiU$Co7y@(xnR{)4Z1S zK2tFvEFZkOIup)5+^FX8fMufNL!)JO)GaNGt4ER&%6uKKmdvZVBZrEGaPz$S4iJStbZ7>d>j2w zpgIajeFK%)zktf@+u89QR=Z01&w8OkbWYX|d%xBeKB$!@>!LiaYMPdODY{PPFrdM$ zmTh{?Pui|-{f-aO-`AtvsWTE9e{@qKbm;a88-$XTr0>I+D!n{|P%Ha^hBCH&OXbg~ z2^7?{)~(9v#n^(s( zY;Ac?8hxa}%|Er$oyC%YF|i76NRc;PI;5`xif>>;-CWP%m>hD>p;kkn|ha17BLq#;1A2w41B4uzZ(Y!n@9MgmkK?t?C8ay&vO6`U#9*8Xaz!7ZK zOO+sT`?PbVCZ{b8%7kZ>m7JTu>E#mO0Eq2jD^$v z756;di2aL>D&obo$f=8dIN~)1ksOWHS$nCr_&`bZk>6TWwdOMC-MiR|CU3lZFy^CL zwxUfNx4KzHg5Zp9j4KmtNU71l7uohJirdrZty8pgzGu0_N@V1%NbNZ1>(;8bldFBm zy0gXpw4Pu-h1~l=k#-Sefz5fv6y9e|(1`O6Hc_dm9l?$Q9Whs_8-a%%oXI#V?~;Nx z4fP6i1g`W(yFe9Jdl;Q%hAt_&S+x!9K$bViKVVi}*p_m>@Gw;HYw7fAuv1NK!7;28 z?*>kt9QCVWk3-HfwP+YV8Eq5jTYY_W+Ww>oDu1hr_;<7M=YHqEmFwTl#^24x|E<}0 Z$}(!nv(}q5bB1!b{z-~uR_3X}e*pT#(PIDr literal 4132 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 delta 4600 zcmVP5Adm;YOfL-p)G*Sy%!C0K-rOf&|E5;mL@@Q9p{# zCa{ht!KA12mXUGOj9WmB{)m~=mRH&R$JdHfFN|^z&uPnj+oPaywTP?}ZqA#AV z-F^$;3U2#Q(SPFeI+hc6AG?fwBQ)np>_>2yc2ULU3`mWI(sCeaeS3Gy!`!?xfF0cU zCDypb0S|&Xj%!li*A|}vA?{(Xum$j*+N&}^D9xXz=XA2}WniwM#ku$V z0t?eAIPy+yqjvggGJrH&p&|~u^BE#$RXa5Im&EGYRevs%+3S*)E*cGR_{Q;$K))1a zi}%?*Ml8o5$&DlAO62?n%Uie!DS{?5W4q@SI9Pjd3TJbc znI5k>9zvnBT=ok6M4YRWrXd3snspW4m~Bo$?x1MlS)rX7T>XPFrMm4)7rIgoN3IZK z-D8aUEq~x3{3b8C=U6ocpkZClHVEXXIp><%1Z?@M3hj-q4QFEVP%Gx>7^S*5RC7+p zP!GDGT+2}ufd*%bT=_EW-fu@xtSbS4x z(s%foPSr`!szl&=dU%MU4!&GX#E)&RveP>G=cEuP31J8yXj6{bk)J~|6qQGA(LFP#bSa&Dqc`}Or$WSrGC{Zl>pI;wmT0}-ub z(tpyF%C9d)`DX>Ll*XN)rKj$M>{GiS3er>jU&C*zZV2G*C%ROT?Q&%VDo$V%R90^4 zyO59zavE2)SL9{p+1D5U4>A#A&5O^cobc>xLE^;Gw!!dplP@*pT?R zacilG%NtHW^s0ZYDbR&L#Bt^8W`#K$V zNFi!Up(z|bNkdn3yI*_Z1i-vL<2{m(V`CPRW_^A{59_Yu^HS8u zg+*4HMk1TDEc@7S1Uy&ZoVkRM{=FK9g=TW%{ANL7?u}-j_FKlxM?{;_^>(L_8Lrou zzkcrX5?`z6APexFQ|~E)B@W$X)0T}n_rF7-F;Flo1_>&LNQUN3~2gc1+?5p5-Smu zby6-9)9!giVC{FfH@9wbvlMqUe|?-9wx%>}&R?K*LPqPX$METW5xPZ2?Qz%+pru-OjJ#AMlDcgZ5<42BLW z6%XVCW`b*rL&H*EK7m|NwSY^Z{7JY&0GoGQH&=RR>hXs~HC#z^bH+GUoR6QhX)%iOn&Aq?f2k>+siZLte-AVS;M@;PH)u-8C~@3K0M;8}e1=YS@i#-@<h_`Nb9%he_UftzYaxiJFID^eVS654O_lyrF$*u z$;}niI~&0(TQtDv-=XJ{xnXhLu$5riZkw{^NbMA84wekzEwE@6 zt4yEp(|>4sax&IB8d-}I5W5vw*dETwbq1#?Pr@0e@RA!i|6!<#i2aTU z5KdEaA|_vxdgWKFG9_ zSDIYH{NcRYR^3|4yJ&DcE?V8ne*<^s#MS^L?Ue+u@nJy#f0q&+1QH5o5;n>Z)X+ZH zXTkyXA_-e4u!ZF$*FNHtWSz)@kU6AY*XRW7W0v)U-iul5k$7&HR&vWm&_WZ!S5L8m7-tFA^~>Mu@`G%g9}|w7qM+JOP^LALwnO5~$GVPlp&?9C4)>Lz zLf8gT;RHgddnj4-xUgKG^Ew>$A;y9XO&VT?)tIX+@U-Y~p z+pf%{wb%e&m}_;sONOplqG*Q;WpKsEQ}90EeEH;|*ZivgF ze!j3Y7y`+vdd2$yGaV`7;{sBbVfy-%=L| zArng_FmzXDQ)SMhyqt37pa=H;mfZLBQl+S=<63_4nG}c^1u^LrH-BjAwXF^`699{P zdN4U1RC;{9T_2|i39_lL=^kEpw1rFdVvl;9$-vIO3RUnMj5x2Z2wCbW;(EETW4iYf zP#6*=e~@h^Gj>E4f$(t~|0@s@(LH1X-!Lk3G=R$4nd+qye-j~4gfT?cv90C{x>^XhOk+3f3ngv@0UKIE0f-anehEgugJ|3lx7~8 z_tat6V9_0#8_9a9^AZ$n*R##dFju8P1B~=eVJ>@7iKCyL82Ws-uX8#?U)vrf0iI6; zmLEnp-oDFDyFV}or$zCKz2H7`yX)pOoZ$P;QQ$mdhiJLm;}68y!*W7C*_7WAX(wF4i&q zoNtGni&zK0JqJG>Tz54SWKO@==^{J4M%L z@AM+=qs%(zxYR4~mv+Cl%tB*wDu&U3^dX1Bsw%C=#Oz5Q2)Gv;%5Slu2n6jcVEl!P zWvAkRgZ1MIICu?P%XJ-AAf(oWOk0kM+ZG>+&9o&v)YP?hkDzpjv5BqXvO3Uce=8-+ z0l^4|nDy$6Z%N5W=^7?>4_7w9=}9eIBr1JB)faSqcIj1y?+Hf0$cEv8{rJi z7A!nQ6Ax}d$k*TX)*HZ1eZYhCf5!DM;v}`&?0~at)KECT+b&Iru0h1NkJoF8gnhds zOBcfWE@<)-@2=`i>vBPxeN0gN;IO}ls)Kk`Pod+4iyg37Vr&lkMDr;AZ{%gl=8yjf zD!6NqL-=GJGJnLZGb{TGSvS1lJ1~sOlEwDD*6gsD+1h31k0Ld44cBLXe``{IF~%2; zrcsv@N}$=i`1=Ue4N$NlEU&Eq`TD@hHaO+u~HI$2$-eSlQw72hM&oVIn+oLx$Z2yd&IdvgV}`*=gQ zd{rQPt5OmGMkDi4)h-y6e^4WuF8&h9M{VWsNDyX9cvJNUh2+bYtn=w&AvED_?QqrK zbbf!@hrd;;1A;3|6UWef+4GV*Tfkag^pJrqKRAwr-_ou%t&DQZTml;ZE??3*3JSgZ z&bMHGfb+Pxaj$T%l^x9*fhT?dO`;7+QZXw_ZN?-EE6dJsCT9X~UV`a##AUK$qR(LY zcpN&PJmdADnQMo)wgdHK{PtWhJ}@CL2?hl#4g&%j1povT&m^pkA$qid%0Vn`?PsWl iGvR*P1Qb5#8<-`35#*}`hd2h=o!>@j2wMUJ0fwOb0P8UT diff --git a/tests/security-resources/unknown-server-keystore.jceks b/tests/security-resources/unknown-server-keystore.jceks index ef2b078e4c5f4420a316339a8958f023d81ea0c0..3a0e45f0f77949c3b3ddc95eb781fb607aff931c 100644 GIT binary patch literal 4112 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 Fe*mM)!4v=h literal 4112 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 YXAMmPq}$O?I|eVUvU82omO+;P0EZIVrT_o{ literal 4131 zcmeI#XIN8N8UWx_AfSL!L~1CZ3f$0pM35HIC?c2uQVeB6Z=si<3RXbf2J{JqT{CpbBV$Ku~{x0`&$c5bHcB1Pp<|><+_j1N)f}vau$y zpZ0^nY;X{80et}AV1io0Sh-mAo&4PbiRP!#oB;b?h>eR`-`Ah$;pT_d0Mzy(P%g!P z3_||+gtYiJ0i*%Z*WbrmnL_OwdzBlU=aA85&(QV2|`?8@GcAvKp71n#MpHq zb{H>=`w`t;_o$@N)FdLM>+?d~P-c;j&+k{2ahMpZ3I7^1$R<4xGp~cmL>;$7Xva zdu`Q!&^34Xrp;tGUsC+NX?pJ*`Vs0U#Dp603s)|9ej@QBuac?zS^lZkD$hlavX=xI zOev_aDc$iWNb3`txd57i8YSQBeG*=+)FZ zi;x7Io}8b`;^ylq>ws9C-?%AyXD;Z_lY+WJ&CCx6ovllg`3!~Ac!yr}fFzysi{B?n zmu#w26+TtTl7PI zgnJjQlW*s^y^L9LRgJ;5*+(mkzV*W1FLB_>adg~lF-kQuF5}>EJhn7kzOrxBFZ(g1 zrFi$6{;Lu`b9G0*9J929X2EpyWrEU-9@Xtfu9ff9r46; z)lLk8Xw0N6mFr@N8tcwV=nfnl>{`-^)3ji2Wsnd!cV2#5EtRC33l1Ii+xZ$(MzTKZx-Z%L!}4b|C> zLwVZx9Rw?d`@Q!kW;)ACxpi*foubm|z8e|E#!EA2@KrpHEkem1F&NL}1D|S%GaXAo zD0%)5L!4bRzG^P#Pl=^*zT1)%MjoCnNfz7o?zrC;$EPma@cZ z=G^wOQpMQm<J!s8A%`J>pM;vjLkiGj(rBNj|WnX=z#$)Vy8DclRh?baMD&J>l zYwiS_@A)8o>$dzNlzX5n_L&v4c$n+YQ_XFBX(gI|n%d}4ee*Zh8%&CF!s4WtukhPr zEl)c+hX@}uDrFh?5TX29P94l%dr$bej&7f>Uv*xGEGfM3rlv*4$iu8^!&GFed=&~M z2TyU){u)fkB4%CH_;Yo#j^D<_#-dj>s@e!eWr=_Nkztaf|@m)IiE zL*a`Ldu(9HO&U+vlF=wzkb`uGrmhuuYn1rn;9JBd|i&x7Jx8x3&S)xo-)_7k_#?cB}I%29$i zyL?pJyDL&>Cj&o!CKfC5sMOmJm(8iY`RKiP|M+9XPh=~#GfoTVkR~Ca8sVw4+EX{o zD`FKn#aR)dZE#T=M-ee5wj51}%}(Lmm{?&kE5_bbavni;Rt4**!BSndHdx~E((I{J z7)An8$5EfNao3}_PqiGjKA}y)c%KHh9dBIeljt5736x(o^{ui|dwt$ZcTLJgljoo^ zR2e(J?n;>!X}#ovwa6Sdoo2J?dHGf*m}qffu&*oU$RPh^QJ-T7!R3a<;Pe=(hl5p| z+0ABpj2ugLZ`Fc>0D-WQ5G~qh7yV!+e9W&B z8~f9>^8W|IAB>`^$}ox&nt>K|G_YqBMgRy#?*mx(1jNJz)n^FlM~~$XJ?8Jd{|Z#n zfaG^jiTo3&EWh6!KVZdB%D>kOh0=mA)e)Pm>7d>0FW@qE*WVYEp|MXXAyNgGGUkH} z(K&xrusP$R8He;cGx1;B8w77to4^4ErP;jO2h+BeIvQxuyMgZ)^hF|EoLtWy?HC1N z679EFP*v|wImlcbaOI(J zrm}RcQvZ0S?-#AdS$YtM&$&Q;nZ~R{HSo2NJUaE}9fWdh8rcV@EAXuc4Do69KX35R z-L6hLU__quFjA}Xe7i-u_gycJfIT3xhZS)cAHW;+uepNQ8^8sF03d*C?-d2x+ea40 zRSbtSL&3k(&a&H7$S|soBrWhU86hJzKAN?) zk9wg~DzN*7aPo4(@z))M}r&PV~1Rd_x-t z2=9MP#k7;he|lucv|_n!ixCc9g-UnEXQc}IR#j&QG_7w6^0&(mlbc52lAqb~`)eI& zBc1Z1Od>V4BK($Kh18|d2Ce>t26(6)TinLFDQC3bU&Y3s>z)6Uu74F9e-#`5vtpz5 Y$k|Cl=$oB8PfyYA(!BLTGHM8a2cRO!4gdfE diff --git a/tests/security-resources/unknown-server-keystore.p12 b/tests/security-resources/unknown-server-keystore.p12 index 8fbec3e02642ac61dac2139225689a8062a0268f..2921764460b1d97afc1f10696bfc52c6f9c86d7d 100644 GIT binary patch delta 4599 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