com.datastax.oss.pulsar.jms.PulsarConnectionFactory Maven / Gradle / Ivy
/*
* Copyright DataStax, Inc.
*
* Licensed 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 com.datastax.oss.pulsar.jms;
import static com.datastax.oss.pulsar.jms.Utils.getAndRemoveString;
import static org.apache.pulsar.client.util.MathUtils.signSafeMod;
import com.datastax.oss.pulsar.jms.api.JMSAdmin;
import com.fasterxml.jackson.databind.ObjectMapper;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.jms.ConnectionFactory;
import jakarta.jms.Destination;
import jakarta.jms.IllegalStateException;
import jakarta.jms.InvalidClientIDException;
import jakarta.jms.InvalidDestinationException;
import jakarta.jms.JMSContext;
import jakarta.jms.JMSException;
import jakarta.jms.JMSRuntimeException;
import jakarta.jms.JMSSecurityException;
import jakarta.jms.JMSSecurityRuntimeException;
import jakarta.jms.Queue;
import jakarta.jms.QueueConnection;
import jakarta.jms.QueueConnectionFactory;
import jakarta.jms.Topic;
import jakarta.jms.TopicConnection;
import jakarta.jms.TopicConnectionFactory;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
import lombok.extern.slf4j.Slf4j;
import org.apache.pulsar.client.admin.PulsarAdmin;
import org.apache.pulsar.client.admin.PulsarAdminException;
import org.apache.pulsar.client.api.Authentication;
import org.apache.pulsar.client.api.AuthenticationFactory;
import org.apache.pulsar.client.api.BatcherBuilder;
import org.apache.pulsar.client.api.ClientBuilder;
import org.apache.pulsar.client.api.Consumer;
import org.apache.pulsar.client.api.ConsumerBuilder;
import org.apache.pulsar.client.api.Message;
import org.apache.pulsar.client.api.MessageId;
import org.apache.pulsar.client.api.MessageRouter;
import org.apache.pulsar.client.api.Producer;
import org.apache.pulsar.client.api.ProducerBuilder;
import org.apache.pulsar.client.api.PulsarClient;
import org.apache.pulsar.client.api.PulsarClientException;
import org.apache.pulsar.client.api.Reader;
import org.apache.pulsar.client.api.ReaderBuilder;
import org.apache.pulsar.client.api.Schema;
import org.apache.pulsar.client.api.SubscriptionInitialPosition;
import org.apache.pulsar.client.api.SubscriptionMode;
import org.apache.pulsar.client.api.SubscriptionType;
import org.apache.pulsar.client.api.TopicMetadata;
import org.apache.pulsar.client.impl.ConsumerBase;
import org.apache.pulsar.client.impl.ConsumerImpl;
import org.apache.pulsar.client.impl.MultiTopicsConsumerImpl;
import org.apache.pulsar.client.impl.PulsarClientImpl;
import org.apache.pulsar.client.impl.auth.AuthenticationToken;
import org.apache.pulsar.common.naming.TopicName;
import org.apache.pulsar.common.partition.PartitionedTopicMetadata;
@Slf4j
public class PulsarConnectionFactory
implements ConnectionFactory,
QueueConnectionFactory,
TopicConnectionFactory,
AutoCloseable,
Serializable {
private static final long serialVersionUID = 1231231L;
private static final String PENDING_ACK_STORE_SUFFIX = "__transaction_pending_ack";
private static final String SHADED_PREFIX = "com.datastax.oss.pulsar.jms.shaded.";
private static final boolean NEEDS_RELOCATION =
PulsarClient.class.getName().startsWith(SHADED_PREFIX);
private static final Set clientIdentifiers = new ConcurrentSkipListSet<>();
// see resetDefaultValues for final fields
private final transient Map> producers = new ConcurrentHashMap<>();
private final transient Set connections =
Collections.synchronizedSet(new HashSet<>());
private final transient List> consumers = new CopyOnWriteArrayList<>();
private final transient List> readers = new CopyOnWriteArrayList<>();
private transient PulsarClient pulsarClient;
private transient PulsarAdmin pulsarAdmin;
private transient Map producerConfiguration;
private transient ConsumerConfiguration defaultConsumerConfiguration;
private transient String systemNamespace = "public/default";
private transient String defaultClientId = null;
private transient boolean enableTransaction = false;
private transient boolean emulateTransactions = false;
private transient boolean enableClientSideEmulation = false;
private transient boolean transactionsStickyPartitions = false;
private transient boolean useServerSideFiltering = false;
private transient boolean enableJMSPriority = false;
private transient boolean priorityUseLinearMapping = true;
private transient boolean forceDeleteTemporaryDestinations = false;
private transient boolean useExclusiveSubscriptionsForSimpleConsumers = false;
private transient boolean acknowledgeRejectedMessages = false;
private transient String tckUsername = "";
private transient String tckPassword = "";
private transient boolean useCredentialsFromCreateConnection = false;
private transient String lastConnectUsername = null;
private transient String lastConnectPassword = null;
private transient String queueSubscriptionName = "jms-queue";
private transient SubscriptionType topicSharedSubscriptionType = SubscriptionType.Shared;
private transient long waitForServerStartupTimeout = 60000;
private transient boolean usePulsarAdmin = true;
private transient boolean allowTemporaryTopicWithoutAdmin = false;
private transient boolean precreateQueueSubscription = true;
private transient int precreateQueueSubscriptionConsumerQueueSize = 0;
private transient boolean initialized;
private transient boolean closed;
private transient int refreshServerSideFiltersPeriod = 300;
private transient boolean maxMessagesLimitsParallelism = false;
private transient int connectionConsumerStopTimeout = 20000;
private transient Map configuration = Collections.emptyMap();
private transient ScheduledExecutorService sessionListenersThreadPool;
private transient int sessionListenersThreads;
private transient int connectionConsumerParallelism = 1;
public PulsarConnectionFactory() throws JMSException {
this(new HashMap<>());
}
public PulsarConnectionFactory(Map properties) {
setConfiguration(properties);
}
public PulsarConnectionFactory(String configuration) throws JMSException {
this();
setJsonConfiguration(configuration);
}
/**
* Utility method for configuration based on JSON
*
* @return JSON encoded configuration
*/
public String getJsonConfiguration() {
return Utils.runtimeException(() -> new ObjectMapper().writeValueAsString(getConfiguration()));
}
/**
* Apply configuration from a JSON encoded string
*
* @param json the JSON
*/
public void setJsonConfiguration(String json) {
if (json == null || json.isEmpty()) {
setConfiguration(Collections.emptyMap());
return;
}
setConfiguration(Utils.runtimeException(() -> new ObjectMapper().readValue(json, Map.class)));
}
public synchronized Map getConfiguration() {
return Utils.deepCopyMap(configuration);
}
public synchronized void setConfiguration(Map configuration) {
this.configuration = copyAndApplyShadedPrefix(configuration);
}
private static Map copyAndApplyShadedPrefix(Map configuration) {
if (configuration == null) {
return null;
}
if (!NEEDS_RELOCATION) {
return new HashMap<>(configuration);
}
Map copy = new HashMap<>();
configuration.forEach(
(key, value) -> {
if (value instanceof Map) {
copy.put(key, copyAndApplyShadedPrefix((Map) value));
return;
}
if (value instanceof String) {
String result = (String) value;
if (result.length() > 17
&& result
.substring(1)
.startsWith("rg.apache.pulsar") // hack to deal with the Maven Shade plugin
) {
result = SHADED_PREFIX + value;
}
if (log.isDebugEnabled()) {
log.debug("Relocating {} = {} -> {}", key, value, result);
}
copy.put(key, result);
return;
}
copy.put(key, value);
});
return copy;
}
synchronized ConsumerConfiguration getConsumerConfiguration(
ConsumerConfiguration overrideConsumerConfiguration, PulsarDestination destination)
throws InvalidDestinationException {
ConsumerConfiguration result = defaultConsumerConfiguration;
if (overrideConsumerConfiguration != null) {
result = overrideConsumerConfiguration.applyDefaults(result);
}
ConsumerConfiguration overriddenByDestination =
Utils.computeConsumerOverrideConfiguration(destination);
if (overriddenByDestination != null) {
result = overriddenByDestination.applyDefaults(result);
}
return result;
}
private synchronized Map getProducerConfiguration() {
return producerConfiguration;
}
private synchronized void ensureInitialized(String connectUsername, String connectPassword)
throws JMSException {
if (initialized) {
return;
}
if (closed) {
throw new IllegalStateException("This ConnectionFactory is closed");
}
Map configurationCopy = Utils.deepCopyMap(this.configuration);
try {
Map producerConfiguration =
(Map) configurationCopy.remove("producerConfig");
if (producerConfiguration != null) {
Object batcherBuilder = producerConfiguration.get("batcherBuilder");
if (batcherBuilder != null) {
if (batcherBuilder instanceof String) {
String batcherBuilderString = (String) batcherBuilder;
BatcherBuilder builder = BatcherBuilder.DEFAULT;
switch (batcherBuilderString) {
case "KEY_BASED":
builder = BatcherBuilder.KEY_BASED;
break;
case "DEFAULT":
builder = BatcherBuilder.DEFAULT;
break;
default:
throw new IllegalArgumentException(
"Unsupported batcherBuilder " + batcherBuilderString);
}
producerConfiguration.put("batcherBuilder", builder);
}
}
this.producerConfiguration = new HashMap(producerConfiguration);
} else {
this.producerConfiguration = Collections.emptyMap();
}
Map consumerConfigurationM =
(Map) configurationCopy.remove("consumerConfig");
this.defaultConsumerConfiguration =
ConsumerConfiguration.buildConsumerConfiguration(consumerConfigurationM);
this.systemNamespace =
getAndRemoveString("jms.systemNamespace", "public/default", configurationCopy);
this.tckUsername = getAndRemoveString("jms.tckUsername", "", configurationCopy);
this.tckPassword = getAndRemoveString("jms.tckPassword", "", configurationCopy);
// with useCredentialsFromCreateConnection we pick the username/password from
// Connection.connect() to
// configure authentication parameters.
// the meaning of username/password depends on the Authentication Plugin
this.useCredentialsFromCreateConnection =
Boolean.parseBoolean(
getAndRemoveString(
"jms.useCredentialsFromCreateConnection", "false", configurationCopy));
this.defaultClientId = getAndRemoveString("jms.clientId", null, configurationCopy);
this.queueSubscriptionName =
getAndRemoveString("jms.queueSubscriptionName", "jms-queue", configurationCopy);
this.usePulsarAdmin =
Boolean.parseBoolean(getAndRemoveString("jms.usePulsarAdmin", "true", configurationCopy));
this.allowTemporaryTopicWithoutAdmin =
Boolean.parseBoolean(
getAndRemoveString(
"jms.allowTemporaryTopicWithoutAdmin", "false", configurationCopy));
this.precreateQueueSubscription =
Boolean.parseBoolean(
getAndRemoveString("jms.precreateQueueSubscription", "true", configurationCopy));
this.precreateQueueSubscriptionConsumerQueueSize =
Integer.parseInt(
getAndRemoveString(
"jms.precreateQueueSubscriptionConsumerQueueSize", "0", configurationCopy));
this.refreshServerSideFiltersPeriod =
Integer.parseInt(
getAndRemoveString("jms.refreshServerSideFiltersPeriod", "300", configurationCopy));
this.maxMessagesLimitsParallelism =
Boolean.parseBoolean(
getAndRemoveString("jms.maxMessagesLimitsParallelism", "false", configurationCopy));
this.connectionConsumerStopTimeout =
Integer.parseInt(
getAndRemoveString("jms.connectionConsumerStopTimeout", "20000", configurationCopy));
this.sessionListenersThreads =
Integer.parseInt(
getAndRemoveString(
"jms.sessionListenersThreads",
(Runtime.getRuntime().availableProcessors() * 2) + "",
configurationCopy));
final String rawTopicSharedSubscriptionType =
getAndRemoveString(
"jms.topicSharedSubscriptionType", SubscriptionType.Shared.name(), configurationCopy);
this.topicSharedSubscriptionType =
Stream.of(SubscriptionType.values())
.filter(
t ->
t.name().equalsIgnoreCase(rawTopicSharedSubscriptionType)
&& t != SubscriptionType.Exclusive)
.findFirst()
.orElseThrow(
() ->
new IllegalArgumentException(
"Invalid jms.topicSubscriptionType: "
+ rawTopicSharedSubscriptionType
+ ", only "
+ SubscriptionType.Shared
+ ", "
+ SubscriptionType.Key_Shared
+ " and "
+ SubscriptionType.Failover
+ " "));
this.waitForServerStartupTimeout =
Long.parseLong(
getAndRemoveString("jms.waitForServerStartupTimeout", "60000", configurationCopy));
this.enableClientSideEmulation =
Boolean.parseBoolean(
getAndRemoveString("jms.enableClientSideEmulation", "false", configurationCopy));
this.transactionsStickyPartitions =
Boolean.parseBoolean(
getAndRemoveString("jms.transactionsStickyPartitions", "false", configurationCopy));
this.useServerSideFiltering =
Boolean.parseBoolean(
getAndRemoveString("jms.useServerSideFiltering", "false", configurationCopy));
this.enableJMSPriority =
Boolean.parseBoolean(
getAndRemoveString("jms.enableJMSPriority", "false", configurationCopy));
String priorityMapping =
getAndRemoveString("jms.priorityMapping", "linear", configurationCopy);
switch (priorityMapping) {
case "linear":
this.priorityUseLinearMapping = true;
break;
case "non-linear":
this.priorityUseLinearMapping = false;
break;
default:
throw new IllegalArgumentException(
"jms.priorityMapping value '"
+ priorityMapping
+ "' is not valid, only 'linear' and 'non-linear'");
}
// in Exclusive mode Pulsar does not support delayed messages
// with this flag you force to not use Exclusive subscription and so to support
// delayed messages are well
this.useExclusiveSubscriptionsForSimpleConsumers =
Boolean.parseBoolean(
getAndRemoveString(
"jms.useExclusiveSubscriptionsForSimpleConsumers", "true", configurationCopy));
// This flag is to force acknowledgement for messages that are rejected due to
// filtering in case of Shared subscription.
// If you have a shared subscription on a topic (Topic or Queue) and a message
// is filtered out, by default we negatively acknowledge the message in order to
// let another consumer on the same subscription to receive it.
// with this flag turned to "true" when a Consumer receives a message and it filters
// it out, we acknowledge the message, this way it won't be consumed anymore.
this.acknowledgeRejectedMessages =
Boolean.parseBoolean(
getAndRemoveString("jms.acknowledgeRejectedMessages", "false", configurationCopy));
// default is false
this.forceDeleteTemporaryDestinations =
Boolean.parseBoolean(
getAndRemoveString(
"jms.forceDeleteTemporaryDestinations", "false", configurationCopy));
this.enableTransaction =
Boolean.parseBoolean(
configurationCopy.getOrDefault("enableTransaction", "false").toString());
this.emulateTransactions =
Boolean.parseBoolean(
getAndRemoveString("jms.emulateTransactions", "false", configurationCopy).toString());
if (emulateTransactions && enableTransaction) {
throw new IllegalStateException(
"You cannot set both enableTransaction and jms.emulateTransactions");
}
String webServiceUrl =
getAndRemoveString("webServiceUrl", "http://localhost:8080", configurationCopy);
String brokenServiceUrl = getAndRemoveString("brokerServiceUrl", "", configurationCopy);
PulsarClient pulsarClient = null;
PulsarAdmin pulsarAdmin = null;
try {
// must be the same as
// https://pulsar.apache.org/docs/en/security-tls-keystore/#configuring-clients
String authPluginClassName = getAndRemoveString("authPlugin", "", configurationCopy);
String authParamsString = getAndRemoveString("authParams", "", configurationCopy);
if (useCredentialsFromCreateConnection) {
if (connectUsername == null) {
connectUsername = "";
}
if (connectPassword == null) {
connectPassword = "";
}
// for JWT token authentication the "password" is passed as "authParams"
if (authPluginClassName.equals(AuthenticationToken.class.getName())) {
authParamsString = connectPassword;
} else {
throw new jakarta.jms.IllegalStateRuntimeException(
"With jms.useCredentialsFromConnect:true "
+ "only JWT (AuthenticationToken) authentication is currently supported");
}
}
Authentication authentication =
AuthenticationFactory.create(authPluginClassName, authParamsString);
if (log.isDebugEnabled()) {
log.debug("Authentication {}", authentication);
}
boolean tlsAllowInsecureConnection =
Boolean.parseBoolean(
getAndRemoveString("tlsAllowInsecureConnection", "false", configurationCopy));
boolean tlsEnableHostnameVerification =
Boolean.parseBoolean(
getAndRemoveString("tlsEnableHostnameVerification", "true", configurationCopy));
final String tlsTrustCertsFilePath =
(String) getAndRemoveString("tlsTrustCertsFilePath", "", configurationCopy);
boolean useKeyStoreTls =
Boolean.parseBoolean(getAndRemoveString("useKeyStoreTls", "false", configurationCopy));
String tlsTrustStoreType =
getAndRemoveString("tlsTrustStoreType", "JKS", configurationCopy);
String tlsTrustStorePath = getAndRemoveString("tlsTrustStorePath", "", configurationCopy);
String tlsTrustStorePassword =
getAndRemoveString("tlsTrustStorePassword", "", configurationCopy);
pulsarAdmin =
PulsarAdmin.builder()
.serviceHttpUrl(webServiceUrl)
.allowTlsInsecureConnection(tlsAllowInsecureConnection)
.enableTlsHostnameVerification(tlsEnableHostnameVerification)
.tlsTrustCertsFilePath(tlsTrustCertsFilePath)
.useKeyStoreTls(useKeyStoreTls)
.tlsTrustStoreType(tlsTrustStoreType)
.tlsTrustStorePath(tlsTrustStorePath)
.tlsTrustStorePassword(tlsTrustStorePassword)
.authentication(authentication)
.build();
ClientBuilder clientBuilder =
PulsarClient.builder()
.loadConf(configurationCopy)
.tlsTrustStorePassword(tlsTrustStorePassword)
.tlsTrustStorePath(tlsTrustStorePath)
.tlsTrustCertsFilePath(tlsTrustCertsFilePath)
.tlsTrustStoreType(tlsTrustStoreType)
.useKeyStoreTls(useKeyStoreTls)
.enableTlsHostnameVerification(tlsEnableHostnameVerification)
.allowTlsInsecureConnection(tlsAllowInsecureConnection)
.serviceUrl(webServiceUrl)
.authentication(authentication);
if (!brokenServiceUrl.isEmpty()) {
clientBuilder.serviceUrl(brokenServiceUrl);
}
pulsarClient = buildPulsarClient(clientBuilder);
if (pulsarClient != null && refreshServerSideFiltersPeriod > 0 && useServerSideFiltering) {
PulsarClientImpl impl = (PulsarClientImpl) pulsarClient;
ScheduledExecutorService timer =
(ScheduledExecutorService) impl.getScheduledExecutorProvider().getExecutor();
timer.scheduleWithFixedDelay(
this::refreshServerSideSelectors,
refreshServerSideFiltersPeriod,
refreshServerSideFiltersPeriod,
TimeUnit.SECONDS);
}
} catch (PulsarClientException err) {
if (pulsarAdmin != null) {
pulsarAdmin.close();
}
if (pulsarClient != null) {
pulsarClient.close();
}
throw err;
}
this.pulsarClient = pulsarClient;
this.pulsarAdmin = pulsarAdmin;
if (useCredentialsFromCreateConnection) {
if (lastConnectUsername == null) {
// commit credentials only in case of success
this.lastConnectUsername = connectUsername;
this.lastConnectPassword = connectPassword;
}
}
this.initialized = true;
} catch (Throwable t) {
throw Utils.handleException(t);
}
}
protected PulsarClient buildPulsarClient(ClientBuilder builder) throws PulsarClientException {
return builder.build();
}
private void validateConnectUsernamePasswordReused(String connectUsername, String connectPassword)
throws IllegalStateException {
if (lastConnectUsername != null) {
if (!Objects.equals(connectUsername, lastConnectUsername)) {
throw new IllegalStateException(
"With jms.useCredentialsFromConnect:true "
+ "once you call connect(username,password) you must always use the same credentials, "
+ "bad username "
+ connectUsername
+ ", expecting "
+ lastConnectUsername);
}
if (!Objects.equals(connectPassword, lastConnectPassword)) {
throw new IllegalStateException(
"With jms.useCredentialsFromConnect:true "
+ "once you call connect(username,password) you must always use the same credentials, "
+ "password does not match");
}
}
}
public synchronized boolean isEnableClientSideEmulation() {
return enableClientSideEmulation;
}
public synchronized boolean isTransactionsStickyPartitions() {
return transactionsStickyPartitions;
}
public synchronized boolean isUseServerSideFiltering() {
return useServerSideFiltering;
}
public synchronized boolean isEnableJMSPriority() {
return enableJMSPriority;
}
public synchronized boolean isPriorityUseLinearMapping() {
return priorityUseLinearMapping;
}
synchronized String getDefaultClientId() {
return defaultClientId;
}
public synchronized boolean isEnableTransaction() {
return enableTransaction;
}
public synchronized boolean isEmulateTransactions() {
return emulateTransactions;
}
public synchronized PulsarClient getPulsarClient() {
return pulsarClient;
}
public synchronized PulsarAdmin getPulsarAdmin() throws jakarta.jms.IllegalStateException {
if (!usePulsarAdmin) {
throw new jakarta.jms.IllegalStateException(
"jms.usePulsarAdmin is set to false, this feature is not available");
}
return pulsarAdmin;
}
public synchronized String getSystemNamespace() {
return systemNamespace;
}
/**
* Creates a connection with the default user identity. The connection is created in stopped mode.
* No messages will be delivered until the {@code Connection.start} method is explicitly called.
*
* @return a newly created connection
* @throws JMSException if the JMS provider fails to create the connection due to some internal
* error.
* @throws JMSSecurityException if client authentication fails due to an invalid user name or
* password.
* @since JMS 1.1
*/
@Override
public PulsarConnection createConnection() throws JMSException {
ensureInitialized(null, null);
validateUserNamePassword(true, null, null);
PulsarConnection res = new PulsarConnection(this);
connections.add(res);
return res;
}
/**
* Creates a connection with the specified user identity. The connection is created in stopped
* mode. No messages will be delivered until the {@code Connection.start} method is explicitly
* called.
*
* @param userName the caller's user name
* @param password the caller's password
* @return a newly created connection
* @throws JMSException if the JMS provider fails to create the connection due to some internal
* error.
* @throws JMSSecurityException if client authentication fails due to an invalid user name or
* password.
* @since JMS 1.1
*/
@Override
public PulsarConnection createConnection(String userName, String password) throws JMSException {
ensureInitialized(userName, password);
validateUserNamePassword(false, userName, password);
PulsarConnection res = new PulsarConnection(this);
connections.add(res);
return res;
}
private synchronized void validateUserNamePassword(
boolean anonymous, String userName, String password) throws JMSException {
if (useCredentialsFromCreateConnection) {
validateConnectUsernamePasswordReused(userName, password);
}
if (!anonymous && tckUsername != null && !tckUsername.isEmpty()) {
if (!Objects.equals(tckUsername, userName) || !Objects.equals(tckPassword, password)) {
// this verification is here only for the TCK
throw new JMSSecurityException("Unauthorized");
}
}
}
/**
* Creates a JMSContext with the default user identity and an unspecified sessionMode.
*
* A connection and session are created for use by the new JMSContext. The connection is
* created in stopped mode but will be automatically started when a JMSConsumer is created.
*
*
The behaviour of the session that is created depends on whether this method is called in a
* Java SE environment, in the Java EE application client container, or in the Java EE web or EJB
* container. If this method is called in the Java EE web or EJB container then the behaviour of
* the session also depends on whether or not there is an active JTA transaction in progress.
*
*
In a Java SE environment or in the Java EE application client container:
*
*
* - The session will be non-transacted and received messages will be acknowledged
* automatically using an acknowledgement mode of {@code JMSContext.AUTO_ACKNOWLEDGE} For a
* definition of the meaning of this acknowledgement mode see the link below.
*
*
* In a Java EE web or EJB container, when there is an active JTA transaction in
* progress:
*
*
* - The session will participate in the JTA transaction and will be committed or rolled back
* when that transaction is committed or rolled back, not by calling the {@code
* JMSContext}'s {@code commit} or {@code rollback} methods.
*
*
* In the Java EE web or EJB container, when there is no active JTA transaction in
* progress:
*
*
* - The session will be non-transacted and received messages will be acknowledged
* automatically using an acknowledgement mode of {@code JMSContext.AUTO_ACKNOWLEDGE} For a
* definition of the meaning of this acknowledgement mode see the link below.
*
*
* @return a newly created JMSContext
* @throws JMSRuntimeException if the JMS provider fails to create the JMSContext due to some
* internal error.
* @throws JMSSecurityRuntimeException if client authentication fails due to an invalid user name
* or password.
* @see JMSContext#AUTO_ACKNOWLEDGE
* @see ConnectionFactory#createContext(int)
* @see ConnectionFactory#createContext(String, String)
* @see ConnectionFactory#createContext(String, String, int)
* @see JMSContext#createContext(int)
* @since JMS 2.0
*/
@Override
public JMSContext createContext() {
return createContext(JMSContext.AUTO_ACKNOWLEDGE);
}
/**
* Creates a JMSContext with the specified user identity and an unspecified sessionMode.
*
* A connection and session are created for use by the new JMSContext. The connection is
* created in stopped mode but will be automatically started when a JMSConsumer.
*
*
The behaviour of the session that is created depends on whether this method is called in a
* Java SE environment, in the Java EE application client container, or in the Java EE web or EJB
* container. If this method is called in the Java EE web or EJB container then the behaviour of
* the session also depends on whether or not there is an active JTA transaction in progress.
*
*
In a Java SE environment or in the Java EE application client container:
*
*
* - The session will be non-transacted and received messages will be acknowledged
* automatically using an acknowledgement mode of {@code JMSContext.AUTO_ACKNOWLEDGE} For a
* definition of the meaning of this acknowledgement mode see the link below.
*
*
* In a Java EE web or EJB container, when there is an active JTA transaction in
* progress:
*
*
* - The session will participate in the JTA transaction and will be committed or rolled back
* when that transaction is committed or rolled back, not by calling the {@code
* JMSContext}'s {@code commit} or {@code rollback} methods.
*
*
* In the Java EE web or EJB container, when there is no active JTA transaction in
* progress:
*
*
* - The session will be non-transacted and received messages will be acknowledged
* automatically using an acknowledgement mode of {@code JMSContext.AUTO_ACKNOWLEDGE} For a
* definition of the meaning of this acknowledgement mode see the link below.
*
*
* @param userName the caller's user name
* @param password the caller's password
* @return a newly created JMSContext
* @throws JMSRuntimeException if the JMS provider fails to create the JMSContext due to some
* internal error.
* @throws JMSSecurityRuntimeException if client authentication fails due to an invalid user name
* or password.
* @see JMSContext#AUTO_ACKNOWLEDGE
* @see ConnectionFactory#createContext()
* @see ConnectionFactory#createContext(int)
* @see ConnectionFactory#createContext(String, String, int)
* @see JMSContext#createContext(int)
* @since JMS 2.0
*/
@Override
public JMSContext createContext(String userName, String password) {
return createContext(userName, password, JMSContext.AUTO_ACKNOWLEDGE);
}
/**
* Creates a JMSContext with the specified user identity and the specified session mode.
*
* A connection and session are created for use by the new JMSContext. The JMSContext is
* created in stopped mode but will be automatically started when a JMSConsumer is created.
*
*
The effect of setting the {@code sessionMode} argument depends on whether this method is
* called in a Java SE environment, in the Java EE application client container, or in the Java EE
* web or EJB container. If this method is called in the Java EE web or EJB container then the
* effect of setting the {@code sessionMode} argument also depends on whether or not there is an
* active JTA transaction in progress.
*
*
In a Java SE environment or in the Java EE application client container:
*
*
* - If {@code sessionMode} is set to {@code JMSContext.SESSION_TRANSACTED} then the session
* will use a local transaction which may subsequently be committed or rolled back by
* calling the {@code JMSContext}'s {@code commit} or {@code rollback} methods.
*
- If {@code sessionMode} is set to any of {@code JMSContext.CLIENT_ACKNOWLEDGE}, {@code
* JMSContext.AUTO_ACKNOWLEDGE} or {@code JMSContext.DUPS_OK_ACKNOWLEDGE}. then the session
* will be non-transacted and messages received by this session will be acknowledged
* according to the value of {@code sessionMode}. For a definition of the meaning of these
* acknowledgement modes see the links below.
*
*
* In a Java EE web or EJB container, when there is an active JTA transaction in
* progress:
*
*
* - The argument {@code sessionMode} is ignored. The session will participate in the JTA
* transaction and will be committed or rolled back when that transaction is committed or
* rolled back, not by calling the {@code JMSContext}'s {@code commit} or {@code rollback}
* methods. Since the argument is ignored, developers are recommended to use {@code
* createContext(String userName, String password)} instead of this method.
*
*
* In the Java EE web or EJB container, when there is no active JTA transaction in
* progress:
*
*
* - The argument {@code acknowledgeMode} must be set to either of {@code
* JMSContext.AUTO_ACKNOWLEDGE} or {@code JMSContext.DUPS_OK_ACKNOWLEDGE}. The session will
* be non-transacted and messages received by this session will be acknowledged
* automatically according to the value of {@code acknowledgeMode}. For a definition of the
* meaning of these acknowledgement modes see the links below. The values {@code
* JMSContext.SESSION_TRANSACTED} and {@code JMSContext.CLIENT_ACKNOWLEDGE} may not be used.
*
*
* @param userName the caller's user name
* @param password the caller's password
* @param sessionMode indicates which of four possible session modes will be used.
*
* - If this method is called in a Java SE environment or in the Java EE application
* client container, the permitted values are {@code JMSContext.SESSION_TRANSACTED},
* {@code JMSContext.CLIENT_ACKNOWLEDGE}, {@code JMSContext.AUTO_ACKNOWLEDGE} and {@code
* JMSContext.DUPS_OK_ACKNOWLEDGE}.
*
- If this method is called in the Java EE web or EJB container when there is an active
* JTA transaction in progress then this argument is ignored.
*
- If this method is called in the Java EE web or EJB container when there is no active
* JTA transaction in progress, the permitted values are {@code
* JMSContext.AUTO_ACKNOWLEDGE} and {@code JMSContext.DUPS_OK_ACKNOWLEDGE}. In this case
* the values {@code JMSContext.TRANSACTED} and {@code JMSContext.CLIENT_ACKNOWLEDGE}
* are not permitted.
*
*
* @return a newly created JMSContext
* @throws JMSRuntimeException if the JMS provider fails to create the JMSContext due to some
* internal error.
* @throws JMSSecurityRuntimeException if client authentication fails due to an invalid user name
* or password.
* @see JMSContext#SESSION_TRANSACTED
* @see JMSContext#CLIENT_ACKNOWLEDGE
* @see JMSContext#AUTO_ACKNOWLEDGE
* @see JMSContext#DUPS_OK_ACKNOWLEDGE
* @see ConnectionFactory#createContext()
* @see ConnectionFactory#createContext(int)
* @see ConnectionFactory#createContext(String, String)
* @see JMSContext#createContext(int)
* @since JMS 2.0
*/
@Override
public JMSContext createContext(String userName, String password, int sessionMode) {
Utils.runtimeException(() -> ensureInitialized(userName, password));
Utils.runtimeException(() -> validateUserNamePassword(false, userName, password));
return new PulsarJMSContext(this, sessionMode, false, userName, password);
}
/**
* Creates a JMSContext with the default user identity and the specified session mode.
*
* A connection and session are created for use by the new JMSContext. The JMSContext is
* created in stopped mode but will be automatically started when a JMSConsumer is created.
*
*
The effect of setting the {@code sessionMode} argument depends on whether this method is
* called in a Java SE environment, in the Java EE application client container, or in the Java EE
* web or EJB container. If this method is called in the Java EE web or EJB container then the
* effect of setting the {@code sessionMode} argument also depends on whether or not there is an
* active JTA transaction in progress.
*
*
In a Java SE environment or in the Java EE application client container:
*
*
* - If {@code sessionMode} is set to {@code JMSContext.SESSION_TRANSACTED} then the session
* will use a local transaction which may subsequently be committed or rolled back by
* calling the {@code JMSContext}'s {@code commit} or {@code rollback} methods.
*
- If {@code sessionMode} is set to any of {@code JMSContext.CLIENT_ACKNOWLEDGE}, {@code
* JMSContext.AUTO_ACKNOWLEDGE} or {@code JMSContext.DUPS_OK_ACKNOWLEDGE}. then the session
* will be non-transacted and messages received by this session will be acknowledged
* according to the value of {@code sessionMode}. For a definition of the meaning of these
* acknowledgement modes see the links below.
*
*
* In a Java EE web or EJB container, when there is an active JTA transaction in
* progress:
*
*
* - The argument {@code sessionMode} is ignored. The session will participate in the JTA
* transaction and will be committed or rolled back when that transaction is committed or
* rolled back, not by calling the {@code JMSContext}'s {@code commit} or {@code rollback}
* methods. Since the argument is ignored, developers are recommended to use {@code
* createContext()} instead of this method.
*
*
* In the Java EE web or EJB container, when there is no active JTA transaction in
* progress:
*
*
* - The argument {@code acknowledgeMode} must be set to either of {@code
* JMSContext.AUTO_ACKNOWLEDGE} or {@code JMSContext.DUPS_OK_ACKNOWLEDGE}. The session will
* be non-transacted and messages received by this session will be acknowledged
* automatically according to the value of {@code acknowledgeMode}. For a definition of the
* meaning of these acknowledgement modes see the links below. The values {@code
* JMSContext.SESSION_TRANSACTED} and {@code JMSContext.CLIENT_ACKNOWLEDGE} may not be used.
*
*
* @param sessionMode indicates which of four possible session modes will be used.
*
* - If this method is called in a Java SE environment or in the Java EE application
* client container, the permitted values are {@code JMSContext.SESSION_TRANSACTED},
* {@code JMSContext.CLIENT_ACKNOWLEDGE}, {@code JMSContext.AUTO_ACKNOWLEDGE} and {@code
* JMSContext.DUPS_OK_ACKNOWLEDGE}.
*
- If this method is called in the Java EE web or EJB container when there is an active
* JTA transaction in progress then this argument is ignored.
*
- If this method is called in the Java EE web or EJB container when there is no active
* JTA transaction in progress, the permitted values are {@code
* JMSContext.AUTO_ACKNOWLEDGE} and {@code JMSContext.DUPS_OK_ACKNOWLEDGE}. In this case
* the values {@code JMSContext.TRANSACTED} and {@code JMSContext.CLIENT_ACKNOWLEDGE}
* are not permitted.
*
*
* @return a newly created JMSContext
* @throws JMSRuntimeException if the JMS provider fails to create the JMSContext due to some
* internal error.
* @throws JMSSecurityRuntimeException if client authentication fails due to an invalid user name
* or password.
* @see JMSContext#SESSION_TRANSACTED
* @see JMSContext#CLIENT_ACKNOWLEDGE
* @see JMSContext#AUTO_ACKNOWLEDGE
* @see JMSContext#DUPS_OK_ACKNOWLEDGE
* @see ConnectionFactory#createContext()
* @see ConnectionFactory#createContext(String, String)
* @see ConnectionFactory#createContext(String, String, int)
* @see JMSContext#createContext(int)
* @since JMS 2.0
*/
@Override
public JMSContext createContext(int sessionMode) {
Utils.runtimeException(() -> ensureInitialized(null, null));
Utils.runtimeException(() -> validateUserNamePassword(true, null, null));
return new PulsarJMSContext(this, sessionMode, true, null, null);
}
public void close() {
synchronized (this) {
closed = true;
if (!initialized) {
return;
}
}
// close all connections and wait for ongoing operations
// to complete
for (PulsarConnection con : new ArrayList<>(connections)) {
try {
con.close();
} catch (Exception ignore) {
// ignore
Utils.handleException(ignore);
}
}
for (Producer> producer : producers.values()) {
try {
producer.close();
} catch (PulsarClientException ignore) {
// ignore
Utils.handleException(ignore);
}
}
if (this.pulsarAdmin != null) {
this.pulsarAdmin.close();
}
try {
if (this.pulsarClient != null) {
this.pulsarClient.close();
}
} catch (PulsarClientException err) {
log.info("Error closing PulsarClient", err);
}
if (sessionListenersThreadPool != null) {
sessionListenersThreadPool.shutdown();
sessionListenersThreadPool = null;
}
}
public static PulsarDestination toPulsarDestination(Destination destination) throws JMSException {
if (destination instanceof PulsarDestination) {
return (PulsarDestination) destination;
} else if (destination instanceof Queue) {
return new PulsarQueue(((Queue) destination).getQueueName());
} else if (destination instanceof Topic) {
return new PulsarTopic(((Topic) destination).getTopicName());
} else {
throw new IllegalStateException("Cannot convert " + destination + " to a PulsarDestination");
}
}
public String getPulsarTopicName(Destination defaultDestination) throws JMSException {
PulsarDestination destination = toPulsarDestination(defaultDestination);
String topicName = destination.getInternalTopicName();
return applySystemNamespace(topicName);
}
Producer getProducerForDestination(Destination defaultDestination, boolean transactions)
throws JMSException {
try {
String fullQualifiedTopicName = getPulsarTopicName(defaultDestination);
String key = transactions ? fullQualifiedTopicName + "-tx" : fullQualifiedTopicName;
boolean transactionsStickyPartitions = transactions && isTransactionsStickyPartitions();
boolean enableJMSPriority = isEnableJMSPriority();
boolean producerJMSPriorityUseLinearMapping =
enableJMSPriority && isPriorityUseLinearMapping();
return producers.computeIfAbsent(
key,
d -> {
try {
return Utils.invoke(
() -> {
Map producerConfiguration = getProducerConfiguration();
ProducerBuilder producerBuilder =
pulsarClient
.newProducer()
.topic(applySystemNamespace(fullQualifiedTopicName))
.loadConf(producerConfiguration);
if (producerConfiguration.containsKey("batcherBuilder")) {
producerBuilder.batcherBuilder(
(BatcherBuilder) producerConfiguration.get("batcherBuilder"));
}
Map properties = new HashMap<>();
if (transactions) {
properties.put("jms.transactions", "enabled");
} else {
properties.put("jms.transactions", "disabled");
}
if (enableJMSPriority) {
properties.put("jms.priority", "enabled");
properties.put(
"jms.priorityMapping",
producerJMSPriorityUseLinearMapping ? "linear" : "non-linear");
producerBuilder.messageRouter(
new MessageRouter() {
@Override
public int choosePartition(Message> msg, TopicMetadata metadata) {
int key = PulsarMessage.readJMSPriority(msg);
return Utils.mapPriorityToPartition(
key,
metadata.numPartitions(),
producerJMSPriorityUseLinearMapping);
}
});
} else if (transactions && transactionsStickyPartitions) {
producerBuilder.messageRouter(
new MessageRouter() {
@Override
public int choosePartition(Message> msg, TopicMetadata metadata) {
long key = Long.parseLong(msg.getProperty("JMSTX"));
return signSafeMod(key, metadata.numPartitions());
}
});
}
producerBuilder.properties(properties);
return producerBuilder.create();
});
} catch (JMSException err) {
throw new RuntimeException(err);
}
});
} catch (RuntimeException err) {
throw (JMSException) err.getCause();
}
}
synchronized boolean isUsePulsarAdmin() {
return usePulsarAdmin;
}
synchronized boolean isPrecreateQueueSubscription() {
return precreateQueueSubscription;
}
public void ensureQueueSubscription(PulsarDestination destination) throws JMSException {
if (!isPrecreateQueueSubscription()) {
return;
}
if (destination.isRegExp()) {
// for regexp we cannot create the subscriptions
return;
}
if (destination.isMultiTopic()) {
for (PulsarDestination subDestination : destination.getDestinations()) {
ensureQueueSubscription(subDestination);
}
return;
}
long start = System.currentTimeMillis();
// please note that in the special jms-queue subscription we cannot
// set a selector, because it is shared among all the Consumers of the Queue
String fullQualifiedTopicName = getPulsarTopicName(destination);
while (true) {
String subscriptionName = getQueueSubscriptionName(destination);
try {
if (isUsePulsarAdmin()) {
getPulsarAdmin()
.topics()
.createSubscription(fullQualifiedTopicName, subscriptionName, MessageId.earliest);
} else {
// if we cannot use PulsarAdmin,
// let's try to create a consumer with zero queue
getPulsarClient()
.newConsumer()
.subscriptionType(getTopicSharedSubscriptionType())
.subscriptionName(subscriptionName)
.subscriptionInitialPosition(SubscriptionInitialPosition.Earliest)
.receiverQueueSize(
getPrecreateQueueSubscriptionConsumerQueueSize(destination.isRegExp()))
.topic(fullQualifiedTopicName)
.subscribe()
.close();
}
break;
} catch (PulsarAdminException.ConflictException exists) {
log.debug(
"Subscription {} already exists for {}", subscriptionName, fullQualifiedTopicName);
break;
} catch (PulsarAdminException | PulsarClientException err) {
// special handling for server startup
// it mitigates problems in tests
// but also it is useful in order to let
// applications start when the server is not available
long now = System.currentTimeMillis();
if (now - start > getWaitForServerStartupTimeout()) {
throw Utils.handleException(err);
} else {
log.info(
"Got {} error while setting up subscription for queue {}, maybe the namespace/broker is still starting",
err.toString(),
fullQualifiedTopicName);
try {
Thread.sleep(1000);
} catch (InterruptedException interruptedException) {
Thread.currentThread().interrupt();
throw Utils.handleException(err);
}
}
}
}
}
public ConsumerBase> createConsumer(
PulsarDestination destination,
String consumerName,
SubscriptionMode subscriptionMode,
SubscriptionType subscriptionType,
String messageSelector,
boolean noLocal,
PulsarSession session)
throws JMSException {
if (destination.isQueue() && subscriptionMode != SubscriptionMode.Durable) {
throw new IllegalStateException("only durable mode for queues");
}
if (destination.isQueue() && subscriptionType == SubscriptionType.Exclusive) {
throw new IllegalStateException("only Shared SubscriptionType for queues");
}
log.debug(
"createConsumer {} {} {} {}",
destination,
consumerName,
subscriptionMode,
subscriptionType,
messageSelector);
Map subscriptionProperties = new HashMap<>();
Map consumerMetadata = new HashMap<>();
consumerMetadata.put("jms.destination.type", destination.isQueue() ? "queue" : "topic");
consumerMetadata.put(
"jms.acknowledgeMode",
PulsarSession.ACKNOWLEDGE_MODE_TO_STRING(session.getAcknowledgeMode()));
if (isUseServerSideFiltering()) {
// this flag enables filtering on the subscription/consumer
// the plugin will apply filtering only on these subscriptions/consumers,
// in order to not impact on other subscriptions
consumerMetadata.put("jms.filtering", "true");
subscriptionProperties.put("jms.destination.type", destination.isQueue() ? "queue" : "topic");
if (noLocal) {
consumerMetadata.put(
"jms.filter.JMSConnectionID", session.getConnection().getConnectionId());
}
}
if (isUseServerSideFiltering()) {
if (messageSelector != null) {
consumerMetadata.put("jms.selector", messageSelector);
}
if (destination.isTopic()) {
consumerMetadata.put("jms.selector.reject.action", "drop");
} else {
// for Queue is it on the Consumer
consumerMetadata.put("jms.selector.reject.action", "reschedule");
}
}
if (isAcknowledgeRejectedMessages()) {
consumerMetadata.put("jms.force.drop.rejected", "true");
}
boolean enablePriority = false;
if (isEnableJMSPriority()) {
enablePriority = true;
consumerMetadata.put("jms.priority", "enabled");
}
try {
ConsumerConfiguration consumerConfiguration =
getConsumerConfiguration(session.getOverrideConsumerConfiguration(), destination);
Schema> schema = consumerConfiguration.getConsumerSchema();
if (schema == null) {
schema = Schema.BYTES;
}
// for queues we have a single shared subscription
String subscriptionName =
destination.isQueue() ? getQueueSubscriptionName(destination) : consumerName;
SubscriptionInitialPosition initialPosition =
destination.isTopic()
? SubscriptionInitialPosition.Latest
: SubscriptionInitialPosition.Earliest;
ConsumerBuilder> builder =
pulsarClient
.newConsumer(schema)
// these properties can be overridden by the configuration
.negativeAckRedeliveryDelay(1, TimeUnit.SECONDS)
.loadConf(consumerConfiguration.getConsumerConfiguration())
.properties(consumerMetadata)
// these properties cannot be overwritten by the configuration
.subscriptionInitialPosition(initialPosition)
.subscriptionMode(subscriptionMode)
.subscriptionProperties(subscriptionProperties)
.subscriptionType(subscriptionType)
.subscriptionName(subscriptionName);
if (enablePriority) {
builder.startPaused(true);
}
if (destination.isRegExp()) {
String fullQualifiedTopicName = getPulsarTopicName(destination);
builder.topicsPattern(fullQualifiedTopicName);
} else if (destination.isMultiTopic()) {
List extends PulsarDestination> destinations = destination.getDestinations();
List fullQualifiedTopicNames = new ArrayList<>(destinations.size());
for (PulsarDestination d : destinations) {
fullQualifiedTopicNames.add(getPulsarTopicName(d));
}
builder.topics(fullQualifiedTopicNames);
} else {
String fullQualifiedTopicName = getPulsarTopicName(destination);
builder.topic(fullQualifiedTopicName);
}
if (consumerConfiguration.getDeadLetterPolicy() != null) {
builder.deadLetterPolicy(consumerConfiguration.getDeadLetterPolicy());
}
if (consumerConfiguration.getNegativeAckRedeliveryBackoff() != null) {
builder.negativeAckRedeliveryBackoff(
consumerConfiguration.getNegativeAckRedeliveryBackoff());
}
if (consumerConfiguration.getAckTimeoutRedeliveryBackoff() != null) {
builder.ackTimeoutRedeliveryBackoff(consumerConfiguration.getAckTimeoutRedeliveryBackoff());
}
builder.intercept(session.getConsumerInterceptor());
Consumer> newConsumer = builder.subscribe();
if (log.isDebugEnabled()) {
if (newConsumer instanceof MultiTopicsConsumerImpl) {
MultiTopicsConsumerImpl multiTopicsConsumer = (MultiTopicsConsumerImpl) newConsumer;
log.debug("Destinations {}", multiTopicsConsumer.getPartitions());
}
}
consumers.add(newConsumer);
if (isEnableJMSPriority()) {
replaceIncomingMessageList(newConsumer);
newConsumer.resume();
}
return (ConsumerBase) newConsumer;
} catch (PulsarClientException err) {
throw Utils.handleException(err);
}
}
private static void replaceIncomingMessageList(Consumer c) {
try {
ConsumerBase consumerBase = (ConsumerBase) c;
Field incomingMessages = ConsumerBase.class.getDeclaredField("incomingMessages");
incomingMessages.setAccessible(true);
Object oldQueue = incomingMessages.get(consumerBase);
BlockingQueue newQueue;
if (oldQueue.getClass().isAssignableFrom(PriorityBlockingQueue.class)) {
newQueue =
new PriorityBlockingQueue(
10,
new Comparator() {
@Override
public int compare(Message o1, Message o2) {
int priority1 = PulsarMessage.readJMSPriority(o1);
int priority2 = PulsarMessage.readJMSPriority(o2);
return Integer.compare(priority2, priority1);
}
});
} else if (oldQueue
.getClass()
.isAssignableFrom(MessagePriorityGrowableArrayBlockingQueue.class)) {
newQueue = new MessagePriorityGrowableArrayBlockingQueue();
} else {
log.warn(
"Field incomingMessages is not a PriorityBlockingQueue/GrowableArrayBlockingQueue, it is a {}."
+ " We cannot apply priority to the messages in the local buffer.",
oldQueue.getClass().getName());
return;
}
// drain messages that could have been pre-fetched (the Consumer is paused, so this should
// not
// happen)
((BlockingQueue) oldQueue).drainTo(newQueue);
incomingMessages.set(c, newQueue);
if (consumerBase instanceof MultiTopicsConsumerImpl) {
setReceiverQueueSizeForJMSPriority(consumerBase);
}
} catch (Exception err) {
throw new RuntimeException(err);
}
}
private static void setReceiverQueueSizeForJMSPriority(ConsumerBase consumerBase)
throws Exception {
Field consumersField = MultiTopicsConsumerImpl.class.getDeclaredField("consumers");
consumersField.setAccessible(true);
ConcurrentHashMap> consumers =
(ConcurrentHashMap) consumersField.get(consumerBase);
Method setCurrentReceiverQueueSizeMethod =
ConsumerImpl.class.getDeclaredMethod("setCurrentReceiverQueueSize", int.class);
setCurrentReceiverQueueSizeMethod.setAccessible(true);
// set the queue size for each consumer based on the partition index
// we set a higher number to the consumers for the higher priority partitions
// this way the backlog is drained more quickly for the higher priority partitions
int numConsumers = consumers.size();
int sumPriorities =
(numConsumers * (numConsumers + 1)) / 2; // 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10
int receiverQueueSize = consumerBase.getCurrentReceiverQueueSize();
for (ConsumerImpl> consumer : consumers.values()) {
String topic = consumer.getTopic();
int partitionIndex = TopicName.get(topic).getPartitionIndex();
// no need to map exactly the partition index to the priority
int prio = Math.max(partitionIndex, 0);
// the size is proportional to the priority (partition index)
int size = Math.max(1, (prio + 1) * receiverQueueSize / sumPriorities);
log.info("Setting receiverQueueSize={} for {} (to handle JMSPriority)", size, topic);
setCurrentReceiverQueueSizeMethod.invoke(consumer, size);
}
}
public String downloadServerSideFilter(
String fullQualifiedTopicName, String subscriptionName, SubscriptionMode subscriptionMode)
throws JMSException {
if (!isUseServerSideFiltering() || subscriptionMode != SubscriptionMode.Durable) {
return null;
}
log.info(
"downloadServerSideFilter {} {} {}",
fullQualifiedTopicName,
subscriptionName,
subscriptionMode);
long start = System.currentTimeMillis();
while (true) {
try {
Map subscriptionPropertiesFromBroker =
pulsarAdmin
.topics()
.getSubscriptionProperties(fullQualifiedTopicName, subscriptionName);
if (subscriptionPropertiesFromBroker != null) {
log.debug("subscriptionPropertiesFromBroker {}", subscriptionPropertiesFromBroker);
boolean filtering = "true".equals(subscriptionPropertiesFromBroker.get("jms.filtering"));
if (filtering) {
String selectorOnSubscription =
subscriptionPropertiesFromBroker.getOrDefault("jms.selector", "");
if (!selectorOnSubscription.isEmpty()) {
log.info(
"Detected selector {} on Subscription {} on topic {}",
selectorOnSubscription,
subscriptionName,
fullQualifiedTopicName);
return selectorOnSubscription;
}
}
}
return null;
} catch (PulsarAdminException.PreconditionFailedException notReady) {
// special handling for "PreconditionFailedException: Can't find owner for topic
// persistent://xxx/xx/xxxx"
long now = System.currentTimeMillis();
if (now - start > getWaitForServerStartupTimeout()) {
throw Utils.handleException(notReady);
} else {
log.info(
"Temporary error, cannot download server-side filters {}: {}",
fullQualifiedTopicName,
notReady + "");
try {
Thread.sleep(1000);
} catch (InterruptedException interruptedException) {
Thread.currentThread().interrupt();
throw Utils.handleException(notReady);
}
}
} catch (PulsarAdminException err) {
throw Utils.handleException(err);
}
}
}
public List> createReadersForBrowser(
PulsarQueue destination, ConsumerConfiguration overrideConsumerConfiguration)
throws JMSException {
if (destination.isRegExp()) {
try {
String topicName = getPulsarTopicName(destination);
List topicNames =
TopicDiscoveryUtils.discoverTopicsByPattern(topicName, getPulsarClient(), 1000);
log.info("createReadersForBrowser {} - {} - {}", destination, topicName, topicNames);
List> res = new ArrayList<>();
for (String sub : topicNames) {
String queueName = sub + ":" + getQueueSubscriptionName(destination);
PulsarQueue queue = new PulsarQueue(queueName);
res.addAll(createReadersForBrowser(queue, overrideConsumerConfiguration));
}
return res;
} catch (Exception err) {
throw Utils.handleException(err);
}
} else if (destination.isMultiTopic()) {
List> res = new ArrayList<>();
List destinations = destination.getDestinations();
for (PulsarDestination sub : destinations) {
res.addAll(createReadersForBrowser((PulsarQueue) sub, overrideConsumerConfiguration));
}
return res;
} else {
String fullQualifiedTopicName = getPulsarTopicName(destination);
String queueSubscriptionName = getQueueSubscriptionName(destination);
try {
PartitionedTopicMetadata partitionedTopicMetadata =
getPulsarAdmin().topics().getPartitionedTopicMetadata(fullQualifiedTopicName);
List> readers = new ArrayList<>();
if (partitionedTopicMetadata.partitions == 0) {
Reader> readerForBrowserForNonPartitionedTopic =
createReaderForBrowserForNonPartitionedTopic(
queueSubscriptionName,
fullQualifiedTopicName,
overrideConsumerConfiguration,
destination);
readers.add(readerForBrowserForNonPartitionedTopic);
} else {
for (int i = 0; i < partitionedTopicMetadata.partitions; i++) {
String partitionName = fullQualifiedTopicName + "-partition-" + i;
Reader> readerForBrowserForNonPartitionedTopic =
createReaderForBrowserForNonPartitionedTopic(
queueSubscriptionName,
partitionName,
overrideConsumerConfiguration,
destination);
readers.add(readerForBrowserForNonPartitionedTopic);
}
}
return readers;
} catch (PulsarAdminException.NotFoundException err) {
return Collections.emptyList();
} catch (PulsarAdminException err) {
throw Utils.handleException(err);
}
}
}
private Reader> createReaderForBrowserForNonPartitionedTopic(
String queueSubscriptionName,
String fullQualifiedTopicName,
ConsumerConfiguration overrideConsumerConfiguration,
PulsarDestination destination)
throws JMSException {
try {
// peekMessages works only for non-partitioned topics
List> messages =
getPulsarAdmin().topics().peekMessages(fullQualifiedTopicName, queueSubscriptionName, 1);
MessageId seekMessageId;
if (messages.isEmpty()) {
// no more messages
seekMessageId = MessageId.latest;
} else {
seekMessageId = messages.get(0).getMessageId();
}
if (log.isDebugEnabled()) {
log.debug("createBrowser {} at {}", fullQualifiedTopicName, seekMessageId);
}
log.info("createBrowser {} at {}", fullQualifiedTopicName, seekMessageId);
ConsumerConfiguration consumerConfiguration =
getConsumerConfiguration(overrideConsumerConfiguration, destination);
Schema> schema = consumerConfiguration.getConsumerSchema();
if (schema == null) {
schema = Schema.BYTES;
}
Map readerConfiguration =
Utils.deepCopyMap(consumerConfiguration.getConsumerConfiguration());
readerConfiguration.remove("batchIndexAckEnabled");
ReaderBuilder> builder =
pulsarClient
.newReader(schema)
// these properties can be overridden by the configuration
.loadConf(readerConfiguration)
// these properties cannot be overwritten by the configuration
.readerName("jms-queue-browser-" + UUID.randomUUID())
.startMessageId(seekMessageId)
.startMessageIdInclusive()
.topic(fullQualifiedTopicName);
Reader> newReader = builder.create();
readers.add(newReader);
return newReader;
} catch (PulsarClientException | PulsarAdminException err) {
throw Utils.handleException(err);
}
}
public void removeConsumer(Consumer> consumer) {
consumers.remove(consumer);
}
public void removeReader(Reader> reader) {
readers.remove(reader);
}
@SuppressFBWarnings("REC_CATCH_EXCEPTION")
public boolean deleteSubscription(PulsarDestination destination, String name)
throws JMSException {
String systemNamespace = getSystemNamespace();
boolean somethingDone = false;
try {
if (destination != null) {
if (destination.isVirtualDestination()) {
throw new InvalidDestinationException(
"Virtual destinations are not supported for unsubscribe");
}
String fullQualifiedTopicName = getPulsarTopicName(destination);
log.info("deleteSubscription topic {} name {}", fullQualifiedTopicName, name);
try {
pulsarAdmin.topics().deleteSubscription(fullQualifiedTopicName, name, true);
somethingDone = true;
} catch (PulsarAdminException.NotFoundException notFound) {
log.error("Cannot unsubscribe {} from {}: not found", name, fullQualifiedTopicName);
}
}
if (!somethingDone) {
// required for TCK, scan for all subscriptions
List allTopics = pulsarAdmin.topics().getList(systemNamespace);
for (String topic : allTopics) {
if (topic.endsWith(PENDING_ACK_STORE_SUFFIX)) {
// skip Transaction related system topics
log.info("Ignoring system topic {}", topic);
continue;
}
log.info("Scanning topic {}", topic);
List subscriptions;
try {
subscriptions = pulsarAdmin.topics().getSubscriptions(topic);
log.info("Subscriptions {}", subscriptions);
} catch (PulsarAdminException.NotFoundException notFound) {
log.error("Skipping topic {}", topic);
subscriptions = Collections.emptyList();
}
for (String subscription : subscriptions) {
log.info("Found subscription {} ", subscription);
if (subscription.equals(name)) {
log.info("deleteSubscription topic {} name {}", topic, name);
pulsarAdmin.topics().deleteSubscription(topic, name, true);
somethingDone = true;
}
}
}
}
} catch (Exception err) {
throw Utils.handleException(err);
}
return somethingDone;
}
public void registerClientId(String clientID) throws InvalidClientIDException {
log.debug("registerClientId {}, existing {}", clientID, clientIdentifiers);
if (!clientIdentifiers.add(clientID)) {
throw new InvalidClientIDException(
"A connection with this client id '" + clientID + "'is already opened locally");
}
}
public void unregisterConnection(PulsarConnection connection) {
if (connection.clientId != null) {
clientIdentifiers.remove(connection.clientId);
log.debug("unregisterClientId {} {}", connection.clientId, clientIdentifiers);
}
connections.remove(connection);
}
@Override
public QueueConnection createQueueConnection() throws JMSException {
return createConnection();
}
@Override
public QueueConnection createQueueConnection(String s, String s1) throws JMSException {
return createConnection(s, s1);
}
@Override
public TopicConnection createTopicConnection() throws JMSException {
return createConnection();
}
@Override
public TopicConnection createTopicConnection(String s, String s1) throws JMSException {
return createConnection(s, s1);
}
public synchronized boolean isForceDeleteTemporaryDestinations() {
return forceDeleteTemporaryDestinations;
}
public synchronized String getQueueSubscriptionName(PulsarDestination destination)
throws InvalidDestinationException {
String customSubscriptionName = destination.extractSubscriptionName();
if (customSubscriptionName != null) {
return customSubscriptionName;
}
return queueSubscriptionName;
}
public synchronized long getWaitForServerStartupTimeout() {
return waitForServerStartupTimeout;
}
public synchronized SubscriptionType getExclusiveSubscriptionTypeForSimpleConsumers(
Destination destination) {
return useExclusiveSubscriptionsForSimpleConsumers
? SubscriptionType.Exclusive
: destination instanceof Queue ? SubscriptionType.Shared : getTopicSharedSubscriptionType();
}
public synchronized SubscriptionType getTopicSharedSubscriptionType() {
return topicSharedSubscriptionType;
}
public String applySystemNamespace(String destination) {
if (destination == null) {
return null;
}
if (destination.startsWith("persistent://") || destination.startsWith("non-persistent://")) {
return destination;
}
return "persistent://" + getSystemNamespace() + "/" + destination;
}
public boolean isAcknowledgeRejectedMessages() {
return acknowledgeRejectedMessages;
}
public boolean isAllowTemporaryTopicWithoutAdmin() {
return allowTemporaryTopicWithoutAdmin;
}
public synchronized boolean isClosed() {
return closed;
}
private synchronized int getPrecreateQueueSubscriptionConsumerQueueSize(boolean regExp) {
if (regExp) {
return Math.max(precreateQueueSubscriptionConsumerQueueSize, 1);
}
return precreateQueueSubscriptionConsumerQueueSize;
}
private synchronized void writeObject(ObjectOutputStream out) throws IOException {
String serialisedConfiguration = new ObjectMapper().writeValueAsString(configuration);
if (log.isDebugEnabled()) {
log.debug("Serializing this PulsarConnectionFactory as {}", serialisedConfiguration);
}
out.writeUTF(serialisedConfiguration);
}
// this method must not be synchronizes, see RS_READOBJECT_SYNC
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
resetDefaultValues();
String readConfiguration = in.readUTF();
if (log.isDebugEnabled()) {
log.debug("Deserialize configuration as {}", configuration);
}
try {
setJsonConfiguration(readConfiguration);
} catch (Exception err) {
throw new IOException("Cannot decode JSON configuration " + configuration);
}
}
private void setFinalField(String name, Object value) {
try {
Field field = this.getClass().getDeclaredField(name);
boolean accessible = field.isAccessible();
if (!accessible) {
field.setAccessible(true);
Field modifiersField = getModifiersField();
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
}
try {
field.set(this, value);
} finally {
if (!accessible) {
field.setAccessible(false);
Field modifiersField = getModifiersField();
modifiersField.setInt(field, field.getModifiers() | Modifier.FINAL);
}
}
} catch (Exception err) {
log.error("Error while setting final field {}", name, err);
throw new RuntimeException(err);
}
}
private static Field getModifiersField()
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
Method getDeclaredFields0 = Class.class.getDeclaredMethod("getDeclaredFields0", boolean.class);
getDeclaredFields0.setAccessible(true);
Field[] fields = (Field[]) getDeclaredFields0.invoke(Field.class, false);
Field modifiersField = null;
for (Field each : fields) {
if ("modifiers".equals(each.getName())) {
modifiersField = each;
break;
}
}
if (modifiersField == null) {
throw new RuntimeException("Cannot find modifiers field");
}
modifiersField.setAccessible(true);
return modifiersField;
}
private synchronized void resetDefaultValues() {
if (initialized) {
throw new java.lang.IllegalStateException();
}
// final fields
setFinalField("producers", new ConcurrentHashMap<>());
setFinalField("connections", Collections.synchronizedSet(new HashSet<>()));
setFinalField("consumers", new CopyOnWriteArrayList<>());
setFinalField("readers", new CopyOnWriteArrayList<>());
this.initialized = false;
this.closed = false;
}
private void refreshServerSideSelectors() {
connections.forEach(
c -> {
c.refreshServerSideSelectors();
});
}
/**
* Access to the high level Admin JMS API
*
* @return the handle to the Admin API.
*/
public JMSAdmin getAdmin() {
return new PulsarJMSAdminImpl(this);
}
/**
* Internal method to ensure the the PulsarClient is started
*
* @throws JMSException
*/
PulsarClient ensureClient() throws JMSException {
createConnection().close();
if (pulsarClient == null) {
throw new IllegalStateException(
"This PulsarConnectionFactory is not configured to bootstrap a PulsarClient");
}
return pulsarClient;
}
PulsarAdmin ensurePulsarAdmin() throws JMSException {
createConnection().close();
if (pulsarAdmin == null) {
throw new IllegalStateException(
"This PulsarConnectionFactory is not configured to bootstrap a PulsarAdmin");
}
return pulsarAdmin;
}
public synchronized int getSessionListenersThreads() {
return sessionListenersThreads;
}
public synchronized ScheduledExecutorService getSessionListenersThreadPool() {
if (sessionListenersThreads > 0 && sessionListenersThreadPool == null) {
log.info(
"{} Starting MessageListeners thread pool, size is jms.sessionListenersThreads={}",
this,
sessionListenersThreads);
sessionListenersThreadPool =
new ScheduledThreadPoolExecutor(
sessionListenersThreads,
new SessionListenersThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
}
return sessionListenersThreadPool;
}
public synchronized boolean isMaxMessagesLimitsParallelism() {
return maxMessagesLimitsParallelism;
}
public synchronized int getConnectionConsumerStopTimeout() {
return connectionConsumerStopTimeout;
}
private static class SessionListenersThreadFactory implements ThreadFactory {
private static final AtomicInteger sessionThreadNumber = new AtomicInteger();
public SessionListenersThreadFactory() {}
@Override
public Thread newThread(Runnable r) {
String name = "jms-session-thread-" + sessionThreadNumber.getAndIncrement();
Thread thread = new Thread(r, name);
thread.setDaemon(true);
thread.setUncaughtExceptionHandler(
new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
log.error("Internal error in JMS Session thread {}", t, e);
}
});
return thread;
}
}
}