diff --git a/adapter-pulsar/pom.xml b/adapter-pulsar/pom.xml
index 2428c54fe..5ed541f12 100644
--- a/adapter-pulsar/pom.xml
+++ b/adapter-pulsar/pom.xml
@@ -62,6 +62,13 @@
${pulsar.version}
+
+
+ org.apache.commons
+ commons-lang3
+ 3.12.0
+
+
commons-beanutils
@@ -82,13 +89,6 @@
avro1.11.1
-
-
-
- org.apache.commons
- commons-lang3
- 3.12.0
-
diff --git a/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/util/PulsarClientConf.java b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/util/PulsarClientConf.java
index 4af681509..fc6966f4a 100644
--- a/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/util/PulsarClientConf.java
+++ b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/util/PulsarClientConf.java
@@ -73,7 +73,7 @@ public class PulsarClientConf {
// Convert the raw configuration map () to the required map ()
producerConfMapTgt.putAll(PulsarConfConverter.convertStdRawProducerConf(producerConfMapRaw));
consumerConfMapTgt.putAll(PulsarConfConverter.convertStdRawConsumerConf(consumerConfMapRaw));
- // TODO: Reader API is not disabled at the moment. Revisit when needed
+ // TODO: Reader API is not enabled at the moment. Revisit when needed
}
diff --git a/adapter-s4j/pom.xml b/adapter-s4j/pom.xml
new file mode 100644
index 000000000..41bb02db6
--- /dev/null
+++ b/adapter-s4j/pom.xml
@@ -0,0 +1,89 @@
+
+ 4.0.0
+
+ adapter-s4j
+ jar
+
+
+ mvn-defaults
+ io.nosqlbench
+ 4.17.31-SNAPSHOT
+ ../mvn-defaults
+
+
+ ${project.artifactId}
+
+ A Starlight for JMS driver for nosqlbench. This provides the ability to inject synthetic data
+ into a pulsar system via JMS 2.0 compatible APIs.
+
+ NOTE: this is JMS compatible driver from DataStax that allows using a Pulsar cluster
+ as the potential JMS Destination
+
+
+
+ 3.2.0
+
+
+
+
+
+ kr.motd.maven
+ os-maven-plugin
+ 1.7.1
+
+
+
+
+
+
+
+ io.nosqlbench
+ engine-api
+ 4.17.31-SNAPSHOT
+
+
+
+ io.nosqlbench
+ adapters-api
+ 4.17.31-SNAPSHOT
+
+
+
+
+ com.datastax.oss
+ pulsar-jms-all
+ ${s4j.version}
+
+
+
+
+ org.apache.commons
+ commons-lang3
+ 3.12.0
+
+
+
+
+ commons-beanutils
+ commons-beanutils
+ 1.9.4
+
+
+
+
+ org.apache.commons
+ commons-configuration2
+ 2.8.0
+
+
+
+
+ org.conscrypt
+ conscrypt-openjdk
+ 2.5.2
+ ${os.detected.classifier}
+
+
+
+
+
diff --git a/adapter-s4j/src/main/java/io/nosqlbench/adapter/s4j/S4JDriverAdapter.java b/adapter-s4j/src/main/java/io/nosqlbench/adapter/s4j/S4JDriverAdapter.java
new file mode 100644
index 000000000..49527f9c0
--- /dev/null
+++ b/adapter-s4j/src/main/java/io/nosqlbench/adapter/s4j/S4JDriverAdapter.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2022 nosqlbench
+ *
+ * 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 io.nosqlbench.adapter.s4j;
+
+import io.nosqlbench.adapter.s4j.ops.S4JOp;
+import io.nosqlbench.api.config.standard.NBConfigModel;
+import io.nosqlbench.api.config.standard.NBConfiguration;
+import io.nosqlbench.engine.api.activityimpl.OpMapper;
+import io.nosqlbench.engine.api.activityimpl.uniform.BaseDriverAdapter;
+import io.nosqlbench.engine.api.activityimpl.uniform.DriverAdapter;
+import io.nosqlbench.engine.api.activityimpl.uniform.DriverSpaceCache;
+import io.nosqlbench.nb.annotations.Service;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.function.Function;
+
+@Service(value = DriverAdapter.class, selector = "s4j")
+public class S4JDriverAdapter extends BaseDriverAdapter {
+ private final static Logger logger = LogManager.getLogger(S4JDriverAdapter.class);
+
+ @Override
+ public OpMapper getOpMapper() {
+ DriverSpaceCache extends S4JSpace> spaceCache = getSpaceCache();
+ NBConfiguration adapterConfig = getConfiguration();
+ return new S4JOpMapper(this, adapterConfig, spaceCache);
+ }
+
+ @Override
+ public Function getSpaceInitializer(NBConfiguration cfg) {
+ return (s) -> new S4JSpace(s, cfg);
+ }
+
+ @Override
+ public NBConfigModel getConfigModel() {
+ return super.getConfigModel().add(S4JSpace.getConfigModel());
+ }
+}
diff --git a/adapter-s4j/src/main/java/io/nosqlbench/adapter/s4j/S4JOpMapper.java b/adapter-s4j/src/main/java/io/nosqlbench/adapter/s4j/S4JOpMapper.java
new file mode 100644
index 000000000..95b39ec57
--- /dev/null
+++ b/adapter-s4j/src/main/java/io/nosqlbench/adapter/s4j/S4JOpMapper.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2022 nosqlbench
+ *
+ * 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 io.nosqlbench.adapter.s4j;
+
+import io.nosqlbench.adapter.s4j.dispensers.MessageConsumerOpDispenser;
+import io.nosqlbench.adapter.s4j.dispensers.MessageProducerOpDispenser;
+import io.nosqlbench.adapter.s4j.ops.S4JOp;
+import io.nosqlbench.api.config.standard.NBConfiguration;
+import io.nosqlbench.engine.api.activityimpl.OpDispenser;
+import io.nosqlbench.engine.api.activityimpl.OpMapper;
+import io.nosqlbench.engine.api.activityimpl.uniform.DriverAdapter;
+import io.nosqlbench.engine.api.activityimpl.uniform.DriverSpaceCache;
+import io.nosqlbench.engine.api.templating.ParsedOp;
+import io.nosqlbench.engine.api.templating.TypeAndTarget;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+public class S4JOpMapper implements OpMapper {
+
+ private final static Logger logger = LogManager.getLogger(S4JOpMapper.class);
+
+ private final NBConfiguration cfg;
+ private final DriverSpaceCache extends S4JSpace> spaceCache;
+ private final DriverAdapter adapter;
+
+ public S4JOpMapper(DriverAdapter adapter, NBConfiguration cfg, DriverSpaceCache extends S4JSpace> spaceCache) {
+ this.cfg = cfg;
+ this.spaceCache = spaceCache;
+ this.adapter = adapter;
+ }
+
+ @Override
+ public OpDispenser extends S4JOp> apply(ParsedOp op) {
+ String spaceName = op.getStaticConfigOr("space", "default");
+ S4JSpace s4jSpace = spaceCache.get(spaceName);
+
+ /*
+ * If the user provides a body element, then they want to provide the JSON or
+ * a data structure that can be converted into JSON, bypassing any further
+ * specialized type-checking or op-type specific features
+ */
+ if (op.isDefined("body")) {
+ throw new RuntimeException("This mode is reserved for later. Do not use the 'body' op field.");
+ }
+ else {
+ TypeAndTarget opType = op.getTypeAndTarget(S4JOpType.class, String.class);
+
+ return switch (opType.enumId) {
+ case MessageProduce ->
+ new MessageProducerOpDispenser(adapter, op, opType.targetFunction, s4jSpace);
+ case MessageConsume ->
+ new MessageConsumerOpDispenser(adapter, op, opType.targetFunction, s4jSpace);
+ };
+ }
+ }
+
+}
diff --git a/adapter-s4j/src/main/java/io/nosqlbench/adapter/s4j/S4JOpType.java b/adapter-s4j/src/main/java/io/nosqlbench/adapter/s4j/S4JOpType.java
new file mode 100644
index 000000000..2e4f1144b
--- /dev/null
+++ b/adapter-s4j/src/main/java/io/nosqlbench/adapter/s4j/S4JOpType.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2022 nosqlbench
+ *
+ * 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 io.nosqlbench.adapter.s4j;
+
+public enum S4JOpType {
+ // publishing/sending messages to a JMS queue or a topic
+ MessageProduce,
+ // consuming/receiving messages from a JMS queue or a topic
+ // for a topic, it can be:
+ // - non-durable, non-shared
+ // - durable, non-shared
+ // - non-durable, shared
+ // - durable, shared
+ MessageConsume;
+}
+
+
diff --git a/adapter-s4j/src/main/java/io/nosqlbench/adapter/s4j/S4JSpace.java b/adapter-s4j/src/main/java/io/nosqlbench/adapter/s4j/S4JSpace.java
new file mode 100644
index 000000000..8c552912f
--- /dev/null
+++ b/adapter-s4j/src/main/java/io/nosqlbench/adapter/s4j/S4JSpace.java
@@ -0,0 +1,364 @@
+/*
+ * Copyright (c) 2022 nosqlbench
+ *
+ * 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 io.nosqlbench.adapter.s4j;
+
+import com.datastax.oss.pulsar.jms.PulsarConnectionFactory;
+import io.nosqlbench.adapter.s4j.exception.S4JAdapterInvalidParamException;
+import io.nosqlbench.adapter.s4j.exception.S4JAdapterUnexpectedException;
+import io.nosqlbench.adapter.s4j.util.*;
+import io.nosqlbench.api.config.standard.ConfigModel;
+import io.nosqlbench.api.config.standard.NBConfigModel;
+import io.nosqlbench.api.config.standard.NBConfiguration;
+import io.nosqlbench.api.config.standard.Param;
+import org.apache.commons.lang3.RandomUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import javax.jms.*;
+import java.util.Base64;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
+
+public class S4JSpace implements AutoCloseable {
+
+ private final static Logger logger = LogManager.getLogger(S4JSpace.class);
+
+ private final String spaceName;
+ private final NBConfiguration cfg;
+
+ // - Each S4J space currently represents a number of JMS connections (\"num_conn\" NB CLI parameter);
+ // - JMS connection can have a number of JMS sessions (\"num_session\" NB CLI parameter).
+ // - Each JMS session has its own sets of JMS destinations, producers, consumers, etc.
+ private final ConcurrentHashMap connLvlJmsContexts = new ConcurrentHashMap<>();
+ private final ConcurrentHashMap sessionLvlJmsContexts = new ConcurrentHashMap<>();
+
+ private final String pulsarSvcUrl;
+ private final String webSvcUrl;
+ private final String s4jClientConfFileName;
+ private S4JClientConf s4JClientConf;
+ private final int sessionMode;
+
+ // Whether to do strict error handling while sending/receiving messages
+ // - Yes: any error returned from the Pulsar server while doing message receiving/sending will trigger NB execution stop
+ // - No: pause the current thread that received the error message for 1 second and then continue processing
+ private boolean strictMsgErrorHandling;
+
+ // Maximum time length to execute S4J operations (e.g. message send or consume)
+ // - when NB execution passes this threshold, it is simply NoOp
+ // - 0 means no maximum time constraint. S4JOp is always executed until NB execution cycle finishes
+ private long maxS4JOpTimeInSec;
+ private long s4JActivityStartTimeMills;
+
+ // Whether to keep track of the received message count, which includes
+ // - total received message count
+ // - received null message count (only relevant when non-blocking message receiving is used)
+ // By default, this setting is disabled
+ private boolean trackingMsgRecvCnt;
+
+ // How many JMS connections per NB S4J execution
+ private int maxNumConn;
+ // How many sessions per JMS connection
+ private int maxNumSessionPerConn;
+
+ // Total number of acknowledgement received
+ // - this can apply to both message production and consumption
+ // - for message consumption, this only applies to non-null messages received (which is for async API)
+ private final AtomicLong totalOpResponseCnt = new AtomicLong(0);
+ // Total number of null messages received
+ // - only applicable to message consumption
+ private final AtomicLong nullMsgRecvCnt = new AtomicLong(0);
+
+ // Keep track the transaction count per thread
+ private final ThreadLocal txnBatchTrackingCnt = ThreadLocal.withInitial(() -> 0);
+
+ // Represents the JMS connection
+ private PulsarConnectionFactory s4jConnFactory;
+
+ private long totalCycleNum;
+
+ public S4JSpace(String spaceName, NBConfiguration cfg) {
+ this.spaceName = spaceName;
+ this.cfg = cfg;
+
+ this.pulsarSvcUrl = cfg.get("service_url");
+ this.webSvcUrl = cfg.get("web_url");
+ this.maxNumConn= cfg.getOrDefault("num_conn", Integer.valueOf(1));
+ this.maxNumSessionPerConn = cfg.getOrDefault("num_session", Integer.valueOf(1));
+ this.maxS4JOpTimeInSec= cfg.getOrDefault("max_s4jop_time", Long.valueOf(0));
+ this.trackingMsgRecvCnt=cfg.getOrDefault("track_msg_cnt", Boolean.FALSE);
+ this.strictMsgErrorHandling = cfg.getOrDefault("strict_msg_error_handling", Boolean.FALSE);
+ this.s4jClientConfFileName = cfg.get("config");
+ this.sessionMode = S4JAdapterUtil.getSessionModeFromStr(cfg.get("session_mode"));
+ this.s4JClientConf = new S4JClientConf(pulsarSvcUrl, webSvcUrl, s4jClientConfFileName);
+
+ this.initializeSpace(s4JClientConf);
+ }
+
+ @Override
+ public void close() {
+ shutdownSpace();
+ }
+
+ public static NBConfigModel getConfigModel() {
+ return ConfigModel.of(S4JSpace.class)
+ .add(Param.defaultTo("service_url", "pulsar://localhost:6650")
+ .setDescription("Pulsar broker service URL."))
+ .add(Param.defaultTo("web_url", "http://localhost:8080")
+ .setDescription("Pulsar web service URL."))
+ .add(Param.defaultTo("config", "config.properties")
+ .setDescription("Pulsar client connection configuration property file."))
+ .add(Param.defaultTo("num_conn", 1)
+ .setDescription("Number of JMS connections"))
+ .add(Param.defaultTo("num_session", 1)
+ .setDescription("Number of JMS sessions per JMS connection"))
+ .add(Param.defaultTo("max_s4jop_time", 0)
+ .setDescription("Maximum time (in seconds) to run NB S4J testing scenario."))
+ .add(Param.defaultTo("track_msg_cnt", false)
+ .setDescription("Whether to keep track of message count(s)"))
+ .add(Param.defaultTo("session_mode", "")
+ .setDescription("JMS session mode"))
+ .add(Param.defaultTo("strict_msg_error_handling", false)
+ .setDescription("Whether to do strict error handling which is to stop NB S4J execution."))
+ .asReadOnly();
+ }
+
+ public ConcurrentHashMap getConnLvlJmsContexts() {
+ return connLvlJmsContexts;
+ }
+
+ public ConcurrentHashMap getSessionLvlJmsContexts() {
+ return sessionLvlJmsContexts;
+ }
+
+ public long getS4JActivityStartTimeMills() { return this.s4JActivityStartTimeMills; }
+ public void setS4JActivityStartTimeMills(long startTime) { this.s4JActivityStartTimeMills = startTime; }
+
+ public long getMaxS4JOpTimeInSec() { return this.maxS4JOpTimeInSec; }
+
+ public int getSessionMode() { return sessionMode; }
+
+ public String getS4jClientConfFileName() { return s4jClientConfFileName; }
+ public S4JClientConf getS4JClientConf() { return s4JClientConf; }
+
+ public boolean isTrackingMsgRecvCnt() { return trackingMsgRecvCnt; }
+
+ public int getMaxNumSessionPerConn() { return this.maxNumSessionPerConn; }
+ public int getMaxNumConn() { return this.maxNumConn; }
+
+ public boolean isStrictMsgErrorHandling() { return this.strictMsgErrorHandling; }
+
+ public int getTxnBatchTrackingCnt() { return txnBatchTrackingCnt.get(); }
+ public void incTxnBatchTrackingCnt() {
+ int curVal = getTxnBatchTrackingCnt();
+ txnBatchTrackingCnt.set(curVal + 1);
+ }
+
+ public long getTotalOpResponseCnt() { return totalOpResponseCnt.get();}
+ public long incTotalOpResponseCnt() { return totalOpResponseCnt.incrementAndGet();}
+ public void resetTotalOpResponseCnt() { totalOpResponseCnt.set(0); }
+
+ public long getTotalNullMsgRecvdCnt() { return nullMsgRecvCnt.get();}
+ public void resetTotalNullMsgRecvdCnt() { nullMsgRecvCnt.set(0); }
+
+ public long incTotalNullMsgRecvdCnt() { return nullMsgRecvCnt.incrementAndGet(); }
+
+ public PulsarConnectionFactory getS4jConnFactory() { return s4jConnFactory; }
+
+ public long getTotalCycleNum() { return totalCycleNum; }
+ public void setTotalCycleNum(long cycleNum) { totalCycleNum = cycleNum; }
+
+ public void initializeSpace(S4JClientConf s4JClientConnInfo) {
+ if (s4jConnFactory == null) {
+ Map cfgMap;
+ try {
+ cfgMap = s4JClientConnInfo.getS4jConfObjMap();
+ s4jConnFactory = new PulsarConnectionFactory(cfgMap);
+
+ for (int i=0; i {
+ if (logger.isDebugEnabled()) {
+ logger.error("onException::Unexpected JMS error happened:" + e);
+ }
+ });
+
+ connLvlJmsContexts.put(connLvlJmsConnContextIdStr, jmsConnContext);
+
+ if (logger.isDebugEnabled()) {
+ logger.debug("[Connection level JMSContext] {} -- {}",
+ Thread.currentThread().getName(),
+ jmsConnContext );
+ }
+ }
+ }
+ catch (JMSRuntimeException e) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("[ERROR] Unable to initialize JMS connection factory with the following configuration parameters: {}", s4JClientConnInfo.toString());
+ }
+ throw new S4JAdapterUnexpectedException("Unable to initialize JMS connection factory with the following error message: " + e.getCause());
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public void shutdownSpace() {
+ long shutdownStartTimeMills = System.currentTimeMillis();
+
+ try {
+ waitUntilAllOpFinished(shutdownStartTimeMills);
+
+ this.txnBatchTrackingCnt.remove();
+
+ for (S4JJMSContextWrapper s4JJMSContextWrapper : sessionLvlJmsContexts.values()) {
+ if (s4JJMSContextWrapper != null) {
+ if (s4JJMSContextWrapper.isTransactedMode()) {
+ s4JJMSContextWrapper.getJmsContext().rollback();
+ }
+ s4JJMSContextWrapper.close();
+ }
+ }
+
+ for (JMSContext jmsContext : connLvlJmsContexts.values()) {
+ if (jmsContext != null) jmsContext.close();
+ }
+
+ s4jConnFactory.close();
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ throw new S4JAdapterUnexpectedException("Unexpected error when shutting down NB S4J space.");
+ }
+ }
+
+ // When completing NB execution, don't shut down right away because otherwise, async operation processing may fail.
+ // Instead, shut down when either one of the following condition is satisfied
+ // 1) the total number of the received operation response is the same as the total number of operations being executed;
+ // 2) time has passed for 10 seconds
+ private void waitUntilAllOpFinished(long shutdownStartTimeMills) {
+ long totalCycleNum = getTotalCycleNum();
+ long totalResponseCnt = 0;
+ long totalNullMsgCnt = 0;
+ long timeElapsedMills;
+
+ boolean trackingMsgCnt = isTrackingMsgRecvCnt();
+ boolean continueChk;
+
+ do {
+ S4JAdapterUtil.pauseCurThreadExec(1);
+
+ long curTimeMills = System.currentTimeMillis();
+ timeElapsedMills = curTimeMills - shutdownStartTimeMills;
+ continueChk = (timeElapsedMills <= 10000);
+
+ if (trackingMsgCnt) {
+ totalResponseCnt = this.getTotalOpResponseCnt();
+ totalNullMsgCnt = this.getTotalNullMsgRecvdCnt();
+ continueChk = continueChk && (totalResponseCnt < totalCycleNum);
+ }
+
+ if (logger.isTraceEnabled()) {
+ logger.trace(
+ buildExecSummaryString(trackingMsgCnt, timeElapsedMills, totalResponseCnt, totalNullMsgCnt));
+ }
+ } while (continueChk);
+
+ logger.info(
+ buildExecSummaryString(trackingMsgCnt, timeElapsedMills, totalResponseCnt, totalNullMsgCnt));
+ }
+
+ private String buildExecSummaryString(
+ boolean trackingMsgCnt,
+ long timeElapsedMills,
+ long totalResponseCnt,
+ long totalNullMsgCnt)
+ {
+ StringBuilder stringBuilder = new StringBuilder();
+ stringBuilder
+ .append("shutdownSpace::waitUntilAllOpFinished -- ")
+ .append("shutdown time elapsed: ").append(timeElapsedMills).append("ms; ");
+
+ if (trackingMsgCnt) {
+ stringBuilder.append("response received: ").append(totalResponseCnt).append("; ");
+ stringBuilder.append("null msg received: ").append(totalNullMsgCnt).append("; ");
+ }
+
+ return stringBuilder.toString();
+ }
+
+ public void processMsgAck(JMSContext jmsContext, Message message, float msgAckRatio, int slowAckInSec) throws JMSException {
+ int jmsSessionMode = jmsContext.getSessionMode();
+
+ if ((jmsSessionMode != Session.AUTO_ACKNOWLEDGE) &&
+ (jmsSessionMode != Session.SESSION_TRANSACTED)) {
+ float rndVal = RandomUtils.nextFloat(0, 1);
+ if (rndVal < msgAckRatio) {
+ S4JAdapterUtil.pauseCurThreadExec(slowAckInSec);
+ message.acknowledge();
+ }
+ }
+ }
+
+ public String getConnLvlJmsContextIdentifier(int jmsConnSeqNum) {
+ return S4JAdapterUtil.buildCacheKey(
+ this.spaceName,
+ StringUtils.join("conn-", jmsConnSeqNum));
+ }
+
+ public String getSessionLvlJmsContextIdentifier(int jmsConnSeqNum, int jmsSessionSeqNum) {
+ return S4JAdapterUtil.buildCacheKey(
+ this.spaceName,
+ StringUtils.join("conn-", jmsConnSeqNum),
+ StringUtils.join("session-", jmsSessionSeqNum));
+ }
+
+ // Create JMSContext that represents a new JMS connection
+ public JMSContext getOrCreateConnLvlJMSContext(
+ PulsarConnectionFactory s4jConnFactory,
+ S4JClientConf s4JClientConf,
+ int sessionMode)
+ {
+ boolean useCredentialsEnable = S4JAdapterUtil.isUseCredentialsEnabled(s4JClientConf);
+ JMSContext jmsConnContext;
+
+ if (!useCredentialsEnable)
+ jmsConnContext = s4jConnFactory.createContext(sessionMode);
+ else {
+ String userName = S4JAdapterUtil.getCredentialUserName(s4JClientConf);
+ String passWord = S4JAdapterUtil.getCredentialPassword(s4JClientConf);
+
+ // Password must be in "token:" format
+ if (! StringUtils.startsWith(passWord, "token:")) {
+ throw new S4JAdapterInvalidParamException(
+ "When 'jms.useCredentialsFromCreateConnection' is enabled, " +
+ "the provided password must be in format 'token: ");
+ }
+
+ jmsConnContext = s4jConnFactory.createContext(userName, passWord, sessionMode);
+ }
+
+ return jmsConnContext;
+ }
+}
diff --git a/adapter-s4j/src/main/java/io/nosqlbench/adapter/s4j/dispensers/MessageConsumerOpDispenser.java b/adapter-s4j/src/main/java/io/nosqlbench/adapter/s4j/dispensers/MessageConsumerOpDispenser.java
new file mode 100644
index 000000000..b71e9ad4a
--- /dev/null
+++ b/adapter-s4j/src/main/java/io/nosqlbench/adapter/s4j/dispensers/MessageConsumerOpDispenser.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (c) 2022 nosqlbench
+ *
+ * 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 io.nosqlbench.adapter.s4j.dispensers;
+
+import io.nosqlbench.adapter.s4j.S4JSpace;
+import io.nosqlbench.adapter.s4j.ops.MessageConsumerOp;
+import io.nosqlbench.adapter.s4j.util.S4JAdapterUtil;
+import io.nosqlbench.adapter.s4j.util.S4JJMSContextWrapper;
+import io.nosqlbench.engine.api.activityimpl.uniform.DriverAdapter;
+import io.nosqlbench.engine.api.templating.ParsedOp;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import javax.jms.*;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.LongFunction;
+
+public class MessageConsumerOpDispenser extends S4JBaseOpDispenser {
+
+ private final static Logger logger = LogManager.getLogger("MessageConsumerOpDispenser");
+
+ // Doc-level parameter: blocking_msg_recv (default: false)
+ protected final boolean blockingMsgRecv;
+ // Doc-level parameter: shared_topic (default: false)
+ // - only applicable to Topic as the destination type
+ protected final boolean sharedTopic;
+ // Doc-level parameter: durable_topic (default: false)
+ // - only applicable to Topic as the destination type
+ protected final boolean durableTopic;
+ // default value: false
+ private final boolean noLocal;
+ // default value: 0
+ // value <= 0 : no timeout
+ private final int readTimeout;
+ // default value: false
+ private final boolean recvNoWait;
+ // default value: 1.0 (all received messages are acknowledged)
+ // value must be between 0 and 1 (inclusive)
+ private final float msgAckRatio;
+ // default value: 0
+ // value <= 0 : no slow message ack
+ private final int slowAckInSec;
+ private final LongFunction subNameStrFunc;
+ private final LongFunction localMsgSelectorFunc;
+
+ // Generally the consumer related configurations can be set in the global "config.properties" file,
+ // which can be applied to many testing scenarios.
+ // Setting them here will allow scenario-specific customer configurations. At the moment, only the
+ // DLT related settings are supported
+ private final Map combinedConsumerConfigObjMap = new HashMap<>();
+
+
+ public MessageConsumerOpDispenser(DriverAdapter adapter,
+ ParsedOp op,
+ LongFunction tgtNameFunc,
+ S4JSpace s4jSpace) {
+ super(adapter, op, tgtNameFunc, s4jSpace);
+
+ this.blockingMsgRecv =
+ parsedOp.getStaticConfigOr(S4JAdapterUtil.DOC_LEVEL_PARAMS.BLOCKING_MSG_RECV.label, Boolean.FALSE);
+ this.sharedTopic =
+ parsedOp.getStaticConfigOr(S4JAdapterUtil.DOC_LEVEL_PARAMS.SHARED_TOPIC.label, Boolean.FALSE);
+ this.durableTopic =
+ parsedOp.getStaticConfigOr(S4JAdapterUtil.DOC_LEVEL_PARAMS.DURABLE_TOPIC.label, Boolean.FALSE);
+ this.noLocal =
+ parsedOp.getStaticConfigOr("no_local", Boolean.FALSE);
+ this.readTimeout =
+ parsedOp.getStaticConfigOr("read_timeout", Integer.valueOf(0));
+ this.recvNoWait =
+ parsedOp.getStaticConfigOr("no_wait", Boolean.FALSE);
+ this.msgAckRatio =
+ parsedOp.getStaticConfigOr("msg_ack_ratio", Float.valueOf(1.0f));
+ this.slowAckInSec =
+ parsedOp.getStaticConfigOr("slow_ack_in_sec", Integer.valueOf(0));
+ this.subNameStrFunc =
+ lookupMandtoryStrOpValueFunc("subscription_name");
+ this.localMsgSelectorFunc =
+ lookupOptionalStrOpValueFunc("msg_selector");
+
+ String[] stmtLvlConsumerConfKeyNameList = {
+ "consumer.ackTimeoutMillis",
+ "consumer.deadLetterPolicy",
+ "consumer.negativeAckRedeliveryBackoff",
+ "consumer.ackTimeoutRedeliveryBackoff"};
+ HashMap stmtLvlConsumerConfRawMap = new HashMap<>();
+ for (String confKey : stmtLvlConsumerConfKeyNameList ) {
+ String confVal = parsedOp.getStaticConfigOr(confKey, "");
+ stmtLvlConsumerConfRawMap.put(
+ StringUtils.substringAfter(confKey, "consumer."),
+ confVal);
+ }
+
+ this.combinedConsumerConfigObjMap.putAll(
+ s4jSpace.getS4JClientConf().mergeExtraConsumerConfig(stmtLvlConsumerConfRawMap));
+ }
+
+ @Override
+ public MessageConsumerOp apply(long cycle) {
+ S4JJMSContextWrapper s4JJMSContextWrapper =
+ getOrCreateS4jJmsContextWrapper(cycle, this.combinedConsumerConfigObjMap);
+ JMSContext jmsContext = s4JJMSContextWrapper.getJmsContext();
+ boolean commitTransact = !super.commitTransaction(txnBatchNum, jmsContext.getSessionMode(), cycle);
+
+ Destination destination;
+ try {
+ destination = getOrCreateJmsDestination(
+ s4JJMSContextWrapper, temporaryDest, destType, destNameStrFunc.apply(cycle));
+ }
+ catch (JMSRuntimeException jmsRuntimeException) {
+ throw new RuntimeException("Unable to create the JMS destination!");
+ }
+
+ JMSConsumer jmsConsumer;
+ try {
+ jmsConsumer = getOrCreateJmsConsumer(
+ s4JJMSContextWrapper,
+ destination,
+ destType,
+ subNameStrFunc.apply(cycle),
+ localMsgSelectorFunc.apply(cycle),
+ msgAckRatio,
+ noLocal,
+ durableTopic,
+ sharedTopic,
+ asyncAPI,
+ slowAckInSec);
+ }
+ catch (JMSException jmsException) {
+ throw new RuntimeException("Unable to create the JMS consumer!");
+ }
+
+ return new MessageConsumerOp(
+ s4jAdapterMetrics,
+ s4jSpace,
+ jmsContext,
+ destination,
+ asyncAPI,
+ commitTransact,
+ jmsConsumer,
+ blockingMsgRecv,
+ msgAckRatio,
+ readTimeout,
+ recvNoWait,
+ slowAckInSec);
+ }
+}
diff --git a/adapter-s4j/src/main/java/io/nosqlbench/adapter/s4j/dispensers/MessageProducerOpDispenser.java b/adapter-s4j/src/main/java/io/nosqlbench/adapter/s4j/dispensers/MessageProducerOpDispenser.java
new file mode 100644
index 000000000..8c2e5e1b7
--- /dev/null
+++ b/adapter-s4j/src/main/java/io/nosqlbench/adapter/s4j/dispensers/MessageProducerOpDispenser.java
@@ -0,0 +1,358 @@
+/*
+ * Copyright (c) 2022 nosqlbench
+ *
+ * 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 io.nosqlbench.adapter.s4j.dispensers;
+
+import io.nosqlbench.adapter.s4j.S4JSpace;
+import io.nosqlbench.adapter.s4j.exception.S4JAdapterInvalidParamException;
+import io.nosqlbench.adapter.s4j.exception.S4JAdapterUnexpectedException;
+import io.nosqlbench.adapter.s4j.ops.MessageProducerOp;
+import io.nosqlbench.adapter.s4j.util.S4JAdapterUtil;
+import io.nosqlbench.adapter.s4j.util.S4JJMSContextWrapper;
+import io.nosqlbench.engine.api.activityimpl.uniform.DriverAdapter;
+import io.nosqlbench.engine.api.templating.ParsedOp;
+import org.apache.commons.lang3.BooleanUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import javax.jms.*;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.LongFunction;
+public class MessageProducerOpDispenser extends S4JBaseOpDispenser {
+
+ private final static Logger logger = LogManager.getLogger("MessageProducerOpDispenser");
+
+ public static final String MSG_HEADER_OP_PARAM = "msg_header";
+ public static final String MSG_PROP_OP_PARAM = "msg_property";
+ public static final String MSG_BODY_OP_PARAM = "msg_body";
+ public static final String MSG_TYPE_OP_PARAM = "msg_type";
+
+ private final LongFunction msgHeaderRawJsonStrFunc;
+ private final LongFunction msgPropRawJsonStrFunc;
+ private final LongFunction msgBodyRawJsonStrFunc;
+ private final LongFunction msgTypeFunc;
+
+ public MessageProducerOpDispenser(DriverAdapter adapter,
+ ParsedOp op,
+ LongFunction tgtNameFunc,
+ S4JSpace s4jSpace) {
+ super(adapter, op, tgtNameFunc, s4jSpace);
+
+ this.msgHeaderRawJsonStrFunc = lookupOptionalStrOpValueFunc(MSG_HEADER_OP_PARAM);
+ this.msgPropRawJsonStrFunc = lookupOptionalStrOpValueFunc(MSG_PROP_OP_PARAM);
+ this.msgBodyRawJsonStrFunc = lookupMandtoryStrOpValueFunc(MSG_BODY_OP_PARAM);
+ this.msgTypeFunc = lookupMandtoryStrOpValueFunc(MSG_TYPE_OP_PARAM);
+ }
+
+ private Message createAndSetMessagePayload(
+ S4JJMSContextWrapper s4JJMSContextWrapper,
+ String msgType, String msgBodyRawJsonStr) throws JMSException
+ {
+ Message message;
+ int messageSize = 0;
+
+ JMSContext jmsContext = s4JJMSContextWrapper.getJmsContext();
+
+ if (StringUtils.equalsIgnoreCase(msgType, S4JAdapterUtil.JMS_MESSAGE_TYPES.TEXT.label)) {
+ message = jmsContext.createTextMessage();
+ ((TextMessage) message).setText(msgBodyRawJsonStr);
+ messageSize = msgBodyRawJsonStr.length();
+ } else if (StringUtils.equalsIgnoreCase(msgType, S4JAdapterUtil.JMS_MESSAGE_TYPES.MAP.label)) {
+ message = jmsContext.createMapMessage();
+
+ // The message body json string must be in the format of a collection of key/value pairs
+ // Otherwise, it is an error
+ Map jmsMsgBodyMap;
+ try {
+ jmsMsgBodyMap = S4JAdapterUtil.convertJsonToMap(msgBodyRawJsonStr);
+ } catch (Exception e) {
+ throw new RuntimeException("The specified message payload can't be converted to a map when requiring a 'Map' message type!");
+ }
+
+ for (String key : jmsMsgBodyMap.keySet()) {
+ String value = jmsMsgBodyMap.get(key);
+ ((MapMessage)message).setString(key, value);
+ messageSize += key.length();
+ messageSize += value.length();
+ }
+ } else if (StringUtils.equalsIgnoreCase(msgType, S4JAdapterUtil.JMS_MESSAGE_TYPES.STREAM.label)) {
+ message = jmsContext.createStreamMessage();
+
+ // The message body json string must be in the format of a list of objects
+ // Otherwise, it is an error
+ List