/* * 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.client.impl; import javax.transaction.xa.XAException; import javax.transaction.xa.XAResource; import javax.transaction.xa.Xid; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration; 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.Message; import org.apache.activemq.artemis.api.core.SimpleString; import org.apache.activemq.artemis.api.core.client.ClientConsumer; import org.apache.activemq.artemis.api.core.client.ClientMessage; import org.apache.activemq.artemis.api.core.client.ClientProducer; import org.apache.activemq.artemis.api.core.client.ClientSessionFactory; import org.apache.activemq.artemis.api.core.client.FailoverEventListener; import org.apache.activemq.artemis.api.core.client.SendAcknowledgementHandler; import org.apache.activemq.artemis.api.core.client.SessionFailureListener; import org.apache.activemq.artemis.core.client.ActiveMQClientLogger; import org.apache.activemq.artemis.core.client.ActiveMQClientMessageBundle; import org.apache.activemq.artemis.core.remoting.FailureListener; import org.apache.activemq.artemis.api.core.RoutingType; import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection; import org.apache.activemq.artemis.spi.core.remoting.ConsumerContext; import org.apache.activemq.artemis.spi.core.remoting.ReadyListener; import org.apache.activemq.artemis.spi.core.remoting.SessionContext; import org.apache.activemq.artemis.utils.ConfirmationWindowWarning; import org.apache.activemq.artemis.utils.TokenBucketLimiterImpl; import org.apache.activemq.artemis.utils.UUIDGenerator; import org.apache.activemq.artemis.utils.XidCodecSupport; import org.jboss.logging.Logger; public final class ClientSessionImpl implements ClientSessionInternal, FailureListener { private static final Logger logger = Logger.getLogger(ClientSessionImpl.class); private final Map<String, String> metadata = new HashMap<>(); private final ClientSessionFactoryInternal sessionFactory; private String name; private final String username; private final String password; private final boolean xa; private final Executor executor; // to be sent to consumers as consumers will need a separate consumer for flow control private final Executor flowControlExecutor; /** * All access to producers are guarded (i.e. synchronized) on itself. */ private final Set<ClientProducerInternal> producers = new HashSet<>(); // Consumers must be an ordered map so if we fail we recreate them in the same order with the same ids private final Map<ConsumerContext, ClientConsumerInternal> consumers = new LinkedHashMap<>(); private volatile boolean closed; private final boolean autoCommitAcks; private final boolean preAcknowledge; private final boolean autoCommitSends; private final boolean blockOnAcknowledge; private final boolean autoGroup; private final int ackBatchSize; private final int consumerWindowSize; private final int consumerMaxRate; private final int confirmationWindowSize; private final int producerMaxRate; private final boolean blockOnNonDurableSend; private final boolean blockOnDurableSend; private final int minLargeMessageSize; private final boolean compressLargeMessages; private volatile int initialMessagePacketSize; private final boolean cacheLargeMessageClient; private final SessionContext sessionContext; // For testing only private boolean forceNotSameRM; private final ClientProducerCreditManager producerCreditManager; private volatile boolean started; private volatile boolean rollbackOnly; private volatile boolean workDone; private final String groupID; private volatile boolean inClose; private volatile boolean mayAttemptToFailover = true; /** * Current XID. this will be used in case of failover */ private Xid currentXID; private final AtomicInteger concurrentCall = new AtomicInteger(0); private final ConfirmationWindowWarning confirmationWindowWarning; private final Executor closeExecutor; ClientSessionImpl(final ClientSessionFactoryInternal sessionFactory, final String name, final String username, final String password, final boolean xa, final boolean autoCommitSends, final boolean autoCommitAcks, final boolean preAcknowledge, final boolean blockOnAcknowledge, final boolean autoGroup, final int ackBatchSize, final int consumerWindowSize, final int consumerMaxRate, final int confirmationWindowSize, final int producerWindowSize, final int producerMaxRate, final boolean blockOnNonDurableSend, final boolean blockOnDurableSend, final boolean cacheLargeMessageClient, final int minLargeMessageSize, final boolean compressLargeMessages, final int initialMessagePacketSize, final String groupID, final SessionContext sessionContext, final Executor executor, final Executor flowControlExecutor, final Executor closeExecutor) throws ActiveMQException { this.sessionFactory = sessionFactory; this.name = name; this.username = username; this.password = password; this.executor = executor; this.flowControlExecutor = flowControlExecutor; this.xa = xa; this.autoCommitAcks = autoCommitAcks; this.preAcknowledge = preAcknowledge; this.autoCommitSends = autoCommitSends; this.blockOnAcknowledge = blockOnAcknowledge; this.autoGroup = autoGroup; this.ackBatchSize = ackBatchSize; this.consumerWindowSize = consumerWindowSize; this.consumerMaxRate = consumerMaxRate; this.confirmationWindowSize = confirmationWindowSize; this.producerMaxRate = producerMaxRate; this.blockOnNonDurableSend = blockOnNonDurableSend; this.blockOnDurableSend = blockOnDurableSend; this.cacheLargeMessageClient = cacheLargeMessageClient; this.minLargeMessageSize = minLargeMessageSize; this.compressLargeMessages = compressLargeMessages; this.initialMessagePacketSize = initialMessagePacketSize; this.groupID = groupID; producerCreditManager = new ClientProducerCreditManagerImpl(this, producerWindowSize); this.sessionContext = sessionContext; sessionContext.setSession(this); confirmationWindowWarning = sessionFactory.getConfirmationWindowWarning(); this.closeExecutor = closeExecutor; } // ClientSession implementation // ----------------------------------------------------------------- @Override public void createQueue(final SimpleString address, final SimpleString queueName) throws ActiveMQException { createQueue(address, queueName, false); } @Override public void createQueue(final SimpleString address, final SimpleString queueName, final boolean durable) throws ActiveMQException { createQueue(address, queueName, null, durable, false); } @Override public void createQueue(final String address, final String queueName, final boolean durable) throws ActiveMQException { createQueue(SimpleString.toSimpleString(address), SimpleString.toSimpleString(queueName), durable); } @Override public void createSharedQueue(SimpleString address, SimpleString queueName, boolean durable) throws ActiveMQException { createSharedQueue(address, queueName, null, durable); } @Override public void createSharedQueue(SimpleString address, SimpleString queueName, SimpleString filterString, boolean durable) throws ActiveMQException { createSharedQueue(address, ActiveMQDefaultConfiguration.getDefaultRoutingType(), queueName, filterString, durable); } @Override public void createAddress(final SimpleString address, Set<RoutingType> routingTypes, boolean autoCreated) throws ActiveMQException { checkClosed(); startCall(); try { sessionContext.createAddress(address, routingTypes, autoCreated); } finally { endCall(); } } @Override public void createAddress(final SimpleString address, RoutingType routingType, boolean autoCreated) throws ActiveMQException { Set<RoutingType> routingTypes = new HashSet<>(); routingTypes.add(routingType); createAddress(address, routingTypes, autoCreated); } @Override public void createQueue(final SimpleString address, final SimpleString queueName, final SimpleString filterString, final boolean durable) throws ActiveMQException { createQueue(address, queueName, filterString, durable, false); } @Override public void createQueue(final String address, final String queueName, final String filterString, final boolean durable) throws ActiveMQException { createQueue(SimpleString.toSimpleString(address), SimpleString.toSimpleString(queueName), SimpleString.toSimpleString(filterString), durable); } @Override public void createQueue(final SimpleString address, final SimpleString queueName, final SimpleString filterString, final boolean durable, final boolean autoCreated) throws ActiveMQException { createQueue(address, ActiveMQDefaultConfiguration.getDefaultRoutingType(), queueName, filterString, durable, autoCreated); } @Override public void createQueue(final String address, final String queueName, final String filterString, final boolean durable, final boolean autoCreated) throws ActiveMQException { createQueue(SimpleString.toSimpleString(address), SimpleString.toSimpleString(queueName), SimpleString.toSimpleString(filterString), durable, autoCreated); } @Override public void createTemporaryQueue(final SimpleString address, final SimpleString queueName) throws ActiveMQException { createTemporaryQueue(address, queueName, null); } @Override public void createTemporaryQueue(final String address, final String queueName) throws ActiveMQException { createTemporaryQueue(SimpleString.toSimpleString(address), SimpleString.toSimpleString(queueName)); } @Override public void createTemporaryQueue(final SimpleString address, final SimpleString queueName, final SimpleString filter) throws ActiveMQException { createTemporaryQueue(address, ActiveMQDefaultConfiguration.getDefaultRoutingType(), queueName, filter); } @Override public void createTemporaryQueue(final String address, final String queueName, final String filter) throws ActiveMQException { createTemporaryQueue(SimpleString.toSimpleString(address), SimpleString.toSimpleString(queueName), SimpleString.toSimpleString(filter)); } /** New Queue API **/ @Override public void createQueue(final SimpleString address, final RoutingType routingType, final SimpleString queueName, final SimpleString filterString, final boolean durable, final boolean autoCreated) throws ActiveMQException { internalCreateQueue(address, queueName, routingType, filterString, durable, false, ActiveMQDefaultConfiguration.getDefaultMaxQueueConsumers(), ActiveMQDefaultConfiguration.getDefaultPurgeOnNoConsumers(), autoCreated); } @Override public void createQueue(final String address, final RoutingType routingType, final String queueName, final String filterString, final boolean durable, final boolean autoCreated) throws ActiveMQException { createQueue(SimpleString.toSimpleString(address), SimpleString.toSimpleString(queueName), SimpleString.toSimpleString(filterString), durable, autoCreated); } @Override public void createQueue(final SimpleString address, final RoutingType routingType, final SimpleString queueName, final SimpleString filterString, final boolean durable, final boolean autoCreated, final int maxConsumers, final boolean purgeOnNoConsumers) throws ActiveMQException { internalCreateQueue(address, queueName, routingType, filterString, durable, false, maxConsumers, purgeOnNoConsumers, autoCreated); } @Override public void createQueue(final String address, final RoutingType routingType, final String queueName, final String filterString, final boolean durable, final boolean autoCreated, final int maxConsumers, final boolean purgeOnNoConsumers) throws ActiveMQException { createQueue(SimpleString.toSimpleString(address), routingType, SimpleString.toSimpleString(queueName), SimpleString.toSimpleString(filterString), durable, autoCreated, maxConsumers, purgeOnNoConsumers); } @Override public void createTemporaryQueue(final SimpleString address, final RoutingType routingType, final SimpleString queueName) throws ActiveMQException { createTemporaryQueue(address, routingType, queueName, null); } @Override public void createTemporaryQueue(final String address, final RoutingType routingType, final String queueName) throws ActiveMQException { createTemporaryQueue(SimpleString.toSimpleString(address), routingType, SimpleString.toSimpleString(queueName)); } @Override public void createTemporaryQueue(final SimpleString address, final RoutingType routingType, final SimpleString queueName, final SimpleString filter) throws ActiveMQException { internalCreateQueue(address, queueName, routingType, filter, false, true, ActiveMQDefaultConfiguration.getDefaultMaxQueueConsumers(), ActiveMQDefaultConfiguration.getDefaultPurgeOnNoConsumers(), false); } @Override public void createTemporaryQueue(final String address, final RoutingType routingType, final String queueName, final String filter) throws ActiveMQException { createTemporaryQueue(SimpleString.toSimpleString(address), routingType, SimpleString.toSimpleString(queueName), SimpleString.toSimpleString(filter)); } /** * Creates a <em>non-temporary</em> queue. * * @param address the queue will be bound to this address * @param routingType the delivery mode for this queue, MULTICAST or ANYCAST * @param queueName the name of the queue * @param durable whether the queue is durable or not * @throws ActiveMQException in an exception occurs while creating the queue */ @Override public void createQueue(SimpleString address, RoutingType routingType, SimpleString queueName, boolean durable) throws ActiveMQException { internalCreateQueue(address, queueName, routingType, null, durable, false, ActiveMQDefaultConfiguration.getDefaultMaxQueueConsumers(), ActiveMQDefaultConfiguration.getDefaultPurgeOnNoConsumers(), false); } /** * Creates a transient queue. A queue that will exist as long as there are consumers. When the last consumer is closed the queue will be deleted * <p> * Notice: you will get an exception if the address or the filter doesn't match to an already existent queue * * @param address the queue will be bound to this address * @param routingType the delivery mode for this queue, MULTICAST or ANYCAST * @param queueName the name of the queue * @param durable if the queue is durable * @throws ActiveMQException in an exception occurs while creating the queue */ @Override public void createSharedQueue(SimpleString address, RoutingType routingType, SimpleString queueName, boolean durable) throws ActiveMQException { createSharedQueue(address, routingType, queueName, null, durable); } /** * Creates a transient queue. A queue that will exist as long as there are consumers. When the last consumer is closed the queue will be deleted * <p> * Notice: you will get an exception if the address or the filter doesn't match to an already existent queue * * @param address the queue will be bound to this address * @param routingType the delivery mode for this queue, MULTICAST or ANYCAST * @param queueName the name of the queue * @param filter whether the queue is durable or not * @param durable if the queue is durable * @throws ActiveMQException in an exception occurs while creating the queue */ @Override public void createSharedQueue(SimpleString address, RoutingType routingType, SimpleString queueName, SimpleString filter, boolean durable) throws ActiveMQException { checkClosed(); startCall(); try { sessionContext.createSharedQueue(address, queueName, routingType, filter, durable); } finally { endCall(); } } /** * Creates a <em>non-temporary</em> queue. * * @param address the queue will be bound to this address * @param routingType the delivery mode for this queue, MULTICAST or ANYCAST * @param queueName the name of the queue * @param durable whether the queue is durable or not * @throws ActiveMQException in an exception occurs while creating the queue */ @Override public void createQueue(String address, RoutingType routingType, String queueName, boolean durable) throws ActiveMQException { createQueue(SimpleString.toSimpleString(address), routingType, SimpleString.toSimpleString(queueName), durable); } /** * Creates a <em>non-temporary</em> queue <em>non-durable</em> queue. * * @param address the queue will be bound to this address * @param routingType the delivery mode for this queue, MULTICAST or ANYCAST * @param queueName the name of the queue * @throws ActiveMQException in an exception occurs while creating the queue */ @Override public void createQueue(String address, RoutingType routingType, String queueName) throws ActiveMQException { internalCreateQueue(SimpleString.toSimpleString(address), SimpleString.toSimpleString(queueName), routingType, null, false, true, ActiveMQDefaultConfiguration.getDefaultMaxQueueConsumers(), ActiveMQDefaultConfiguration.getDefaultPurgeOnNoConsumers(), false); } /** * Creates a <em>non-temporary</em> queue <em>non-durable</em> queue. * * @param address the queue will be bound to this address * @param routingType the delivery mode for this queue, MULTICAST or ANYCAST * @param queueName the name of the queue * @throws ActiveMQException in an exception occurs while creating the queue */ @Override public void createQueue(SimpleString address, RoutingType routingType, SimpleString queueName) throws ActiveMQException { internalCreateQueue(address, queueName, routingType, null, false, false, ActiveMQDefaultConfiguration.getDefaultMaxQueueConsumers(), ActiveMQDefaultConfiguration.getDefaultPurgeOnNoConsumers(), false); } /** * Creates a <em>non-temporary</em> queue. * * @param address the queue will be bound to this address * @param routingType the delivery mode for this queue, MULTICAST or ANYCAST * @param queueName the name of the queue * @param filter only messages which match this filter will be put in the queue * @param durable whether the queue is durable or not * @throws ActiveMQException in an exception occurs while creating the queue */ @Override public void createQueue(SimpleString address, RoutingType routingType, SimpleString queueName, SimpleString filter, boolean durable) throws ActiveMQException { internalCreateQueue(address, queueName, routingType, filter, durable, false, ActiveMQDefaultConfiguration.getDefaultMaxQueueConsumers(), ActiveMQDefaultConfiguration.getDefaultPurgeOnNoConsumers(), false); } /** * Creates a <em>non-temporary</em>queue. * * @param address the queue will be bound to this address * @param routingType the delivery mode for this queue, MULTICAST or ANYCAST * @param queueName the name of the queue * @param filter only messages which match this filter will be put in the queue * @param durable whether the queue is durable or not * @throws ActiveMQException in an exception occurs while creating the queue */ @Override public void createQueue(String address, RoutingType routingType, String queueName, String filter, boolean durable) throws ActiveMQException { createQueue(SimpleString.toSimpleString(address), routingType, SimpleString.toSimpleString(queueName), SimpleString.toSimpleString(filter), durable); } @Override public void deleteQueue(final SimpleString queueName) throws ActiveMQException { checkClosed(); startCall(); try { sessionContext.deleteQueue(queueName); } finally { endCall(); } } @Override public void deleteQueue(final String queueName) throws ActiveMQException { deleteQueue(SimpleString.toSimpleString(queueName)); } @Override public QueueQuery queueQuery(final SimpleString queueName) throws ActiveMQException { checkClosed(); startCall(); try { return sessionContext.queueQuery(queueName); } finally { endCall(); } } @Override public AddressQuery addressQuery(final SimpleString address) throws ActiveMQException { checkClosed(); return sessionContext.addressQuery(address); } @Override public ClientConsumer createConsumer(final SimpleString queueName) throws ActiveMQException { return createConsumer(queueName, null, false); } @Override public ClientConsumer createConsumer(final String queueName) throws ActiveMQException { return createConsumer(SimpleString.toSimpleString(queueName)); } @Override public ClientConsumer createConsumer(final SimpleString queueName, final SimpleString filterString) throws ActiveMQException { return createConsumer(queueName, filterString, consumerWindowSize, consumerMaxRate, false); } @Override public void createQueue(final String address, final String queueName) throws ActiveMQException { createQueue(SimpleString.toSimpleString(address), SimpleString.toSimpleString(queueName)); } @Override public ClientConsumer createConsumer(final String queueName, final String filterString) throws ActiveMQException { return createConsumer(SimpleString.toSimpleString(queueName), SimpleString.toSimpleString(filterString)); } @Override public ClientConsumer createConsumer(final SimpleString queueName, final SimpleString filterString, final boolean browseOnly) throws ActiveMQException { return createConsumer(queueName, filterString, consumerWindowSize, consumerMaxRate, browseOnly); } @Override public ClientConsumer createConsumer(final SimpleString queueName, final boolean browseOnly) throws ActiveMQException { return createConsumer(queueName, null, consumerWindowSize, consumerMaxRate, browseOnly); } @Override public ClientConsumer createConsumer(final String queueName, final String filterString, final boolean browseOnly) throws ActiveMQException { return createConsumer(SimpleString.toSimpleString(queueName), SimpleString.toSimpleString(filterString), browseOnly); } @Override public ClientConsumer createConsumer(final String queueName, final boolean browseOnly) throws ActiveMQException { return createConsumer(SimpleString.toSimpleString(queueName), null, browseOnly); } @Override public boolean isWritable(ReadyListener callback) { return sessionContext.isWritable(callback); } /** * Note, we DO NOT currently support direct consumers (i.e. consumers where delivery occurs on * the remoting thread). * <p> * Direct consumers have issues with blocking and failover. E.g. if direct then inside * MessageHandler call a blocking method like rollback or acknowledge (blocking) This can block * until failover completes, which disallows the thread to be used to deliver any responses to * the client during that period, so failover won't occur. If we want direct consumers we need to * rethink how they work. */ @Override public ClientConsumer createConsumer(final SimpleString queueName, final SimpleString filterString, final int windowSize, final int maxRate, final boolean browseOnly) throws ActiveMQException { return internalCreateConsumer(queueName, filterString, windowSize, maxRate, browseOnly); } @Override public ClientConsumer createConsumer(final String queueName, final String filterString, final int windowSize, final int maxRate, final boolean browseOnly) throws ActiveMQException { return createConsumer(SimpleString.toSimpleString(queueName), SimpleString.toSimpleString(filterString), windowSize, maxRate, browseOnly); } @Override public ClientProducer createProducer() throws ActiveMQException { return createProducer((SimpleString) null); } @Override public ClientProducer createProducer(final SimpleString address) throws ActiveMQException { return createProducer(address, producerMaxRate); } @Override public ClientProducer createProducer(final String address) throws ActiveMQException { return createProducer(SimpleString.toSimpleString(address)); } @Override public ClientProducer createProducer(final SimpleString address, final int maxRate) throws ActiveMQException { return internalCreateProducer(address, maxRate); } public ClientProducer createProducer(final String address, final int rate) throws ActiveMQException { return createProducer(SimpleString.toSimpleString(address), rate); } @Override public XAResource getXAResource() { return this; } private void rollbackOnFailover(boolean outcomeKnown) throws ActiveMQException { rollback(false); if (outcomeKnown) { throw ActiveMQClientMessageBundle.BUNDLE.txRolledBack(); } throw ActiveMQClientMessageBundle.BUNDLE.txOutcomeUnknown(); } @Override public void commit() throws ActiveMQException { checkClosed(); if (logger.isTraceEnabled()) { logger.trace("Sending commit"); } /* * we have failed over since any work was done so we should rollback * */ if (rollbackOnly) { rollbackOnFailover(true); } flushAcks(); /* * if we have failed over whilst flushing the acks then we should rollback and throw exception before attempting to * commit as committing might actually commit something but we we wouldn't know and rollback after the commit * */ if (rollbackOnly) { rollbackOnFailover(true); } try { sessionContext.simpleCommit(); } catch (ActiveMQException e) { if (e.getType() == ActiveMQExceptionType.UNBLOCKED || rollbackOnly) { // The call to commit was unlocked on failover, we therefore rollback the tx, // and throw a transaction rolled back exception instead //or //if we have been set to rollbackonly then we have probably failed over and don't know if the tx has committed rollbackOnFailover(false); } else { throw e; } } //oops, we have failed over during the commit and don't know what happened if (rollbackOnly) { rollbackOnFailover(false); } workDone = false; } @Override public boolean isRollbackOnly() { return rollbackOnly; } @Override public void rollback() throws ActiveMQException { rollback(false); } @Override public void rollback(final boolean isLastMessageAsDelivered) throws ActiveMQException { rollback(isLastMessageAsDelivered, true); } public void rollback(final boolean isLastMessageAsDelivered, final boolean waitConsumers) throws ActiveMQException { if (logger.isTraceEnabled()) { logger.trace("calling rollback(isLastMessageAsDelivered=" + isLastMessageAsDelivered + ")"); } checkClosed(); // We do a "JMS style" rollback where the session is stopped, and the buffer is cancelled back // first before rolling back // This ensures messages are received in the same order after rollback w.r.t. to messages in the buffer // For core we could just do a straight rollback, it really depends if we want JMS style semantics or not... boolean wasStarted = started; if (wasStarted) { stop(); } // We need to make sure we don't get any inflight messages for (ClientConsumerInternal consumer : cloneConsumers()) { consumer.clear(waitConsumers); } // Acks must be flushed here *after connection is stopped and all onmessages finished executing flushAcks(); sessionContext.simpleRollback(isLastMessageAsDelivered); if (wasStarted) { start(); } rollbackOnly = false; } @Override public void markRollbackOnly() { rollbackOnly = true; } @Override public ClientMessage createMessage(final byte type, final boolean durable, final long expiration, final long timestamp, final byte priority) { return new ClientMessageImpl(type, durable, expiration, timestamp, priority, initialMessagePacketSize); } @Override public ClientMessage createMessage(final byte type, final boolean durable) { return this.createMessage(type, durable, 0, System.currentTimeMillis(), (byte) 4); } @Override public ClientMessage createMessage(final boolean durable) { return this.createMessage((byte) 0, durable); } @Override public boolean isClosed() { return closed; } @Override public boolean isAutoCommitSends() { return autoCommitSends; } @Override public boolean isAutoCommitAcks() { return autoCommitAcks; } @Override public boolean isBlockOnAcknowledge() { return blockOnAcknowledge; } @Override public boolean isXA() { return xa; } @Override public void resetIfNeeded() throws ActiveMQException { if (rollbackOnly) { ActiveMQClientLogger.LOGGER.resettingSessionAfterFailure(); rollback(false); } } @Override public ClientSessionImpl start() throws ActiveMQException { checkClosed(); if (!started) { for (ClientConsumerInternal clientConsumerInternal : cloneConsumers()) { clientConsumerInternal.start(); } sessionContext.sessionStart(); started = true; } return this; } @Override public void stop() throws ActiveMQException { stop(true); } public void stop(final boolean waitForOnMessage) throws ActiveMQException { checkClosed(); if (started) { for (ClientConsumerInternal clientConsumerInternal : cloneConsumers()) { clientConsumerInternal.stop(waitForOnMessage); } sessionContext.sessionStop(); started = false; } } @Override public void addFailureListener(final SessionFailureListener listener) { sessionFactory.addFailureListener(listener); } @Override public boolean removeFailureListener(final SessionFailureListener listener) { return sessionFactory.removeFailureListener(listener); } @Override public void addFailoverListener(FailoverEventListener listener) { sessionFactory.addFailoverListener(listener); } @Override public boolean removeFailoverListener(FailoverEventListener listener) { return sessionFactory.removeFailoverListener(listener); } @Override public int getVersion() { return sessionContext.getServerVersion(); } @Override public boolean isClosing() { return inClose; } @Override public String getNodeId() { return sessionFactory.getLiveNodeId(); } // ClientSessionInternal implementation // ------------------------------------------------------------ @Override public int getMinLargeMessageSize() { return minLargeMessageSize; } @Override public boolean isCompressLargeMessages() { return compressLargeMessages; } /** * @return the cacheLargeMessageClient */ @Override public boolean isCacheLargeMessageClient() { return cacheLargeMessageClient; } @Override public String getName() { return name; } /** * Acknowledges all messages received by the consumer so far. */ @Override public void acknowledge(final ClientConsumer consumer, final Message message) throws ActiveMQException { // if we're pre-acknowledging then we don't need to do anything if (preAcknowledge) { return; } checkClosed(); if (logger.isDebugEnabled()) { logger.debug("client ack messageID = " + message.getMessageID()); } startCall(); try { sessionContext.sendACK(false, blockOnAcknowledge, consumer, message); } finally { endCall(); } } @Override public void individualAcknowledge(final ClientConsumer consumer, final Message message) throws ActiveMQException { // if we're pre-acknowledging then we don't need to do anything if (preAcknowledge) { return; } checkClosed(); startCall(); try { sessionContext.sendACK(true, blockOnAcknowledge, consumer, message); } finally { endCall(); } } @Override public void expire(final ClientConsumer consumer, final Message message) throws ActiveMQException { checkClosed(); // We don't send expiries for pre-ack since message will already have been acked on server if (!preAcknowledge) { sessionContext.expireMessage(consumer, message); } } @Override public void addConsumer(final ClientConsumerInternal consumer) { synchronized (consumers) { consumers.put(consumer.getConsumerContext(), consumer); } } @Override public void addProducer(final ClientProducerInternal producer) { synchronized (producers) { producers.add(producer); } } @Override public void removeConsumer(final ClientConsumerInternal consumer) throws ActiveMQException { synchronized (consumers) { consumers.remove(consumer.getConsumerContext()); } } @Override public void removeProducer(final ClientProducerInternal producer) { synchronized (producers) { producers.remove(producer); } } @Override public void handleReceiveMessage(final ConsumerContext consumerID, final ClientMessageInternal message) throws Exception { ClientConsumerInternal consumer = getConsumer(consumerID); if (consumer != null) { consumer.handleMessage(message); } } @Override public void handleReceiveLargeMessage(final ConsumerContext consumerID, ClientLargeMessageInternal clientLargeMessage, long largeMessageSize) throws Exception { ClientConsumerInternal consumer = getConsumer(consumerID); if (consumer != null) { consumer.handleLargeMessage(clientLargeMessage, largeMessageSize); } } @Override public void handleReceiveContinuation(final ConsumerContext consumerID, byte[] chunk, int flowControlSize, boolean isContinues) throws Exception { ClientConsumerInternal consumer = getConsumer(consumerID); if (consumer != null) { consumer.handleLargeMessageContinuation(chunk, flowControlSize, isContinues); } } @Override public void handleConsumerDisconnect(ConsumerContext context) throws ActiveMQException { final ClientConsumerInternal consumer = getConsumer(context); if (consumer != null) { closeExecutor.execute(new Runnable() { @Override public void run() { try { consumer.close(); } catch (ActiveMQException e) { ActiveMQClientLogger.LOGGER.unableToCloseConsumer(e); } } }); } } @Override public void close() throws ActiveMQException { if (closed) { logger.debug("Session was already closed, giving up now, this=" + this); return; } if (logger.isDebugEnabled()) { logger.debug("Calling close on session " + this); } try { closeChildren(); synchronized (this) { producerCreditManager.close(); } inClose = true; sessionContext.sessionClose(); } catch (Throwable e) { // Session close should always return without exception // Note - we only log at trace logger.trace("Failed to close session", e); } doCleanup(false); } @Override public synchronized void cleanUp(boolean failingOver) throws ActiveMQException { if (closed) { return; } producerCreditManager.close(); cleanUpChildren(); doCleanup(failingOver); } @Override public ClientSessionImpl setSendAcknowledgementHandler(final SendAcknowledgementHandler handler) { sessionContext.setSendAcknowledgementHandler(handler); return this; } @Override public void preHandleFailover(RemotingConnection connection) { // We lock the channel to prevent any packets to be added to the re-send // cache during the failover process //we also do this before the connection fails over to give the session a chance to block for failover sessionContext.lockCommunications(); } // Needs to be synchronized to prevent issues with occurring concurrently with close() @Override public void handleFailover(final RemotingConnection backupConnection, ActiveMQException cause) { synchronized (this) { if (closed) { return; } boolean resetCreditManager = false; try { // TODO remove this and encapsulate it boolean reattached = sessionContext.reattachOnNewConnection(backupConnection); if (!reattached) { // We change the name of the Session, otherwise the server could close it while we are still sending the recreate // in certain failure scenarios // For instance the fact we didn't change the name of the session after failover or reconnect // was the reason allowing multiple Sessions to be closed simultaneously breaking concurrency this.name = UUIDGenerator.getInstance().generateStringUUID(); sessionContext.resetName(name); for (ClientConsumerInternal consumer : cloneConsumers()) { consumer.clearAtFailover(); } // The session wasn't found on the server - probably we're failing over onto a backup server where the // session won't exist or the target server has been restarted - in this case the session will need to be // recreated, // and we'll need to recreate any consumers // It could also be that the server hasn't been restarted, but the session is currently executing close, // and // that // has already been executed on the server, that's why we can't find the session- in this case we *don't* // want // to recreate the session, we just want to unblock the blocking call if (!inClose && mayAttemptToFailover) { sessionContext.recreateSession(username, password, minLargeMessageSize, xa, autoCommitSends, autoCommitAcks, preAcknowledge); for (Map.Entry<ConsumerContext, ClientConsumerInternal> entryx : consumers.entrySet()) { ClientConsumerInternal consumerInternal = entryx.getValue(); sessionContext.recreateConsumerOnServer(consumerInternal); } if ((!autoCommitAcks || !autoCommitSends) && workDone) { // this is protected by a lock, so we can guarantee nothing will sneak here // while we do our work here rollbackOnly = true; } if (currentXID != null) { sessionContext.xaFailed(currentXID); rollbackOnly = true; } // Now start the session if it was already started if (started) { for (ClientConsumerInternal consumer : cloneConsumers()) { consumer.clearAtFailover(); consumer.start(); } sessionContext.restartSession(); } resetCreditManager = true; } sessionContext.returnBlocking(cause); } } catch (Throwable t) { ActiveMQClientLogger.LOGGER.failedToHandleFailover(t); } finally { sessionContext.releaseCommunications(); } if (resetCreditManager) { 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<String, String> metaDataToSend; synchronized (metadata) { metaDataToSend = new HashMap<>(metadata); } sessionContext.resetMetadata(metaDataToSend); } @Override public void addMetaData(String key, String data) throws ActiveMQException { synchronized (metadata) { metadata.put(key, data); } sessionContext.addSessionMetadata(key, data); } @Override public void addUniqueMetaData(String key, String data) throws ActiveMQException { sessionContext.addUniqueMetaData(key, data); } @Override public ClientSessionFactory getSessionFactory() { return sessionFactory; } @Override public void setAddress(final Message message, final SimpleString address) { logger.tracef("setAddress() Setting default address as %s", address); message.setAddress(address); } @Override public void setPacketSize(final int packetSize) { if (packetSize > this.initialMessagePacketSize) { this.initialMessagePacketSize = (int) (packetSize * 1.2); } } @Override public void workDone() { workDone = true; } @Override public void sendProducerCreditsMessage(final int credits, final SimpleString address) { sessionContext.sendProducerCreditsMessage(credits, address); } @Override public synchronized ClientProducerCredits getCredits(final SimpleString address, final boolean anon) { ClientProducerCredits credits = producerCreditManager.getCredits(address, anon, sessionContext); return credits; } @Override public void returnCredits(final SimpleString address) { producerCreditManager.returnCredits(address); } @Override public void handleReceiveProducerCredits(final SimpleString address, final int credits) { producerCreditManager.receiveCredits(address, credits); } @Override public void handleReceiveProducerFailCredits(final SimpleString address, int credits) { producerCreditManager.receiveFailCredits(address, credits); } @Override public ClientProducerCreditManager getProducerCreditManager() { return producerCreditManager; } @Override public void startCall() { if (concurrentCall.incrementAndGet() > 1) { ActiveMQClientLogger.LOGGER.invalidConcurrentSessionUsage(new Exception("trace")); } } @Override public void endCall() { concurrentCall.decrementAndGet(); } // CommandConfirmationHandler implementation ------------------------------------ // TODO: this will be encapsulated by the SessionContext // XAResource implementation // -------------------------------------------------------------------- @Override public void commit(final Xid xid, final boolean onePhase) throws XAException { if (logger.isTraceEnabled()) { logger.trace("call commit(xid=" + convert(xid)); } checkXA(); // we should never throw rollback if we have already prepared if (rollbackOnly) { if (onePhase) { throw new XAException(XAException.XAER_RMFAIL); } else { ActiveMQClientLogger.LOGGER.commitAfterFailover(); } } // Note - don't need to flush acks since the previous end would have // done this startCall(); try { sessionContext.xaCommit(xid, onePhase); workDone = false; } catch (XAException xae) { throw xae; } catch (Throwable t) { ActiveMQClientLogger.LOGGER.failoverDuringCommit(); XAException xaException = null; if (onePhase) { //we must return XA_RMFAIL xaException = new XAException(XAException.XAER_RMFAIL); } else { // Any error on commit -> RETRY // We can't rollback a Prepared TX for definition xaException = new XAException(XAException.XA_RETRY); } xaException.initCause(t); throw xaException; } finally { endCall(); } } @Override public void end(final Xid xid, final int flags) throws XAException { if (logger.isTraceEnabled()) { logger.trace("Calling end:: " + convert(xid) + ", flags=" + convertTXFlag(flags)); } checkXA(); try { if (rollbackOnly) { try { rollback(false, false); } catch (Throwable ignored) { logger.debug("Error on rollback during end call!", ignored); } throw new XAException(XAException.XAER_RMFAIL); } try { flushAcks(); startCall(); try { sessionContext.xaEnd(xid, flags); } finally { endCall(); } } catch (XAException xae) { throw xae; } catch (Throwable t) { ActiveMQClientLogger.LOGGER.errorCallingEnd(t); // This could occur if the TM interrupts the thread XAException xaException = new XAException(XAException.XAER_RMFAIL); xaException.initCause(t); throw xaException; } } finally { currentXID = null; } } @Override public void forget(final Xid xid) throws XAException { checkXA(); startCall(); try { sessionContext.xaForget(xid); } catch (XAException xae) { throw xae; } catch (Throwable t) { // This could occur if the TM interrupts the thread XAException xaException = new XAException(XAException.XAER_RMFAIL); xaException.initCause(t); throw xaException; } finally { endCall(); } } @Override public int getTransactionTimeout() throws XAException { checkXA(); try { return sessionContext.recoverSessionTimeout(); } catch (Throwable t) { // This could occur if the TM interrupts the thread XAException xaException = new XAException(XAException.XAER_RMFAIL); xaException.initCause(t); throw xaException; } } @Override public boolean setTransactionTimeout(final int seconds) throws XAException { checkXA(); try { return sessionContext.configureTransactionTimeout(seconds); } catch (Throwable t) { markRollbackOnly(); // The TM will ignore any errors from here, if things are this screwed up we mark rollbackonly // This could occur if the TM interrupts the thread XAException xaException = new XAException(XAException.XAER_RMFAIL); xaException.initCause(t); throw xaException; } } @Override public boolean isSameRM(final XAResource xares) throws XAException { checkXA(); if (forceNotSameRM) { return false; } ClientSessionInternal other = getSessionInternalFromXAResource(xares); if (other == null) { return false; } String liveNodeId = sessionFactory.getLiveNodeId(); String otherLiveNodeId = ((ClientSessionFactoryInternal) other.getSessionFactory()).getLiveNodeId(); if (liveNodeId != null && otherLiveNodeId != null) { return liveNodeId.equals(otherLiveNodeId); } //we shouldn't get here, live node id should always be set return sessionFactory == other.getSessionFactory(); } private ClientSessionInternal getSessionInternalFromXAResource(final XAResource xares) { if (xares == null) { return null; } if (xares instanceof ClientSessionInternal) { return (ClientSessionInternal) xares; } else if (xares instanceof ActiveMQXAResource) { return getSessionInternalFromXAResource(((ActiveMQXAResource) xares).getResource()); } return null; } @Override public int prepare(final Xid xid) throws XAException { checkXA(); if (logger.isTraceEnabled()) { logger.trace("Calling prepare:: " + convert(xid)); } if (rollbackOnly) { throw new XAException(XAException.XAER_RMFAIL); } // Note - don't need to flush acks since the previous end would have // done this startCall(); try { return sessionContext.xaPrepare(xid); } catch (XAException xae) { throw xae; } catch (ActiveMQException e) { if (e.getType() == ActiveMQExceptionType.UNBLOCKED) { // Unblocked on failover try { // will retry once after failover & unblock return sessionContext.xaPrepare(xid); } catch (Throwable t) { // ignore and rollback } ActiveMQClientLogger.LOGGER.failoverDuringPrepareRollingBack(); try { rollback(false); } catch (Throwable t) { // This could occur if the TM interrupts the thread XAException xaException = new XAException(XAException.XAER_RMFAIL); xaException.initCause(t); throw xaException; } ActiveMQClientLogger.LOGGER.errorDuringPrepare(e); throw new XAException(XAException.XAER_RMFAIL); } ActiveMQClientLogger.LOGGER.errorDuringPrepare(e); // This should never occur XAException xaException = new XAException(XAException.XAER_RMFAIL); xaException.initCause(e); throw xaException; } catch (Throwable t) { ActiveMQClientLogger.LOGGER.errorDuringPrepare(t); // This could occur if the TM interrupts the thread XAException xaException = new XAException(XAException.XAER_RMFAIL); xaException.initCause(t); throw xaException; } finally { endCall(); } } @Override public Xid[] recover(final int flags) throws XAException { checkXA(); if ((flags & XAResource.TMSTARTRSCAN) == XAResource.TMSTARTRSCAN) { try { return sessionContext.xaScan(); } catch (Throwable t) { // This could occur if the TM interrupts the thread XAException xaException = new XAException(XAException.XAER_RMFAIL); xaException.initCause(t); throw xaException; } } return new Xid[0]; } @Override public void rollback(final Xid xid) throws XAException { checkXA(); if (logger.isTraceEnabled()) { logger.trace("Calling rollback:: " + convert(xid)); } try { boolean wasStarted = started; if (wasStarted) { stop(false); } // We need to make sure we don't get any inflight messages for (ClientConsumerInternal consumer : cloneConsumers()) { consumer.clear(false); } flushAcks(); try { sessionContext.xaRollback(xid, wasStarted); } finally { if (wasStarted) { start(); } } workDone = false; } catch (XAException xae) { throw xae; } catch (ActiveMQException e) { if (e.getType() == ActiveMQExceptionType.UNBLOCKED) { // Unblocked on failover throw new XAException(XAException.XA_RETRY); } // This should never occur XAException xaException = new XAException(XAException.XAER_RMFAIL); xaException.initCause(e); throw xaException; } catch (Throwable t) { // This could occur if the TM interrupts the thread XAException xaException = new XAException(XAException.XAER_RMFAIL); xaException.initCause(t); throw xaException; } } @Override public void start(final Xid xid, final int flags) throws XAException { if (logger.isTraceEnabled()) { logger.trace("Calling start:: " + convert(xid) + " clientXID=" + xid + " flags = " + convertTXFlag(flags)); } checkXA(); try { sessionContext.xaStart(xid, flags); this.currentXID = xid; } catch (XAException xae) { throw xae; } catch (ActiveMQException e) { // we can retry this only because we know for sure that no work would have been done if (e.getType() == ActiveMQExceptionType.UNBLOCKED) { try { sessionContext.xaStart(xid, flags); } catch (XAException xae) { throw xae; } catch (Throwable t) { // This could occur if the TM interrupts the thread XAException xaException = new XAException(XAException.XAER_RMFAIL); xaException.initCause(t); throw xaException; } } // This should never occur XAException xaException = new XAException(XAException.XAER_RMFAIL); xaException.initCause(e); throw xaException; } catch (Throwable t) { // This could occur if the TM interrupts the thread XAException xaException = new XAException(XAException.XAER_RMFAIL); xaException.initCause(t); throw xaException; } } // FailureListener implementation -------------------------------------------- @Override public void connectionFailed(final ActiveMQException me, boolean failedOver) { try { cleanUp(false); } catch (Exception e) { ActiveMQClientLogger.LOGGER.failedToCleanupSession(e); } } @Override public void connectionFailed(final ActiveMQException me, boolean failedOver, String scaleDownTargetNodeID) { connectionFailed(me, failedOver); } // Public // ---------------------------------------------------------------------------- @Override public void setForceNotSameRM(final boolean force) { forceNotSameRM = force; } @Override public RemotingConnection getConnection() { return sessionContext.getRemotingConnection(); } @Override public String toString() { StringBuilder buffer = new StringBuilder(); synchronized (metadata) { for (Map.Entry<String, String> entry : metadata.entrySet()) { buffer.append(entry.getKey() + "=" + entry.getValue() + ","); } } return "ClientSessionImpl [name=" + name + ", username=" + username + ", closed=" + closed + ", factory = " + this.sessionFactory + ", metaData=(" + buffer + ")]@" + Integer.toHexString(hashCode()); } /** * @param queueName * @param filterString * @param windowSize * @param browseOnly * @return * @throws ActiveMQException */ private ClientConsumer internalCreateConsumer(final SimpleString queueName, final SimpleString filterString, final int windowSize, final int maxRate, final boolean browseOnly) throws ActiveMQException { checkClosed(); ClientConsumerInternal consumer = sessionContext.createConsumer(queueName, filterString, windowSize, maxRate, ackBatchSize, browseOnly, executor, flowControlExecutor); addConsumer(consumer); // Now we send window size credits to start the consumption // We even send it if windowSize == -1, since we need to start the // consumer // TODO: this could semantically change on other servers. I know for instance on stomp this is just an ignore if (windowSize != 0) { sessionContext.sendConsumerCredits(consumer, windowSize); } return consumer; } private ClientProducer internalCreateProducer(final SimpleString address, final int maxRate) throws ActiveMQException { checkClosed(); ClientProducerInternal producer = new ClientProducerImpl(this, address, maxRate == -1 ? null : new TokenBucketLimiterImpl(maxRate, false), autoCommitSends && blockOnNonDurableSend, autoCommitSends && blockOnDurableSend, autoGroup, groupID == null ? null : new SimpleString(groupID), minLargeMessageSize, sessionContext); addProducer(producer); return producer; } private void internalCreateQueue(final SimpleString address, final SimpleString queueName, final RoutingType routingType, final SimpleString filterString, final boolean durable, final boolean temp, final int maxConsumers, final boolean purgeOnNoConsumers, final boolean autoCreated) throws ActiveMQException { checkClosed(); if (durable && temp) { throw ActiveMQClientMessageBundle.BUNDLE.queueMisConfigured(); } startCall(); try { sessionContext.createQueue(address, routingType, queueName, filterString, durable, temp, maxConsumers, purgeOnNoConsumers, autoCreated); } finally { endCall(); } } private void checkXA() throws XAException { if (!xa) { ActiveMQClientLogger.LOGGER.sessionNotXA(); throw new XAException(XAException.XAER_RMFAIL); } } private void checkClosed() throws ActiveMQException { if (closed || inClose) { throw ActiveMQClientMessageBundle.BUNDLE.sessionClosed(); } } private ClientConsumerInternal getConsumer(final ConsumerContext consumerContext) { synchronized (consumers) { ClientConsumerInternal consumer = consumers.get(consumerContext); return consumer; } } private void doCleanup(boolean failingOver) { if (logger.isDebugEnabled()) { logger.debug("calling cleanup on " + this); } synchronized (this) { closed = true; sessionContext.cleanup(); } sessionFactory.removeSession(this, failingOver); } private void cleanUpChildren() throws ActiveMQException { Set<ClientConsumerInternal> consumersClone = cloneConsumers(); for (ClientConsumerInternal consumer : consumersClone) { consumer.cleanUp(); } Set<ClientProducerInternal> producersClone = cloneProducers(); for (ClientProducerInternal producer : producersClone) { producer.cleanUp(); } } /** * Not part of the interface, used on tests only * * @return */ public Set<ClientProducerInternal> cloneProducers() { Set<ClientProducerInternal> producersClone; synchronized (producers) { producersClone = new HashSet<>(producers); } return producersClone; } /** * Not part of the interface, used on tests only * * @return */ public Set<ClientConsumerInternal> cloneConsumers() { synchronized (consumers) { return new HashSet<>(consumers.values()); } } private void closeChildren() throws ActiveMQException { Set<ClientConsumerInternal> consumersClone = cloneConsumers(); for (ClientConsumer consumer : consumersClone) { consumer.close(); } Set<ClientProducerInternal> producersClone = cloneProducers(); for (ClientProducer producer : producersClone) { producer.close(); } } private void flushAcks() throws ActiveMQException { for (ClientConsumerInternal consumer : cloneConsumers()) { consumer.flushAcks(); } } /** * If you ever tried to debug XIDs you will know what this is about. * This will serialize and deserialize the XID to the same way it's going to be printed on server logs * or print-data. * <p> * This will convert to the same XID deserialized on the Server, hence we will be able to debug eventual stuff * * @param xid * @return */ public static Object convert(Xid xid) { ActiveMQBuffer buffer = ActiveMQBuffers.dynamicBuffer(200); XidCodecSupport.encodeXid(xid, buffer); Object obj = XidCodecSupport.decodeXid(buffer); return "xid=" + obj + ",clientXID=" + xid; } private String convertTXFlag(final int flags) { if (flags == XAResource.TMSUSPEND) { return "SESS_XA_SUSPEND"; } else if (flags == XAResource.TMSUCCESS) { return "TMSUCCESS"; } else if (flags == XAResource.TMFAIL) { return "TMFAIL"; } else if (flags == XAResource.TMJOIN) { return "TMJOIN"; } else if (flags == XAResource.TMRESUME) { return "TMRESUME"; } else if (flags == XAResource.TMNOFLAGS) { // Don't need to flush since the previous end will have done this return "TMNOFLAGS"; } else { return "XAER_INVAL(" + flags + ")"; } } @Override public void setStopSignal() { mayAttemptToFailover = false; } @Override public boolean isConfirmationWindowEnabled() { if (confirmationWindowWarning.disabled) { if (!confirmationWindowWarning.warningIssued.get()) { ActiveMQClientLogger.LOGGER.confirmationWindowDisabledWarning(); confirmationWindowWarning.warningIssued.set(true); } return false; } return true; } @Override public void scheduleConfirmation(final SendAcknowledgementHandler handler, final Message message) { executor.execute(new Runnable() { @Override public void run() { handler.sendAcknowledged(message); } }); } @Override public SessionContext getSessionContext() { return sessionContext; } }