diff --git a/.github/workflows/blocking_issues.yml b/.github/workflows/blocking_issues.yml deleted file mode 100644 index 7bea810ac..000000000 --- a/.github/workflows/blocking_issues.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: Blocking Issues - -on: - issues: - types: [closed] - pull_request_target: - types: [opened, edited] - -jobs: - blocking_issues: - runs-on: ubuntu-latest - name: Checks for blocking issues - - steps: - - uses: Levi-Lesches/blocking-issues@v1.1 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b956bbe47..a69a37e0b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,6 +10,7 @@ jobs: steps: - uses: actions/checkout@v3 name: checkout nosqlbench + - uses: actions/setup-java@v3 name: setup java with: @@ -25,16 +26,19 @@ jobs: key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} restore-keys: ${{ runner.os }}-m2 - - name: mvn package + - name: mvn-package run: mvn package - - name: mvn verify + - name: mvn-verify run: mvn verify + - name: Capture + if: success() || failure() + run: tar -cvf logfiles.tar [a-zA-Z]**/logs/* + - name: Archive Test Results - if: always() + if: success() || failure() uses: actions/upload-artifact@v3 with: name: test-results - path: | - [a-zA-Z]**/logs/* + path: logfiles.tar diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index be3570828..1952e496b 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -23,7 +23,7 @@ on: jobs: analyze: name: Analyze - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 permissions: actions: read contents: read diff --git a/.github/workflows/dependabot.yml b/.github/workflows/dependabot.yml deleted file mode 100644 index dfd0e3086..000000000 --- a/.github/workflows/dependabot.yml +++ /dev/null @@ -1,10 +0,0 @@ -# Set update schedule for GitHub Actions - -version: 2 -updates: - - - package-ecosystem: "github-actions" - directory: "/" - schedule: - # Check for updates to GitHub Actions every week - interval: "weekly" diff --git a/.run/NBCLI web foreground dryrun.run.xml b/.run/NBCLI web foreground dryrun.run.xml deleted file mode 100644 index 4180a9a89..000000000 --- a/.run/NBCLI web foreground dryrun.run.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - \ No newline at end of file diff --git a/adapter-cqld4/src/main/java/io/nosqlbench/adapter/cqld4/Cqld4DriverAdapter.java b/adapter-cqld4/src/main/java/io/nosqlbench/adapter/cqld4/Cqld4DriverAdapter.java index d8aa80c30..9a23965d4 100644 --- a/adapter-cqld4/src/main/java/io/nosqlbench/adapter/cqld4/Cqld4DriverAdapter.java +++ b/adapter-cqld4/src/main/java/io/nosqlbench/adapter/cqld4/Cqld4DriverAdapter.java @@ -17,14 +17,14 @@ package io.nosqlbench.adapter.cqld4; import io.nosqlbench.adapter.cqld4.opmappers.Cqld4CoreOpMapper; +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.engine.api.activityimpl.uniform.flowtypes.Op; import io.nosqlbench.nb.annotations.Service; -import io.nosqlbench.api.config.standard.NBConfigModel; -import io.nosqlbench.api.config.standard.NBConfiguration; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; diff --git a/adapter-cqld4/src/main/java/io/nosqlbench/adapter/cqld4/Cqld4Space.java b/adapter-cqld4/src/main/java/io/nosqlbench/adapter/cqld4/Cqld4Space.java index a7aba6c0c..cf9d3d694 100644 --- a/adapter-cqld4/src/main/java/io/nosqlbench/adapter/cqld4/Cqld4Space.java +++ b/adapter-cqld4/src/main/java/io/nosqlbench/adapter/cqld4/Cqld4Space.java @@ -28,9 +28,9 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import io.nosqlbench.adapter.cqld4.optionhelpers.OptionHelpers; import io.nosqlbench.api.config.standard.*; -import io.nosqlbench.api.engine.util.SSLKsFactory; import io.nosqlbench.api.content.Content; import io.nosqlbench.api.content.NBIO; +import io.nosqlbench.api.engine.util.SSLKsFactory; import io.nosqlbench.api.errors.BasicError; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -44,7 +44,7 @@ import java.nio.file.Paths; import java.util.*; import java.util.stream.Collectors; -public class Cqld4Space { +public class Cqld4Space implements AutoCloseable { private final static Logger logger = LogManager.getLogger(Cqld4Space.class); private final String space; @@ -282,8 +282,8 @@ public class Cqld4Space { public static NBConfigModel getConfigModel() { return ConfigModel.of(Cqld4Space.class) .add(Param.optional("localdc")) - .add(Param.optional(List.of("secureconnectbundle","scb"))) - .add(Param.optional(List.of("hosts","host"))) + .add(Param.optional(List.of("secureconnectbundle", "scb"))) + .add(Param.optional(List.of("hosts", "host"))) .add(Param.optional("driverconfig", String.class)) .add(Param.optional("username", String.class, "user name (see also password and passfile)")) .add(Param.optional("userfile", String.class, "file to load the username from")) @@ -299,4 +299,13 @@ public class Cqld4Space { } + @Override + public void close() { + try { + this.getSession().close(); + } catch (Exception e) { + logger.warn("auto-closeable cql session threw exception in cql space(" + this.space + "): " + e); + throw e; + } + } } diff --git a/adapter-diag/src/main/java/io/nosqlbench/adapter/diag/DiagDriverAdapter.java b/adapter-diag/src/main/java/io/nosqlbench/adapter/diag/DiagDriverAdapter.java index a599e2242..037b6fb05 100644 --- a/adapter-diag/src/main/java/io/nosqlbench/adapter/diag/DiagDriverAdapter.java +++ b/adapter-diag/src/main/java/io/nosqlbench/adapter/diag/DiagDriverAdapter.java @@ -51,7 +51,7 @@ public class DiagDriverAdapter extends BaseDriverAdapter impl @Override public synchronized OpMapper getOpMapper() { if (this.mapper == null) { - this.mapper = new DiagOpMapper(this, getSpaceCache()); + this.mapper = new DiagOpMapper(this); } return this.mapper; } diff --git a/adapter-diag/src/main/java/io/nosqlbench/adapter/diag/DiagOp.java b/adapter-diag/src/main/java/io/nosqlbench/adapter/diag/DiagOp.java index 63f7ef3ab..cf6d2f2b7 100644 --- a/adapter-diag/src/main/java/io/nosqlbench/adapter/diag/DiagOp.java +++ b/adapter-diag/src/main/java/io/nosqlbench/adapter/diag/DiagOp.java @@ -28,9 +28,11 @@ public class DiagOp implements CycleOp { private final static Logger logger = LogManager.getLogger(DiagOp.class); private final List mutators; + private final DiagSpace space; - public DiagOp(List mutators) { + public DiagOp(DiagSpace space, List mutators) { this.mutators = mutators; + this.space = space; } @Override diff --git a/adapter-diag/src/main/java/io/nosqlbench/adapter/diag/DiagOpDispenser.java b/adapter-diag/src/main/java/io/nosqlbench/adapter/diag/DiagOpDispenser.java index 0ff2ad2cc..87ad8e76f 100644 --- a/adapter-diag/src/main/java/io/nosqlbench/adapter/diag/DiagOpDispenser.java +++ b/adapter-diag/src/main/java/io/nosqlbench/adapter/diag/DiagOpDispenser.java @@ -17,14 +17,13 @@ package io.nosqlbench.adapter.diag; import io.nosqlbench.adapter.diag.optasks.DiagTask; -import io.nosqlbench.engine.api.activityapi.ratelimits.RateLimiter; -import io.nosqlbench.engine.api.activityimpl.BaseOpDispenser; -import io.nosqlbench.engine.api.activityimpl.uniform.DriverAdapter; -import io.nosqlbench.engine.api.templating.ParsedOp; -import io.nosqlbench.nb.annotations.ServiceSelector; import io.nosqlbench.api.config.standard.NBConfigModel; import io.nosqlbench.api.config.standard.NBConfiguration; import io.nosqlbench.api.config.standard.NBReconfigurable; +import io.nosqlbench.engine.api.activityapi.ratelimits.RateLimiter; +import io.nosqlbench.engine.api.activityimpl.BaseOpDispenser; +import io.nosqlbench.engine.api.templating.ParsedOp; +import io.nosqlbench.nb.annotations.ServiceSelector; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -39,12 +38,12 @@ public class DiagOpDispenser extends BaseOpDispenser implement private LongFunction spaceF; private OpFunc opFuncs; - public DiagOpDispenser(DriverAdapter adapter, ParsedOp op) { + public DiagOpDispenser(DiagDriverAdapter adapter, LongFunction spaceF, ParsedOp op) { super(adapter,op); - this.opFunc = resolveOpFunc(op); + this.opFunc = resolveOpFunc(spaceF, op); } - private OpFunc resolveOpFunc(ParsedOp op) { + private OpFunc resolveOpFunc(LongFunction spaceF, ParsedOp op) { List tasks = new ArrayList<>(); Set tasknames = op.getDefinedNames(); @@ -82,7 +81,7 @@ public class DiagOpDispenser extends BaseOpDispenser implement // Store the task into the diag op's list of things to do when it runs tasks.add(task); } - this.opFunc = new OpFunc(tasks); + this.opFunc = new OpFunc(spaceF,tasks); return opFunc; } @@ -98,13 +97,17 @@ public class DiagOpDispenser extends BaseOpDispenser implement private final static class OpFunc implements LongFunction, NBReconfigurable { private final List tasks; - public OpFunc(List tasks) { + private final LongFunction spaceF; + + public OpFunc(LongFunction spaceF, List tasks) { this.tasks = tasks; + this.spaceF = spaceF; } @Override public DiagOp apply(long value) { - return new DiagOp(tasks); + DiagSpace space = spaceF.apply(value); + return new DiagOp(space, tasks); } @Override diff --git a/adapter-diag/src/main/java/io/nosqlbench/adapter/diag/DiagOpMapper.java b/adapter-diag/src/main/java/io/nosqlbench/adapter/diag/DiagOpMapper.java index 784bbd1d2..2e7328b1f 100644 --- a/adapter-diag/src/main/java/io/nosqlbench/adapter/diag/DiagOpMapper.java +++ b/adapter-diag/src/main/java/io/nosqlbench/adapter/diag/DiagOpMapper.java @@ -16,14 +16,12 @@ package io.nosqlbench.adapter.diag; -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.api.config.standard.NBConfigModel; import io.nosqlbench.api.config.standard.NBConfiguration; import io.nosqlbench.api.config.standard.NBReconfigurable; +import io.nosqlbench.engine.api.activityimpl.OpDispenser; +import io.nosqlbench.engine.api.activityimpl.OpMapper; +import io.nosqlbench.engine.api.templating.ParsedOp; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -31,20 +29,17 @@ import java.util.Map; import java.util.function.LongFunction; public class DiagOpMapper implements OpMapper, NBReconfigurable { - private final DriverSpaceCache spaceCache; private final Map dispensers = new LinkedHashMap<>(); - private final DriverAdapter adapter; + private final DiagDriverAdapter adapter; - public DiagOpMapper(DriverAdapter adapter, DriverSpaceCache spaceCache) { - this.spaceCache = spaceCache; + public DiagOpMapper(DiagDriverAdapter adapter) { this.adapter = adapter; } @Override public OpDispenser apply(ParsedOp op) { - DiagOpDispenser dispenser = new DiagOpDispenser(adapter,op); - LongFunction spaceName = op.getAsFunctionOr("space", "default"); - LongFunction spacef = l -> spaceCache.get(spaceName.apply(l)); + LongFunction spaceF = adapter.getSpaceFunc(op); + DiagOpDispenser dispenser = new DiagOpDispenser(adapter,spaceF,op); dispensers.put(op.getName(),dispenser); return dispenser; } diff --git a/adapter-diag/src/main/java/io/nosqlbench/adapter/diag/DiagSpace.java b/adapter-diag/src/main/java/io/nosqlbench/adapter/diag/DiagSpace.java index 8c5dd3fe9..4a8ca3d2c 100644 --- a/adapter-diag/src/main/java/io/nosqlbench/adapter/diag/DiagSpace.java +++ b/adapter-diag/src/main/java/io/nosqlbench/adapter/diag/DiagSpace.java @@ -26,13 +26,14 @@ import io.nosqlbench.api.config.standard.Param; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -public class DiagSpace implements ActivityDefObserver { +public class DiagSpace implements ActivityDefObserver, AutoCloseable { private final Logger logger = LogManager.getLogger(DiagSpace.class); private final NBConfiguration cfg; private final String name; private RateLimiter diagRateLimiter; private long interval; + private boolean errorOnClose; public DiagSpace(String name, NBConfiguration cfg) { this.cfg = cfg; @@ -42,11 +43,13 @@ public class DiagSpace implements ActivityDefObserver { public void applyConfig(NBConfiguration cfg) { this.interval = cfg.get("interval",long.class); + this.errorOnClose = cfg.get("erroronclose",boolean.class); } public static NBConfigModel getConfigModel() { return ConfigModel.of(DiagSpace.class) .add(Param.defaultTo("interval",1000)) + .add(Param.defaultTo("erroronclose", false)) .asReadOnly(); } @@ -61,4 +64,12 @@ public class DiagSpace implements ActivityDefObserver { NBConfiguration cfg = getConfigModel().apply(activityDef.getParams().getStringStringMap()); this.applyConfig(cfg); } + + @Override + public void close() throws Exception { + logger.debug("closing diag space '" + this.name + "'"); + if (errorOnClose) { + throw new RuntimeException("diag space was configured to throw this error when it was configured."); + } + } } diff --git a/adapter-diag/src/main/java/io/nosqlbench/adapter/diag/optasks/DiagTask_erroroncycle.java b/adapter-diag/src/main/java/io/nosqlbench/adapter/diag/optasks/DiagTask_erroroncycle.java index 258503897..a8739cca4 100644 --- a/adapter-diag/src/main/java/io/nosqlbench/adapter/diag/optasks/DiagTask_erroroncycle.java +++ b/adapter-diag/src/main/java/io/nosqlbench/adapter/diag/optasks/DiagTask_erroroncycle.java @@ -28,7 +28,7 @@ import java.util.Map; * Cause a blocking call to delay the initialization * of this owning operation for a number of milliseconds. */ -@Service(value= DiagTask.class,selector = "erroroncycle") +@Service(value = DiagTask.class, selector = "erroroncycle") public class DiagTask_erroroncycle implements DiagTask { private String name; @@ -36,21 +36,21 @@ public class DiagTask_erroroncycle implements DiagTask { @Override public void applyConfig(NBConfiguration cfg) { - this.name = cfg.get("name",String.class); - error_on_cycle = cfg.get("erroroncycle",long.class); + this.name = cfg.get("name", String.class); + error_on_cycle = cfg.get("erroroncycle", long.class); } @Override public NBConfigModel getConfigModel() { return ConfigModel.of(DiagTask_erroroncycle.class) - .add(Param.required("name",String.class)) - .add(Param.defaultTo("erroroncycle",1L)) - .asReadOnly(); + .add(Param.required("name", String.class)) + .add(Param.defaultTo("erroroncycle", 1L)) + .asReadOnly(); } @Override public Map apply(Long aLong, Map stringObjectMap) { - if (error_on_cycle==aLong) { + if (error_on_cycle == aLong) { throw new RuntimeException("Diag was requested to stop on cycle " + error_on_cycle); } return Map.of(); diff --git a/adapter-dynamodb/pom.xml b/adapter-dynamodb/pom.xml index 5b94bc107..5d6d2d48e 100644 --- a/adapter-dynamodb/pom.xml +++ b/adapter-dynamodb/pom.xml @@ -45,7 +45,7 @@ com.amazonaws aws-java-sdk-dynamodb - 1.12.336 + 1.12.348 diff --git a/driver-jms/pom.xml b/adapter-pulsar/pom.xml similarity index 58% rename from driver-jms/pom.xml rename to adapter-pulsar/pom.xml index c3cbcc803..2428c54fe 100644 --- a/driver-jms/pom.xml +++ b/adapter-pulsar/pom.xml @@ -17,61 +17,49 @@ 4.0.0 + adapter-pulsar + jar + mvn-defaults io.nosqlbench - 4.17.22-SNAPSHOT + 4.17.31-SNAPSHOT ../mvn-defaults - driver-jms - jar ${project.artifactId} - - A JMS driver for nosqlbench. This provides the ability to inject synthetic data - into a pulsar system via JMS 2.0 compatibile APIs. - - NOTE: this is JMS compatible driver from DataStax that allows using a Pulsar cluster - as the potential JMS Destination + A Pulsar driver for nosqlbench. This provides the ability to inject synthetic data + into a pulsar system. - - - - - - - - - - - - - - + + 2.10.1 + - io.nosqlbench engine-api - 4.17.22-SNAPSHOT + 4.17.31-SNAPSHOT - - org.apache.commons - commons-lang3 - 3.12.0 + io.nosqlbench + adapters-api + 4.17.31-SNAPSHOT - - org.projectlombok - lombok - 1.18.24 - provided + org.apache.pulsar + pulsar-client + ${pulsar.version} + + + + org.apache.pulsar + pulsar-client-admin + ${pulsar.version} @@ -88,13 +76,19 @@ 2.8.0 - + - com.datastax.oss - pulsar-jms - 2.4.11 + org.apache.avro + avro + 1.11.1 + + + org.apache.commons + commons-lang3 + 3.12.0 + diff --git a/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/PulsarDriverAdapter.java b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/PulsarDriverAdapter.java new file mode 100644 index 000000000..4d0c2c7fb --- /dev/null +++ b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/PulsarDriverAdapter.java @@ -0,0 +1,54 @@ +/* + * 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.pulsar; + +import io.nosqlbench.adapter.pulsar.ops.PulsarOp; +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.Maturity; +import io.nosqlbench.nb.annotations.Service; +import io.nosqlbench.api.config.standard.NBConfigModel; +import io.nosqlbench.api.config.standard.NBConfiguration; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.function.Function; + +@Service(value = DriverAdapter.class, selector = "pulsar") +public class PulsarDriverAdapter extends BaseDriverAdapter { + + private final static Logger logger = LogManager.getLogger(PulsarDriverAdapter.class); + + @Override + public OpMapper getOpMapper() { + DriverSpaceCache spaceCache = getSpaceCache(); + NBConfiguration adapterConfig = getConfiguration(); + return new PulsarOpMapper(this, adapterConfig, spaceCache); + } + + @Override + public Function getSpaceInitializer(NBConfiguration cfg) { + return (s) -> new PulsarSpace(s, cfg); + } + + @Override + public NBConfigModel getConfigModel() { + return super.getConfigModel().add(PulsarSpace.getConfigModel()); + } +} diff --git a/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/PulsarOpMapper.java b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/PulsarOpMapper.java new file mode 100644 index 000000000..a3db586ee --- /dev/null +++ b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/PulsarOpMapper.java @@ -0,0 +1,82 @@ +/* + * 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.pulsar; + +import io.nosqlbench.adapter.pulsar.dispensers.*; +import io.nosqlbench.adapter.pulsar.ops.PulsarOp; +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 PulsarOpMapper implements OpMapper { + + private final static Logger logger = LogManager.getLogger(PulsarOpMapper.class); + + private final NBConfiguration cfg; + private final DriverSpaceCache spaceCache; + private final DriverAdapter adapter; + + public PulsarOpMapper(DriverAdapter adapter, NBConfiguration cfg, DriverSpaceCache spaceCache) { + this.cfg = cfg; + this.spaceCache = spaceCache; + this.adapter = adapter; + } + + @Override + public OpDispenser apply(ParsedOp op) { + String spaceName = op.getStaticConfigOr("space", "default"); + PulsarSpace pulsarSpace = 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(PulsarOpType.class, String.class); + + return switch (opType.enumId) { + case AdminTenant -> + new AdminTenantOpDispenser(adapter, op, opType.targetFunction, pulsarSpace); + case AdminNamespace -> + new AdminNamespaceOpDispenser(adapter, op, opType.targetFunction, pulsarSpace); + case AdminTopic -> + new AdminTopicOpDispenser(adapter, op, opType.targetFunction, pulsarSpace); + case MessageProduce -> + new MessageProducerOpDispenser(adapter, op, opType.targetFunction, pulsarSpace); + case MessageConsume -> + new MessageConsumerOpDispenser(adapter, op, opType.targetFunction, pulsarSpace); + ////////////////////////// + // NOTE: not sure how useful to have Pulsar message reader API in the NB performance testing + // currently, the reader API in NB Pulsar driver is no-op (see TDOD in MessageReaderOp) + ////////////////////////// + case MessageRead -> + new MessageReaderOpDispenser(adapter, op, opType.targetFunction, pulsarSpace); + }; + } + } + +} diff --git a/driver-jms/src/main/java/io/nosqlbench/driver/jms/ops/JmsTimeTrackOp.java b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/PulsarOpType.java similarity index 62% rename from driver-jms/src/main/java/io/nosqlbench/driver/jms/ops/JmsTimeTrackOp.java rename to adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/PulsarOpType.java index 8d588faff..d63185121 100644 --- a/driver-jms/src/main/java/io/nosqlbench/driver/jms/ops/JmsTimeTrackOp.java +++ b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/PulsarOpType.java @@ -14,20 +14,16 @@ * limitations under the License. */ -package io.nosqlbench.driver.jms.ops; +package io.nosqlbench.adapter.pulsar; -/** - * Base type of all Sync Pulsar Operations including Producers and Consumers. - */ -public abstract class JmsTimeTrackOp implements JmsOp { - - public void run(Runnable timeTracker) { - try { - this.run(); - } finally { - timeTracker.run(); - } - } - - public abstract void run(); +public enum PulsarOpType { + AdminTenant, + AdminNamespace, + AdminTopic, + MessageProduce, + // This also supports multi-topic message consumption + MessageConsume, + MessageRead; } + + diff --git a/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/PulsarSpace.java b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/PulsarSpace.java new file mode 100644 index 000000000..4aba161f1 --- /dev/null +++ b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/PulsarSpace.java @@ -0,0 +1,230 @@ +/* + * 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.pulsar; + +import io.nosqlbench.adapter.pulsar.exception.PulsarAdapterUnexpectedException; +import io.nosqlbench.adapter.pulsar.util.PulsarAdapterUtil; +import io.nosqlbench.adapter.pulsar.util.PulsarClientConf; +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.BooleanUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.pulsar.client.admin.PulsarAdmin; +import org.apache.pulsar.client.admin.PulsarAdminBuilder; +import org.apache.pulsar.client.api.*; +import org.apache.pulsar.common.schema.KeyValueEncodingType; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class PulsarSpace implements AutoCloseable { + + private final static Logger logger = LogManager.getLogger(PulsarSpace.class); + + private final String spaceName; + private final NBConfiguration cfg; + + private final String pulsarSvcUrl; + private final String webSvcUrl; + + private PulsarClientConf pulsarClientConf; + private PulsarClient pulsarClient; + private PulsarAdmin pulsarAdmin; + private Schema pulsarSchema; + + private final ConcurrentHashMap> producers = new ConcurrentHashMap<>(); + private final ConcurrentHashMap> consumers = new ConcurrentHashMap<>(); + private final ConcurrentHashMap> readers = new ConcurrentHashMap<>(); + + + public PulsarSpace(String spaceName, NBConfiguration cfg) { + this.spaceName = spaceName; + this.cfg = cfg; + + this.pulsarSvcUrl = cfg.get("service_url"); + this.webSvcUrl = cfg.get("web_url"); + this.pulsarClientConf = new PulsarClientConf(cfg.get("config")); + + initPulsarAdminAndClientObj(); + createPulsarSchemaFromConf(); + } + + public static NBConfigModel getConfigModel() { + return ConfigModel.of(PulsarSpace.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("cyclerate_per_thread", false) + .setDescription("Apply cycle rate per NB thread")) + .asReadOnly(); + } + + public String getPulsarSvcUrl() { return pulsarSvcUrl; } + public String getWebSvcUrl() { return webSvcUrl; } + public PulsarClientConf getPulsarNBClientConf() { return pulsarClientConf; } + public PulsarClient getPulsarClient() { return pulsarClient; } + public PulsarAdmin getPulsarAdmin() { return pulsarAdmin; } + public Schema getPulsarSchema() { return pulsarSchema; } + public int getProducerSetCnt() { return producers.size(); } + public int getConsumerSetCnt() { return consumers.size(); } + public int getReaderSetCnt() { return readers.size(); } + public Producer getProducer(String name) { return producers.get(name); } + public void setProducer(String name, Producer producer) { producers.put(name, producer); } + public Consumer getConsumer(String name) { return consumers.get(name); } + public void setConsumer(String name, Consumer consumer) { consumers.put(name, consumer); } + + public Reader getReader(String name) { return readers.get(name); } + public void setReader(String name, Reader reader) { readers.put(name, reader); } + + + /** + * Initialize + * - PulsarAdmin object for adding/deleting tenant, namespace, and topic + * - PulsarClient object for message publishing and consuming + */ + private void initPulsarAdminAndClientObj() { + PulsarAdminBuilder adminBuilder = + PulsarAdmin.builder() + .serviceHttpUrl(webSvcUrl); + + ClientBuilder clientBuilder = PulsarClient.builder(); + + try { + Map clientConfMap = pulsarClientConf.getClientConfMapRaw(); + + // Override "client.serviceUrl" setting in config.properties + clientConfMap.remove("serviceUrl"); + clientBuilder.loadConf(clientConfMap).serviceUrl(pulsarSvcUrl); + + // Pulsar Authentication + String authPluginClassName = + pulsarClientConf.getClientConfValueRaw(PulsarAdapterUtil.CLNT_CONF_KEY.authPulginClassName.label); + String authParams = + pulsarClientConf.getClientConfValueRaw(PulsarAdapterUtil.CLNT_CONF_KEY.authParams.label); + + if ( !StringUtils.isAnyBlank(authPluginClassName, authParams) ) { + adminBuilder.authentication(authPluginClassName, authParams); + clientBuilder.authentication(authPluginClassName, authParams); + } + + boolean useTls = StringUtils.contains(pulsarSvcUrl, "pulsar+ssl"); + if ( useTls ) { + String tlsHostnameVerificationEnableStr = + pulsarClientConf.getClientConfValueRaw(PulsarAdapterUtil.CLNT_CONF_KEY.tlsHostnameVerificationEnable.label); + boolean tlsHostnameVerificationEnable = BooleanUtils.toBoolean(tlsHostnameVerificationEnableStr); + + adminBuilder + .enableTlsHostnameVerification(tlsHostnameVerificationEnable); + clientBuilder + .enableTlsHostnameVerification(tlsHostnameVerificationEnable); + + String tlsTrustCertsFilePath = + pulsarClientConf.getClientConfValueRaw(PulsarAdapterUtil.CLNT_CONF_KEY.tlsTrustCertsFilePath.label); + if (!StringUtils.isBlank(tlsTrustCertsFilePath)) { + adminBuilder.tlsTrustCertsFilePath(tlsTrustCertsFilePath); + clientBuilder.tlsTrustCertsFilePath(tlsTrustCertsFilePath); + } + + String tlsAllowInsecureConnectionStr = + pulsarClientConf.getClientConfValueRaw(PulsarAdapterUtil.CLNT_CONF_KEY.tlsAllowInsecureConnection.label); + boolean tlsAllowInsecureConnection = BooleanUtils.toBoolean(tlsAllowInsecureConnectionStr); + adminBuilder.allowTlsInsecureConnection(tlsAllowInsecureConnection); + clientBuilder.allowTlsInsecureConnection(tlsAllowInsecureConnection); + } + + pulsarAdmin = adminBuilder.build(); + pulsarClient = clientBuilder.build(); + + } catch (PulsarClientException e) { + logger.error("Fail to create PulsarAdmin and/or PulsarClient object from the global configuration!"); + throw new RuntimeException("Fail to create PulsarAdmin and/or PulsarClient object from global configuration!"); + } + } + + public void shutdownSpace() { + try { + for (Producer producer : producers.values()) { + if (producer != null) producer.close(); + } + for (Consumer consumer : consumers.values()) { + if (consumer != null) consumer.close(); + } + for (Reader reader : readers.values()) { + if (reader != null) reader.close(); + } + if (pulsarAdmin != null) pulsarAdmin.close(); + if (pulsarClient != null) pulsarClient.close(); + } + catch (Exception e) { + throw new PulsarAdapterUnexpectedException( + "Unexpected error when shutting down the Pulsar space \"" + spaceName + "\"!"); + } + } + + /** + * Get Pulsar schema from the definition string + */ + + private Schema buildSchemaFromDefinition(String schemaTypeConfEntry, + String schemaDefinitionConfEntry) { + String schemaType = pulsarClientConf.getSchemaConfValueRaw(schemaTypeConfEntry); + String schemaDef = pulsarClientConf.getSchemaConfValueRaw(schemaDefinitionConfEntry); + + Schema result; + if (PulsarAdapterUtil.isAvroSchemaTypeStr(schemaType)) { + result = PulsarAdapterUtil.getAvroSchema(schemaType, schemaDef); + } else if (PulsarAdapterUtil.isPrimitiveSchemaTypeStr(schemaType)) { + result = PulsarAdapterUtil.getPrimitiveTypeSchema(schemaType); + } else if (PulsarAdapterUtil.isAutoConsumeSchemaTypeStr(schemaType)) { + result = Schema.AUTO_CONSUME(); + } else { + throw new RuntimeException("Unsupported schema type string: " + schemaType + "; " + + "Only primitive type, Avro type and AUTO_CONSUME are supported at the moment!"); + } + return result; + } + private void createPulsarSchemaFromConf() { + pulsarSchema = buildSchemaFromDefinition("schema.type", "schema.definition"); + + // this is to allow KEY_VALUE schema + if (pulsarClientConf.hasSchemaConfKey("schema.key.type")) { + Schema pulsarKeySchema = buildSchemaFromDefinition("schema.key.type", "schema.key.definition"); + KeyValueEncodingType keyValueEncodingType = KeyValueEncodingType.SEPARATED; + + String encodingType = pulsarClientConf.getSchemaConfValueRaw("schema.keyvalue.encodingtype"); + if (StringUtils.isNotBlank(encodingType)) { + keyValueEncodingType = KeyValueEncodingType.valueOf(encodingType); + } + + pulsarSchema = Schema.KeyValue(pulsarKeySchema, pulsarSchema, keyValueEncodingType); + } + } + + @Override + public void close() { + shutdownSpace(); + } +} + + diff --git a/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/dispensers/AdminNamespaceOpDispenser.java b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/dispensers/AdminNamespaceOpDispenser.java new file mode 100644 index 000000000..eb41ce1ed --- /dev/null +++ b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/dispensers/AdminNamespaceOpDispenser.java @@ -0,0 +1,49 @@ +/* + * 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.pulsar.dispensers; + +import io.nosqlbench.adapter.pulsar.PulsarSpace; +import io.nosqlbench.adapter.pulsar.ops.AdminNamespaceOp; +import io.nosqlbench.engine.api.activityimpl.uniform.DriverAdapter; +import io.nosqlbench.engine.api.templating.ParsedOp; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.pulsar.client.admin.PulsarAdmin; + +import java.util.function.LongFunction; + +public class AdminNamespaceOpDispenser extends PulsarAdminOpDispenser { + + private final static Logger logger = LogManager.getLogger("AdminNamespaceOpDispenser"); + + public AdminNamespaceOpDispenser(DriverAdapter adapter, + ParsedOp op, + LongFunction tgtNameFunc, + PulsarSpace pulsarSpace) { + super(adapter, op, tgtNameFunc, pulsarSpace); + } + + @Override + public AdminNamespaceOp apply(long cycle) { + return new AdminNamespaceOp( + pulsarAdapterMetrics, + pulsarAdmin, + asyncApiFunc.apply(cycle), + adminDelOpFunc.apply(cycle), + tgtNameFunc.apply(cycle)); + } +} diff --git a/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/dispensers/AdminTenantOpDispenser.java b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/dispensers/AdminTenantOpDispenser.java new file mode 100644 index 000000000..e19897ed2 --- /dev/null +++ b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/dispensers/AdminTenantOpDispenser.java @@ -0,0 +1,57 @@ +/* + * 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.pulsar.dispensers; + +import io.nosqlbench.adapter.pulsar.PulsarSpace; +import io.nosqlbench.adapter.pulsar.ops.AdminTenantOp; +import io.nosqlbench.engine.api.activityimpl.uniform.DriverAdapter; +import io.nosqlbench.engine.api.templating.ParsedOp; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.pulsar.client.admin.PulsarAdmin; + +import java.util.*; +import java.util.function.LongFunction; + +public class AdminTenantOpDispenser extends PulsarAdminOpDispenser { + + private final static Logger logger = LogManager.getLogger("AdminTenantOpDispenser"); + + private final LongFunction> adminRolesFunc; + private final LongFunction> allowedClustersFunc; + public AdminTenantOpDispenser(DriverAdapter adapter, + ParsedOp op, + LongFunction tgtNameFunc, + PulsarSpace pulsarSpace) { + super(adapter, op, tgtNameFunc, pulsarSpace); + + adminRolesFunc = lookupStaticStrSetOpValueFunc("admin_roles"); + allowedClustersFunc = lookupStaticStrSetOpValueFunc("allowed_clusters"); + } + + @Override + public AdminTenantOp apply(long cycle) { + return new AdminTenantOp( + pulsarAdapterMetrics, + pulsarAdmin, + asyncApiFunc.apply(cycle), + adminDelOpFunc.apply(cycle), + tgtNameFunc.apply(cycle), + adminRolesFunc.apply(cycle), + allowedClustersFunc.apply(cycle)); + } +} diff --git a/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/dispensers/AdminTopicOpDispenser.java b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/dispensers/AdminTopicOpDispenser.java new file mode 100644 index 000000000..c92436128 --- /dev/null +++ b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/dispensers/AdminTopicOpDispenser.java @@ -0,0 +1,60 @@ +/* + * 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.pulsar.dispensers; + +import io.nosqlbench.adapter.pulsar.PulsarSpace; +import io.nosqlbench.adapter.pulsar.ops.AdminTopicOp; +import io.nosqlbench.engine.api.activityimpl.uniform.DriverAdapter; +import io.nosqlbench.engine.api.templating.ParsedOp; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.pulsar.client.admin.PulsarAdmin; + +import java.util.function.LongFunction; + +public class AdminTopicOpDispenser extends PulsarAdminOpDispenser { + + private final static Logger logger = LogManager.getLogger("AdminTopicOpDispenser"); + + private final LongFunction enablePartFunc; + private final LongFunction partNumFunc; + + public AdminTopicOpDispenser(DriverAdapter adapter, + ParsedOp op, + LongFunction tgtNameFunc, + PulsarSpace pulsarSpace) { + super(adapter, op, tgtNameFunc, pulsarSpace); + + // Non-partitioned topic is default + enablePartFunc = lookupStaticBoolConfigValueFunc("enable_partition", false); + partNumFunc = lookupStaticIntOpValueFunc("partition_num", 1); + } + + @Override + public AdminTopicOp apply(long cycle) { + + return new AdminTopicOp( + pulsarAdapterMetrics, + pulsarAdmin, + asyncApiFunc.apply(cycle), + adminDelOpFunc.apply(cycle), + tgtNameFunc.apply(cycle), + enablePartFunc.apply(cycle), + partNumFunc.apply(cycle) + ); + } +} diff --git a/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/dispensers/MessageConsumerOpDispenser.java b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/dispensers/MessageConsumerOpDispenser.java new file mode 100644 index 000000000..4a60adeb8 --- /dev/null +++ b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/dispensers/MessageConsumerOpDispenser.java @@ -0,0 +1,104 @@ +/* + * 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.pulsar.dispensers; + +import io.nosqlbench.adapter.pulsar.PulsarSpace; +import io.nosqlbench.adapter.pulsar.ops.MessageConsumerOp; +import io.nosqlbench.adapter.pulsar.util.EndToEndStartingTimeSource; +import io.nosqlbench.adapter.pulsar.util.PulsarAdapterUtil; +import io.nosqlbench.adapter.pulsar.util.ReceivedMessageSequenceTracker; +import io.nosqlbench.engine.api.activityimpl.uniform.DriverAdapter; +import io.nosqlbench.engine.api.templating.ParsedOp; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.pulsar.client.api.Consumer; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.LongFunction; + +public class MessageConsumerOpDispenser extends PulsarClientOpDispenser { + + private final static Logger logger = LogManager.getLogger("MessageConsumerOpDispenser"); + + private final LongFunction topicPatternFunc; + private final LongFunction subscriptionNameFunc; + private final LongFunction subscriptionTypeFunc; + private final LongFunction cycleConsumerNameFunc; + private final LongFunction rangesFunc; + private final LongFunction e2eStartTimeSrcParamStrFunc; + private final LongFunction consumerFunction; + + private final ThreadLocal> + receivedMessageSequenceTrackersForTopicThreadLocal = ThreadLocal.withInitial(HashMap::new); + + public MessageConsumerOpDispenser(DriverAdapter adapter, + ParsedOp op, + LongFunction tgtNameFunc, + PulsarSpace pulsarSpace) { + super(adapter, op, tgtNameFunc, pulsarSpace); + + this.topicPatternFunc = + lookupOptionalStrOpValueFunc(PulsarAdapterUtil.CONSUMER_CONF_STD_KEY.topicsPattern.label); + this.subscriptionNameFunc = + lookupMandtoryStrOpValueFunc(PulsarAdapterUtil.CONSUMER_CONF_STD_KEY.subscriptionName.label); + this.subscriptionTypeFunc = + lookupOptionalStrOpValueFunc(PulsarAdapterUtil.CONSUMER_CONF_STD_KEY.subscriptionType.label); + this.cycleConsumerNameFunc = + lookupOptionalStrOpValueFunc(PulsarAdapterUtil.CONSUMER_CONF_STD_KEY.consumerName.label); + this.rangesFunc = + lookupOptionalStrOpValueFunc(PulsarAdapterUtil.CONSUMER_CONF_CUSTOM_KEY.ranges.label); + this.e2eStartTimeSrcParamStrFunc = lookupOptionalStrOpValueFunc( + PulsarAdapterUtil.DOC_LEVEL_PARAMS.E2E_STARTING_TIME_SOURCE.label, "none"); + this.consumerFunction = (l) -> getConsumer( + tgtNameFunc.apply(l), + topicPatternFunc.apply(l), + subscriptionNameFunc.apply(l), + subscriptionTypeFunc.apply(l), + cycleConsumerNameFunc.apply(l), + rangesFunc.apply(l)); + } + + @Override + public MessageConsumerOp apply(long cycle) { + return new MessageConsumerOp( + pulsarAdapterMetrics, + pulsarClient, + pulsarSchema, + asyncApiFunc.apply(cycle), + useTransactFunc.apply(cycle), + seqTrackingFunc.apply(cycle), + transactSupplierFunc.apply(cycle), + payloadRttFieldFunc.apply(cycle), + EndToEndStartingTimeSource.valueOf(e2eStartTimeSrcParamStrFunc.apply(cycle).toUpperCase()), + this::getReceivedMessageSequenceTracker, + consumerFunction.apply(cycle), + pulsarSpace.getPulsarNBClientConf().getConsumerTimeoutSeconds() + ); + } + + private ReceivedMessageSequenceTracker getReceivedMessageSequenceTracker(String topicName) { + return receivedMessageSequenceTrackersForTopicThreadLocal.get() + .computeIfAbsent(topicName, k -> createReceivedMessageSequenceTracker()); + } + + private ReceivedMessageSequenceTracker createReceivedMessageSequenceTracker() { + return new ReceivedMessageSequenceTracker(pulsarAdapterMetrics.getMsgErrOutOfSeqCounter(), + pulsarAdapterMetrics.getMsgErrDuplicateCounter(), + pulsarAdapterMetrics.getMsgErrLossCounter()); + } +} diff --git a/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/dispensers/MessageProducerOpDispenser.java b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/dispensers/MessageProducerOpDispenser.java new file mode 100644 index 000000000..72703c44c --- /dev/null +++ b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/dispensers/MessageProducerOpDispenser.java @@ -0,0 +1,75 @@ +/* + * 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.pulsar.dispensers; + +import io.nosqlbench.adapter.pulsar.PulsarSpace; +import io.nosqlbench.adapter.pulsar.ops.MessageProducerOp; +import io.nosqlbench.adapter.pulsar.util.PulsarAdapterUtil; +import io.nosqlbench.engine.api.activityimpl.uniform.DriverAdapter; +import io.nosqlbench.engine.api.templating.ParsedOp; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.pulsar.client.api.Producer; + +import java.util.function.LongFunction; + +public class MessageProducerOpDispenser extends PulsarClientOpDispenser { + + private final static Logger logger = LogManager.getLogger("MessageProducerOpDispenser"); + + public static final String MSG_KEY_OP_PARAM = "msg_key"; + public static final String MSG_PROP_OP_PARAM = "msg_prop"; + public static final String MSG_VALUE_OP_PARAM = "msg_value"; + + private final LongFunction cycleProducerNameFunc; + private final LongFunction> producerFunc; + private final LongFunction msgKeyFunc; + private final LongFunction msgPropFunc; + private final LongFunction msgValueFunc; + + public MessageProducerOpDispenser(DriverAdapter adapter, + ParsedOp op, + LongFunction tgtNameFunc, + PulsarSpace pulsarSpace) { + super(adapter, op, tgtNameFunc, pulsarSpace); + + this.cycleProducerNameFunc = + lookupOptionalStrOpValueFunc(PulsarAdapterUtil.PRODUCER_CONF_STD_KEY.producerName.label); + this.producerFunc = (l) -> getProducer(tgtNameFunc.apply(l), cycleProducerNameFunc.apply(l)); + this.msgKeyFunc = lookupOptionalStrOpValueFunc(MSG_KEY_OP_PARAM); + this.msgPropFunc = lookupOptionalStrOpValueFunc(MSG_PROP_OP_PARAM); + this.msgValueFunc = lookupMandtoryStrOpValueFunc(MSG_VALUE_OP_PARAM); + } + + @Override + public MessageProducerOp apply(long cycle) { + return new MessageProducerOp( + pulsarAdapterMetrics, + pulsarClient, + pulsarSchema, + asyncApiFunc.apply(cycle), + useTransactFunc.apply(cycle), + seqTrackingFunc.apply(cycle), + transactSupplierFunc.apply(cycle), + msgSeqErrSimuTypeSetFunc.apply(cycle), + producerFunc.apply(cycle), + msgKeyFunc.apply(cycle), + msgPropFunc.apply(cycle), + msgValueFunc.apply(cycle) + ); + } +} diff --git a/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/dispensers/MessageReaderOpDispenser.java b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/dispensers/MessageReaderOpDispenser.java new file mode 100644 index 000000000..3d40563e1 --- /dev/null +++ b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/dispensers/MessageReaderOpDispenser.java @@ -0,0 +1,66 @@ +/* + * 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.pulsar.dispensers; + +import io.nosqlbench.adapter.pulsar.PulsarSpace; +import io.nosqlbench.adapter.pulsar.ops.MessageReaderOp; +import io.nosqlbench.adapter.pulsar.util.PulsarAdapterUtil; +import io.nosqlbench.engine.api.activityimpl.uniform.DriverAdapter; +import io.nosqlbench.engine.api.templating.ParsedOp; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.pulsar.client.api.PulsarClient; +import org.apache.pulsar.client.api.Reader; +import org.apache.pulsar.client.api.Schema; + +import java.util.function.LongFunction; + +public class MessageReaderOpDispenser extends PulsarClientOpDispenser { + + private final static Logger logger = LogManager.getLogger("MessageReaderOpDispenser"); + + private final LongFunction cycleReaderNameFunc; + private final LongFunction msgStartPosStrFunc; + private final LongFunction readerFunc; + + public MessageReaderOpDispenser(DriverAdapter adapter, + ParsedOp op, + LongFunction tgtNameFunc, + PulsarSpace pulsarSpace) { + super(adapter, op, tgtNameFunc, pulsarSpace); + + this.cycleReaderNameFunc = + lookupMandtoryStrOpValueFunc(PulsarAdapterUtil.READER_CONF_STD_KEY.readerName.label); + this.msgStartPosStrFunc = lookupOptionalStrOpValueFunc( + "start_msg_position", PulsarAdapterUtil.READER_MSG_POSITION_TYPE.earliest.label); + this.readerFunc = (l) -> getReader( + tgtNameFunc.apply(l), + cycleReaderNameFunc.apply(l), + msgStartPosStrFunc.apply(l)); + } + + @Override + public MessageReaderOp apply(long cycle) { + + return new MessageReaderOp( + pulsarAdapterMetrics, + pulsarClient, + pulsarSchema, + asyncApiFunc.apply(cycle), + readerFunc.apply(cycle)); + } +} diff --git a/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/dispensers/PulsarAdminOpDispenser.java b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/dispensers/PulsarAdminOpDispenser.java new file mode 100644 index 000000000..a56752e29 --- /dev/null +++ b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/dispensers/PulsarAdminOpDispenser.java @@ -0,0 +1,49 @@ +/* + * 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.pulsar.dispensers; + +import io.nosqlbench.adapter.pulsar.PulsarSpace; +import io.nosqlbench.adapter.pulsar.util.PulsarAdapterUtil; +import io.nosqlbench.engine.api.activityimpl.uniform.DriverAdapter; +import io.nosqlbench.engine.api.templating.ParsedOp; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.pulsar.client.admin.PulsarAdmin; + +import java.util.function.LongFunction; + +public abstract class PulsarAdminOpDispenser extends PulsarBaseOpDispenser { + + private final static Logger logger = LogManager.getLogger("PulsarAdminOpDispenser"); + + + protected final PulsarAdmin pulsarAdmin; + protected final LongFunction adminDelOpFunc; + + public PulsarAdminOpDispenser(DriverAdapter adapter, + ParsedOp op, + LongFunction tgtNameFunc, + PulsarSpace pulsarSpace) { + super(adapter, op, tgtNameFunc, pulsarSpace); + + this.pulsarAdmin = pulsarSpace.getPulsarAdmin(); + + // Doc-level parameter: admin_delop + this.adminDelOpFunc = lookupStaticBoolConfigValueFunc( + PulsarAdapterUtil.DOC_LEVEL_PARAMS.ADMIN_DELOP.label, false); + } +} diff --git a/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/dispensers/PulsarBaseOpDispenser.java b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/dispensers/PulsarBaseOpDispenser.java new file mode 100644 index 000000000..1de99a097 --- /dev/null +++ b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/dispensers/PulsarBaseOpDispenser.java @@ -0,0 +1,577 @@ +package io.nosqlbench.adapter.pulsar.dispensers; + +/* + * 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. + */ + +import io.nosqlbench.adapter.pulsar.PulsarSpace; +import io.nosqlbench.adapter.pulsar.exception.PulsarAdapterInvalidParamException; +import io.nosqlbench.adapter.pulsar.exception.PulsarAdapterUnexpectedException; +import io.nosqlbench.adapter.pulsar.ops.PulsarOp; +import io.nosqlbench.adapter.pulsar.util.PulsarAdapterMetrics; +import io.nosqlbench.adapter.pulsar.util.PulsarAdapterUtil; +import io.nosqlbench.api.config.NBNamedElement; +import io.nosqlbench.engine.api.activityimpl.BaseOpDispenser; +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 org.apache.pulsar.client.api.*; + +import java.util.*; +import java.util.function.LongFunction; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; +import java.util.stream.Collectors; + +public abstract class PulsarBaseOpDispenser extends BaseOpDispenser implements NBNamedElement { + + private final static Logger logger = LogManager.getLogger("PulsarBaseOpDispenser"); + + protected final ParsedOp parsedOp; + protected final PulsarSpace pulsarSpace; + protected final PulsarAdapterMetrics pulsarAdapterMetrics; + protected final LongFunction asyncApiFunc; + protected final LongFunction tgtNameFunc; + + protected final int totalThreadNum; + + protected final long totalCycleNum; + + public PulsarBaseOpDispenser(DriverAdapter adapter, + ParsedOp op, + LongFunction tgtNameFunc, + PulsarSpace pulsarSpace) { + + super(adapter, op); + + this.parsedOp = op; + this.tgtNameFunc = tgtNameFunc; + this.pulsarSpace = pulsarSpace; + + // Doc-level parameter: async_api + this.asyncApiFunc = lookupStaticBoolConfigValueFunc( + PulsarAdapterUtil.DOC_LEVEL_PARAMS.ASYNC_API.label, true); + + String defaultMetricsPrefix = getDefaultMetricsPrefix(this.parsedOp); + this.pulsarAdapterMetrics = new PulsarAdapterMetrics(this, defaultMetricsPrefix); + pulsarAdapterMetrics.initPulsarAdapterInstrumentation(); + + totalThreadNum = NumberUtils.toInt(parsedOp.getStaticValue("threads")); + totalCycleNum = NumberUtils.toLong(parsedOp.getStaticValue("cycles")); + } + + @Override + public String getName() { + return "PulsarBaseOpDispenser"; + } + + public PulsarSpace getPulsarSpace() { return pulsarSpace; } + + protected LongFunction lookupStaticBoolConfigValueFunc(String paramName, boolean defaultValue) { + LongFunction booleanLongFunction; + booleanLongFunction = (l) -> parsedOp.getOptionalStaticConfig(paramName, String.class) + .filter(Predicate.not(String::isEmpty)) + .map(value -> BooleanUtils.toBoolean(value)) + .orElse(defaultValue); + logger.info("{}: {}", paramName, booleanLongFunction.apply(0)); + return booleanLongFunction; + } + + protected LongFunction> lookupStaticStrSetOpValueFunc(String paramName) { + LongFunction> setStringLongFunction; + setStringLongFunction = (l) -> parsedOp.getOptionalStaticValue(paramName, String.class) + .filter(Predicate.not(String::isEmpty)) + .map(value -> { + Set set = new HashSet<>(); + + if (StringUtils.contains(value,',')) { + set = Arrays.stream(value.split(",")) + .map(String::trim) + .filter(Predicate.not(String::isEmpty)) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + + return set; + }).orElse(Collections.emptySet()); + logger.info("{}: {}", paramName, setStringLongFunction.apply(0)); + return setStringLongFunction; + } + + // If the corresponding Op parameter is not provided, use the specified default value + protected LongFunction lookupStaticIntOpValueFunc(String paramName, int defaultValue) { + LongFunction integerLongFunction; + integerLongFunction = (l) -> parsedOp.getOptionalStaticValue(paramName, String.class) + .filter(Predicate.not(String::isEmpty)) + .map(value -> NumberUtils.toInt(value)) + .map(value -> { + if (value < 0) return 0; + else return value; + }).orElse(defaultValue); + logger.info("{}: {}", paramName, integerLongFunction.apply(0)); + return integerLongFunction; + } + + // If the corresponding Op parameter is not provided, use the specified default value + protected LongFunction lookupOptionalStrOpValueFunc(String paramName, String defaultValue) { + LongFunction stringLongFunction; + stringLongFunction = parsedOp.getAsOptionalFunction(paramName, String.class) + .orElse((l) -> defaultValue); + logger.info("{}: {}", paramName, stringLongFunction.apply(0)); + + return stringLongFunction; + } + protected LongFunction lookupOptionalStrOpValueFunc(String paramName) { + return lookupOptionalStrOpValueFunc(paramName, ""); + } + + // Mandatory Op parameter. Throw an error if not specified or having empty value + protected LongFunction lookupMandtoryStrOpValueFunc(String paramName) { + LongFunction stringLongFunction; + stringLongFunction = parsedOp.getAsRequiredFunction(paramName, String.class); + logger.info("{}: {}", paramName, stringLongFunction.apply(0)); + + return stringLongFunction; + } + + /** + * Get a proper Pulsar API metrics prefix depending on the API type + * + * @param apiType - Pulsar API type: producer, consumer, reader, etc. + * @param apiObjName - actual name of a producer, a consumer, a reader, etc. + * @param topicName - topic name + * @return String + */ + private String getPulsarAPIMetricsPrefix(String apiType, String apiObjName, String topicName) { + String apiMetricsPrefix = ""; + + if (PulsarAdapterUtil.isValidPulsarApiType(apiType)) { + if (!StringUtils.isBlank(apiObjName)) { + apiMetricsPrefix = apiObjName + "_"; + } else { + // we want a meaningful name for the API object (producer, consumer, reader, etc.) + // we are not appending the topic name + apiMetricsPrefix = apiType; + + if (apiType.equalsIgnoreCase(PulsarAdapterUtil.PULSAR_API_TYPE.PRODUCER.label)) + apiMetricsPrefix += pulsarSpace.getProducerSetCnt(); + else if (apiType.equalsIgnoreCase(PulsarAdapterUtil.PULSAR_API_TYPE.CONSUMER.label)) + apiMetricsPrefix += pulsarSpace.getConsumerSetCnt(); + else if (apiType.equalsIgnoreCase(PulsarAdapterUtil.PULSAR_API_TYPE.READER.label)) + apiMetricsPrefix += pulsarSpace.getReaderSetCnt(); + + apiMetricsPrefix += "_"; + } + + apiMetricsPrefix += topicName + "_"; + apiMetricsPrefix = apiMetricsPrefix + // default name for tests/demos (in all Pulsar examples) is persistent://public/default/test -> use just the topic name test + .replace("persistent://public/default/", "") + // always remove topic type + .replace("non-persistent://", "") + .replace("persistent://", "") + // persistent://tenant/namespace/topicname -> tenant_namespace_topicname + .replace("/", "_"); + + apiMetricsPrefix += "--"; + } + + return apiMetricsPrefix; + } + + + // A configuration parameter can be set either at the global level (config.properties file), + // or at the cycle level (.yaml file). + // If set at both levels, cycle level setting takes precedence + private String getEffectiveConValue(String confCategory, String confParamName, String cycleConfValue) { + if (!StringUtils.isBlank(cycleConfValue)) { + return cycleConfValue; + } + + if (PulsarAdapterUtil.isValidConfCategory(confCategory)) { + Map catConfMap = new HashMap<>(); + + if (StringUtils.equalsIgnoreCase(confCategory, PulsarAdapterUtil.CONF_GATEGORY.Schema.label)) + catConfMap = pulsarSpace.getPulsarNBClientConf().getSchemaConfMapRaw(); + else if (StringUtils.equalsIgnoreCase(confCategory, PulsarAdapterUtil.CONF_GATEGORY.Client.label)) + catConfMap = pulsarSpace.getPulsarNBClientConf().getClientConfMapRaw(); + else if (StringUtils.equalsIgnoreCase(confCategory, PulsarAdapterUtil.CONF_GATEGORY.Producer.label)) + catConfMap = pulsarSpace.getPulsarNBClientConf().getProducerConfMapRaw(); + else if (StringUtils.equalsIgnoreCase(confCategory, PulsarAdapterUtil.CONF_GATEGORY.Consumer.label)) + catConfMap = pulsarSpace.getPulsarNBClientConf().getConsumerConfMapRaw(); + else if (StringUtils.equalsIgnoreCase(confCategory, PulsarAdapterUtil.CONF_GATEGORY.Reader.label)) + catConfMap = pulsarSpace.getPulsarNBClientConf().getReaderConfMapRaw(); + + String globalConfValue = catConfMap.get(confParamName); + if (!StringUtils.isBlank(globalConfValue)) { + return globalConfValue; + } + } + + return ""; + } + + + public Producer getProducer(String cycleTopicName, String cycleProducerName) { + String topicName = getEffectiveConValue( + PulsarAdapterUtil.CONF_GATEGORY.Producer.label, + PulsarAdapterUtil.PRODUCER_CONF_STD_KEY.topicName.label, + cycleTopicName); + + String producerName = getEffectiveConValue( + PulsarAdapterUtil.CONF_GATEGORY.Producer.label, + PulsarAdapterUtil.PRODUCER_CONF_STD_KEY.producerName.label, + cycleProducerName); + + String producerCacheKey = PulsarAdapterUtil.buildCacheKey(producerName, topicName); + Producer producer = pulsarSpace.getProducer(producerCacheKey); + + if (producer == null) { + PulsarClient pulsarClient = pulsarSpace.getPulsarClient(); + + // Get other possible producer settings that are set at global level + Map producerConf = pulsarSpace.getPulsarNBClientConf().getProducerConfMapTgt(); + + // Remove global level settings + producerConf.remove(PulsarAdapterUtil.PRODUCER_CONF_STD_KEY.topicName.label); + producerConf.remove(PulsarAdapterUtil.PRODUCER_CONF_STD_KEY.producerName.label); + + try { + ProducerBuilder producerBuilder = pulsarClient. + newProducer(pulsarSpace.getPulsarSchema()). + loadConf(producerConf). + topic(topicName); + + if (!StringUtils.isAnyBlank(producerName)) { + producerBuilder = producerBuilder.producerName(producerName); + } + + producer = producerBuilder.create(); + pulsarSpace.setProducer(producerCacheKey, producer); + + pulsarAdapterMetrics.registerProducerApiMetrics(producer, + getPulsarAPIMetricsPrefix( + PulsarAdapterUtil.PULSAR_API_TYPE.PRODUCER.label, + producerName, + topicName)); + } + catch (PulsarClientException ple) { + throw new PulsarAdapterUnexpectedException("Failed to create a Pulsar producer."); + } + } + + return producer; + } + + private List getEffectiveConsumerTopicNameList(String cycleTopicNameListStr) { + String effectiveTopicNamesStr = getEffectiveConValue( + PulsarAdapterUtil.CONF_GATEGORY.Consumer.label, + PulsarAdapterUtil.CONSUMER_CONF_STD_KEY.topicNames.label, + cycleTopicNameListStr); + + String[] names = effectiveTopicNamesStr.split("[;,]"); + ArrayList effectiveTopicNameList = new ArrayList<>(); + + for (String name : names) { + if (!StringUtils.isBlank(name)) + effectiveTopicNameList.add(name.trim()); + } + + return effectiveTopicNameList; + } + + private Pattern getEffectiveConsumerTopicPattern(String cycleTopicPatternStr) { + String effectiveTopicPatternStr = getEffectiveConValue( + PulsarAdapterUtil.CONF_GATEGORY.Consumer.label, + PulsarAdapterUtil.CONSUMER_CONF_STD_KEY.topicsPattern.label, + cycleTopicPatternStr); + + Pattern topicsPattern; + try { + if (!StringUtils.isBlank(effectiveTopicPatternStr)) + topicsPattern = Pattern.compile(effectiveTopicPatternStr); + else + topicsPattern = null; + } catch (PatternSyntaxException pse) { + topicsPattern = null; + } + return topicsPattern; + } + + private SubscriptionType getEffectiveSubscriptionType(String cycleSubscriptionType) { + String subscriptionTypeStr = getEffectiveConValue( + PulsarAdapterUtil.CONF_GATEGORY.Consumer.label, + PulsarAdapterUtil.CONSUMER_CONF_STD_KEY.subscriptionType.label, + cycleSubscriptionType); + + SubscriptionType subscriptionType = SubscriptionType.Exclusive; // default subscription type + if (!StringUtils.isBlank(subscriptionTypeStr)) { + try { + subscriptionType = SubscriptionType.valueOf(subscriptionTypeStr); + } + catch (Exception e) { + throw new PulsarAdapterInvalidParamException( + "Invalid effective subscription type for a consumer (\"" + subscriptionTypeStr + "\"). " + + "It must be one of the following values: " + PulsarAdapterUtil.getValidSubscriptionTypeList()); + } + } + + return subscriptionType; + } + + public Consumer getConsumer(String cycleTopicNameListStr, + String cycleTopicPatternStr, + String cycleSubscriptionName, + String cycleSubscriptionType, + String cycleConsumerName, + String cycleKeySharedSubscriptionRanges) { + + List topicNameList = getEffectiveConsumerTopicNameList(cycleTopicNameListStr); + + String topicPatternStr = getEffectiveConValue( + PulsarAdapterUtil.CONF_GATEGORY.Consumer.label, + PulsarAdapterUtil.CONSUMER_CONF_STD_KEY.topicsPattern.label, + cycleTopicPatternStr); + Pattern topicPattern = getEffectiveConsumerTopicPattern(cycleTopicPatternStr); + + String subscriptionName = getEffectiveConValue( + PulsarAdapterUtil.CONF_GATEGORY.Consumer.label, + PulsarAdapterUtil.CONSUMER_CONF_STD_KEY.subscriptionName.label, + cycleSubscriptionName); + + SubscriptionType subscriptionType = getEffectiveSubscriptionType(cycleSubscriptionType); + + String consumerName = getEffectiveConValue( + PulsarAdapterUtil.CONF_GATEGORY.Consumer.label, + PulsarAdapterUtil.CONSUMER_CONF_STD_KEY.consumerName.label, + cycleConsumerName); + + if ( subscriptionType.equals(SubscriptionType.Exclusive) && (totalThreadNum > 1) ) { + throw new PulsarAdapterInvalidParamException( + PulsarAdapterUtil.CONSUMER_CONF_STD_KEY.subscriptionType.label, + "creating multiple consumers of \"Exclusive\" subscription type under the same subscription name"); + } + + if ( (topicNameList.isEmpty() && (topicPattern == null)) || + (!topicNameList.isEmpty() && (topicPattern != null)) ) { + throw new PulsarAdapterInvalidParamException( + "Invalid combination of topic name(s) and topic patterns; only specify one parameter!"); + } + + boolean multiTopicConsumer = (topicNameList.size() > 1 || (topicPattern != null)); + + String consumerTopicListString; + if (!topicNameList.isEmpty()) { + consumerTopicListString = String.join("|", topicNameList); + } else { + consumerTopicListString = topicPatternStr; + } + + String consumerCacheKey = PulsarAdapterUtil.buildCacheKey( + consumerName, + subscriptionName, + consumerTopicListString); + Consumer consumer = pulsarSpace.getConsumer(consumerCacheKey); + + if (consumer == null) { + PulsarClient pulsarClient = pulsarSpace.getPulsarClient(); + + // Get other possible consumer settings that are set at global level + Map consumerConf = + new HashMap<>(pulsarSpace.getPulsarNBClientConf().getConsumerConfMapTgt()); + Map consumerConfToLoad = new HashMap<>(); + consumerConfToLoad.putAll(consumerConf); + + try { + ConsumerBuilder consumerBuilder; + + // Remove settings that will be handled outside "loadConf()" + consumerConfToLoad.remove(PulsarAdapterUtil.CONSUMER_CONF_STD_KEY.topicNames.label); + consumerConfToLoad.remove(PulsarAdapterUtil.CONSUMER_CONF_STD_KEY.topicsPattern.label); + consumerConfToLoad.remove(PulsarAdapterUtil.CONSUMER_CONF_STD_KEY.subscriptionName.label); + consumerConfToLoad.remove(PulsarAdapterUtil.CONSUMER_CONF_STD_KEY.subscriptionType.label); + consumerConfToLoad.remove(PulsarAdapterUtil.CONSUMER_CONF_STD_KEY.consumerName.label); + + // TODO: It looks like loadConf() method can't handle the following settings properly. + // Do these settings manually for now + // - deadLetterPolicy + // - negativeAckRedeliveryBackoff + // - ackTimeoutRedeliveryBackoff + consumerConfToLoad.remove(PulsarAdapterUtil.CONSUMER_CONF_STD_KEY.deadLetterPolicy.label); + consumerConfToLoad.remove(PulsarAdapterUtil.CONSUMER_CONF_STD_KEY.negativeAckRedeliveryBackoff.label); + consumerConfToLoad.remove(PulsarAdapterUtil.CONSUMER_CONF_STD_KEY.ackTimeoutRedeliveryBackoff.label); + + if (!multiTopicConsumer) { + assert (topicNameList.size() == 1); + consumerBuilder = pulsarClient.newConsumer(pulsarSpace.getPulsarSchema()); + consumerBuilder.topic(topicNameList.get(0)); + } + else { + consumerBuilder = pulsarClient.newConsumer(); + if (!topicNameList.isEmpty()) { + assert (topicNameList.size() > 1); + consumerBuilder.topics(topicNameList); + } + else { + consumerBuilder.topicsPattern(topicPattern); + } + } + + consumerBuilder.loadConf(consumerConfToLoad); + + if (consumerConf.containsKey(PulsarAdapterUtil.CONSUMER_CONF_STD_KEY.deadLetterPolicy.label)) { + consumerBuilder.deadLetterPolicy((DeadLetterPolicy) + consumerConf.get(PulsarAdapterUtil.CONSUMER_CONF_STD_KEY.deadLetterPolicy.label)); + } + if (consumerConf.containsKey(PulsarAdapterUtil.CONSUMER_CONF_STD_KEY.negativeAckRedeliveryBackoff.label)) { + consumerBuilder.negativeAckRedeliveryBackoff((RedeliveryBackoff) + consumerConf.get(PulsarAdapterUtil.CONSUMER_CONF_STD_KEY.negativeAckRedeliveryBackoff.label)); + } + if (consumerConf.containsKey(PulsarAdapterUtil.CONSUMER_CONF_STD_KEY.ackTimeoutRedeliveryBackoff.label)) { + consumerBuilder.ackTimeoutRedeliveryBackoff((RedeliveryBackoff) + consumerConf.get(PulsarAdapterUtil.CONSUMER_CONF_STD_KEY.ackTimeoutRedeliveryBackoff.label)); + } + + consumerBuilder + .subscriptionName(subscriptionName) + .subscriptionType(subscriptionType); + + if (!StringUtils.isBlank(consumerName)) + consumerBuilder.consumerName(consumerName); + + if (subscriptionType == SubscriptionType.Key_Shared) { + KeySharedPolicy keySharedPolicy = KeySharedPolicy.autoSplitHashRange(); + if (cycleKeySharedSubscriptionRanges != null && !cycleKeySharedSubscriptionRanges.isEmpty()) { + Range[] ranges = parseRanges(cycleKeySharedSubscriptionRanges); + logger.info("Configuring KeySharedPolicy#stickyHashRange with ranges {}", ranges); + keySharedPolicy = KeySharedPolicy.stickyHashRange().ranges(ranges); + } + consumerBuilder.keySharedPolicy(keySharedPolicy); + } + + consumer = consumerBuilder.subscribe(); + pulsarSpace.setConsumer(consumerCacheKey, consumer); + + pulsarAdapterMetrics.registerConsumerApiMetrics( + consumer, + getPulsarAPIMetricsPrefix( + PulsarAdapterUtil.PULSAR_API_TYPE.CONSUMER.label, + consumerName, + consumerTopicListString)); + } + catch (PulsarClientException ple) { + throw new PulsarAdapterUnexpectedException("Failed to create a Pulsar consumer!"); + } + } + + return consumer; + } + + private static Range[] parseRanges(String ranges) { + if (ranges == null || ranges.isEmpty()) { + return new Range[0]; + } + String[] split = ranges.split(","); + Range[] result = new Range[split.length]; + for (int i = 0; i < split.length; i++) { + String range = split[i]; + int pos = range.indexOf(".."); + if (pos <= 0) { + throw new IllegalArgumentException("Invalid range '" + range + "'"); + } + try { + int start = Integer.parseInt(range.substring(0, pos)); + int end = Integer.parseInt(range.substring(pos + 2)); + result[i] = Range.of(start, end); + } catch (NumberFormatException err) { + throw new IllegalArgumentException("Invalid range '" + range + "'"); + } + } + return result; + } + + public Reader getReader(String cycleTopicName, + String cycleReaderName, + String cycleStartMsgPos) { + + String topicName = getEffectiveConValue( + PulsarAdapterUtil.CONF_GATEGORY.Reader.label, + PulsarAdapterUtil.READER_CONF_STD_KEY.topicName.label, + cycleTopicName); + + String readerName = getEffectiveConValue( + PulsarAdapterUtil.CONF_GATEGORY.Reader.label, + PulsarAdapterUtil.READER_CONF_STD_KEY.readerName.label, + cycleReaderName); + + String startMsgPosStr = getEffectiveConValue( + PulsarAdapterUtil.CONF_GATEGORY.Reader.label, + PulsarAdapterUtil.READER_CONF_CUSTOM_KEY.startMessagePos.label, + cycleStartMsgPos); + if (!PulsarAdapterUtil.isValideReaderStartPosition(startMsgPosStr)) { + throw new RuntimeException("Reader:: Invalid value for reader start message position!"); + } + + String readerCacheKey = PulsarAdapterUtil.buildCacheKey(topicName, readerName, startMsgPosStr); + Reader reader = pulsarSpace.getReader(readerCacheKey); + + if (reader == null) { + PulsarClient pulsarClient = pulsarSpace.getPulsarClient();; + + Map readerConf = pulsarSpace.getPulsarNBClientConf().getReaderConfMapTgt(); + + // Remove global level settings: "topicName" and "readerName" + readerConf.remove(PulsarAdapterUtil.READER_CONF_STD_KEY.topicName.label); + readerConf.remove(PulsarAdapterUtil.READER_CONF_STD_KEY.readerName.label); + // Remove non-standard reader configuration properties + readerConf.remove(PulsarAdapterUtil.READER_CONF_CUSTOM_KEY.startMessagePos.label); + + try { + ReaderBuilder readerBuilder = pulsarClient. + newReader(pulsarSpace.getPulsarSchema()). + loadConf(readerConf). + topic(topicName). + readerName(readerName); + + MessageId startMsgId = MessageId.latest; + if (startMsgPosStr.equalsIgnoreCase(PulsarAdapterUtil.READER_MSG_POSITION_TYPE.earliest.label)) { + startMsgId = MessageId.earliest; + } + //TODO: custom start message position is NOT supported yet + //else if (startMsgPosStr.startsWith(PulsarAdapterUtil.READER_MSG_POSITION_TYPE.custom.label)) { + // startMsgId = MessageId.latest; + //} + + reader = readerBuilder.startMessageId(startMsgId).create(); + + } catch (PulsarClientException ple) { + ple.printStackTrace(); + throw new RuntimeException("Unable to create a Pulsar reader!"); + } + + pulsarSpace.setReader(readerCacheKey, reader); + } + + return reader; + } + // + ////////////////////////////////////// + // Reader Processing <-- end + ////////////////////////////////////// +} diff --git a/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/dispensers/PulsarClientOpDispenser.java b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/dispensers/PulsarClientOpDispenser.java new file mode 100644 index 000000000..87e3c32fb --- /dev/null +++ b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/dispensers/PulsarClientOpDispenser.java @@ -0,0 +1,127 @@ +/* + * 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.pulsar.dispensers; + +import com.codahale.metrics.Timer; +import io.nosqlbench.adapter.pulsar.PulsarSpace; +import io.nosqlbench.adapter.pulsar.util.PulsarAdapterUtil; +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 org.apache.pulsar.client.api.PulsarClient; +import org.apache.pulsar.client.api.PulsarClientException; +import org.apache.pulsar.client.api.Schema; +import org.apache.pulsar.client.api.transaction.Transaction; + +import java.util.*; +import java.util.concurrent.ExecutionException; +import java.util.function.LongFunction; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +public abstract class PulsarClientOpDispenser extends PulsarBaseOpDispenser { + + private final static Logger logger = LogManager.getLogger("PulsarClientOpDispenser"); + + protected final PulsarClient pulsarClient; + protected final Schema pulsarSchema; + + protected final LongFunction useTransactFunc; + // TODO: add support for "operation number per transaction" + // protected final LongFunction transactBatchNumFunc; + protected final LongFunction seqTrackingFunc; + protected final LongFunction payloadRttFieldFunc; + protected final LongFunction> transactSupplierFunc; + protected final LongFunction> msgSeqErrSimuTypeSetFunc; + + public PulsarClientOpDispenser(DriverAdapter adapter, + ParsedOp op, + LongFunction tgtNameFunc, + PulsarSpace pulsarSpace) { + super(adapter, op, tgtNameFunc, pulsarSpace); + + this.pulsarClient = pulsarSpace.getPulsarClient(); + this.pulsarSchema = pulsarSpace.getPulsarSchema(); + + // Doc-level parameter: use_transaction + this.useTransactFunc = lookupStaticBoolConfigValueFunc( + PulsarAdapterUtil.DOC_LEVEL_PARAMS.USE_TRANSACTION.label, false); + + // TODO: add support for "operation number per transaction" + // Doc-level parameter: transact_batch_num + // this.transactBatchNumFunc = lookupStaticIntOpValueFunc( + // PulsarAdapterUtil.DOC_LEVEL_PARAMS.TRANSACT_BATCH_NUM.label, 1); + + // Doc-level parameter: seq_tracking + this.seqTrackingFunc = lookupStaticBoolConfigValueFunc( + PulsarAdapterUtil.DOC_LEVEL_PARAMS.SEQ_TRACKING.label, false); + + // Doc-level parameter: payload-tracking-field + this.payloadRttFieldFunc = (l) -> parsedOp.getStaticConfigOr( + PulsarAdapterUtil.DOC_LEVEL_PARAMS.RTT_TRACKING_FIELD.label, ""); + + this.transactSupplierFunc = (l) -> getTransactionSupplier(); + + this.msgSeqErrSimuTypeSetFunc = getStaticErrSimuTypeSetOpValueFunc(); + } + + protected Supplier getTransactionSupplier() { + return () -> { + try (Timer.Context time = pulsarAdapterMetrics.getCommitTransactionTimer().time() ){ + return pulsarClient + .newTransaction() + .build() + .get(); + } catch (ExecutionException | InterruptedException err) { + if (logger.isWarnEnabled()) { + logger.warn("Error while starting a new transaction", err); + } + throw new RuntimeException(err); + } catch (PulsarClientException err) { + throw new RuntimeException("Transactions are not enabled on Pulsar Client, " + + "please set client.enableTransaction=true in your Pulsar Client configuration"); + } + }; + } + + protected LongFunction> getStaticErrSimuTypeSetOpValueFunc() { + LongFunction> setStringLongFunction; + setStringLongFunction = (l) -> + parsedOp.getOptionalStaticValue(PulsarAdapterUtil.DOC_LEVEL_PARAMS.SEQERR_SIMU.label, String.class) + .filter(Predicate.not(String::isEmpty)) + .map(value -> { + Set set = new HashSet<>(); + + if (StringUtils.contains(value,',')) { + set = Arrays.stream(value.split(",")) + .map(PulsarAdapterUtil.MSG_SEQ_ERROR_SIMU_TYPE::parseSimuType) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + + return set; + }).orElse(Collections.emptySet()); + logger.info( + PulsarAdapterUtil.DOC_LEVEL_PARAMS.SEQERR_SIMU.label + ": {}", + setStringLongFunction.apply(0)); + return setStringLongFunction; + } +} diff --git a/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/exception/PulsarAdapterAsyncOperationFailedException.java b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/exception/PulsarAdapterAsyncOperationFailedException.java new file mode 100644 index 000000000..3cb31e4ba --- /dev/null +++ b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/exception/PulsarAdapterAsyncOperationFailedException.java @@ -0,0 +1,26 @@ +/* + * 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.pulsar.exception; + +public class PulsarAdapterAsyncOperationFailedException extends RuntimeException { + + public PulsarAdapterAsyncOperationFailedException(Throwable t) { + super(t); + printStackTrace(); + } +} diff --git a/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/exception/PulsarAdapterInvalidParamException.java b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/exception/PulsarAdapterInvalidParamException.java new file mode 100644 index 000000000..5996e8573 --- /dev/null +++ b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/exception/PulsarAdapterInvalidParamException.java @@ -0,0 +1,29 @@ +/* + * 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.pulsar.exception; + +public class PulsarAdapterInvalidParamException extends RuntimeException { + + public PulsarAdapterInvalidParamException(String paramName, String errDesc) { + super("Invalid setting for parameter (" + paramName + "): " + errDesc); + } + + public PulsarAdapterInvalidParamException(String fullErrDesc) { + super(fullErrDesc); + } +} diff --git a/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/exception/PulsarAdapterUnexpectedException.java b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/exception/PulsarAdapterUnexpectedException.java new file mode 100644 index 000000000..4f0031fce --- /dev/null +++ b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/exception/PulsarAdapterUnexpectedException.java @@ -0,0 +1,30 @@ +/* + * 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.pulsar.exception; + +public class PulsarAdapterUnexpectedException extends RuntimeException { + + public PulsarAdapterUnexpectedException(String message) { + super(message); + printStackTrace(); + } + public PulsarAdapterUnexpectedException(Exception e) { + super(e); + printStackTrace(); + } +} diff --git a/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/exception/PulsarAdapterUnsupportedOpException.java b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/exception/PulsarAdapterUnsupportedOpException.java new file mode 100644 index 000000000..475d358ea --- /dev/null +++ b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/exception/PulsarAdapterUnsupportedOpException.java @@ -0,0 +1,25 @@ +/* + * 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.pulsar.exception; + +public class PulsarAdapterUnsupportedOpException extends RuntimeException { + + public PulsarAdapterUnsupportedOpException(String pulsarOpType) { + super("Unsupported Pulsar adapter operation type: \"" + pulsarOpType + "\""); + } +} diff --git a/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/ops/AdminNamespaceOp.java b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/ops/AdminNamespaceOp.java new file mode 100644 index 000000000..2caca86bc --- /dev/null +++ b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/ops/AdminNamespaceOp.java @@ -0,0 +1,122 @@ +/* + * 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.pulsar.ops; + +import io.nosqlbench.adapter.pulsar.exception.PulsarAdapterUnexpectedException; +import io.nosqlbench.adapter.pulsar.util.PulsarAdapterMetrics; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.pulsar.client.admin.Namespaces; +import org.apache.pulsar.client.admin.PulsarAdmin; +import org.apache.pulsar.client.admin.PulsarAdminException; + +import java.util.concurrent.CompletableFuture; + +public class AdminNamespaceOp extends PulsarAdminOp { + + private final static Logger logger = LogManager.getLogger(AdminNamespaceOp.class); + + // in format: / + private final String nsName; + + public AdminNamespaceOp(PulsarAdapterMetrics pulsarAdapterMetrics, + PulsarAdmin pulsarAdmin, + boolean asyncApi, + boolean adminDelOp, + String nsName) { + super(pulsarAdapterMetrics, pulsarAdmin, asyncApi, adminDelOp); + this.nsName = nsName; + } + + @Override + public Void apply(long value) { + + // Do nothing if the namespace name is empty + if ( !StringUtils.isBlank(nsName) ) { + + Namespaces namespaces = pulsarAdmin.namespaces(); + + // Admin API - create tenants and namespaces + if (!adminDelOp) { + try { + if (!asyncApi) { + namespaces.createNamespace(nsName); + if (logger.isDebugEnabled()) { + logger.debug("Successful sync creation of namespace \"{}\"", nsName); + } + } else { + CompletableFuture future = namespaces.createNamespaceAsync(nsName); + future.whenComplete((unused, throwable) -> { + if (logger.isDebugEnabled()) { + logger.trace("Successful async creation of namespace \"{}\"", nsName); + } + }).exceptionally(ex -> { + if (logger.isDebugEnabled()) { + logger.error("Failed async creation of namespace \"{}\"", nsName); + } + return null; + }); + } + } + catch (PulsarAdminException.ConflictException ce) { + if (logger.isDebugEnabled()) { + logger.error("Namespace \"{}\" already exists - skip creation!", nsName); + } + } + catch (PulsarAdminException e) { + throw new PulsarAdapterUnexpectedException( + "Unexpected error when creating pulsar namespace \"" + nsName + "\""); + } + } + // Admin API - delete tenants and namespaces + else { + try { + if (!asyncApi) { + namespaces.deleteNamespace(nsName); + if (logger.isDebugEnabled()) { + logger.debug("Successful sync deletion of namespace \"{}\"", nsName); + } + } else { + CompletableFuture future = namespaces.deleteNamespaceAsync(nsName, true); + future.whenComplete((unused, throwable) -> { + if (logger.isDebugEnabled()) { + logger.debug("Successful sync deletion of namespace \"{}\"", nsName); + } + }).exceptionally(ex -> { + if (logger.isDebugEnabled()) { + logger.error("Failed async deletion of namespace \"{}\"", nsName); + } + return null; + }); + } + } + catch (PulsarAdminException.NotFoundException nfe) { + if (logger.isDebugEnabled()) { + logger.error("Namespace \"{}\" doesn't exists - skip deletion!", nsName); + } + } + catch (PulsarAdminException e) { + throw new PulsarAdapterUnexpectedException( + "Unexpected error when deleting pulsar namespace \"" + nsName + "\""); + } + } + } + + return null; + } +} diff --git a/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/ops/AdminTenantOp.java b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/ops/AdminTenantOp.java new file mode 100644 index 000000000..f3050e82a --- /dev/null +++ b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/ops/AdminTenantOp.java @@ -0,0 +1,147 @@ +/* + * 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.pulsar.ops; + +import io.nosqlbench.adapter.pulsar.PulsarDriverAdapter; +import io.nosqlbench.adapter.pulsar.exception.PulsarAdapterUnexpectedException; +import io.nosqlbench.adapter.pulsar.util.PulsarAdapterMetrics; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.pulsar.client.admin.*; +import org.apache.pulsar.common.policies.data.TenantInfo; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +public class AdminTenantOp extends PulsarAdminOp { + + private final static Logger logger = LogManager.getLogger(AdminTenantOp.class); + + private final Set adminRoles; + private final Set allowedClusters; + private final String tntName; + + public AdminTenantOp(PulsarAdapterMetrics pulsarAdapterMetrics, + PulsarAdmin pulsarAdmin, + boolean asyncApi, + boolean adminDelOp, + String tntName, + Set adminRoles, + Set allowedClusters) { + super(pulsarAdapterMetrics, pulsarAdmin, asyncApi, adminDelOp); + + this.tntName = tntName; + this.adminRoles = adminRoles; + this.allowedClusters = allowedClusters; + } + + @Override + public Void apply(long value) { + + // Do nothing if the tenant name is empty + if ( !StringUtils.isBlank(tntName) ) { + Tenants tenants = pulsarAdmin.tenants(); + + // Admin API - create tenants and namespaces + if (!adminDelOp) { + try { + Set existingPulsarClusters = new HashSet<>(); + Clusters clusters = pulsarAdmin.clusters(); + CollectionUtils.addAll(existingPulsarClusters, clusters.getClusters().listIterator()); + + TenantInfo tenantInfo = TenantInfo.builder() + .adminRoles(adminRoles) + .allowedClusters(!allowedClusters.isEmpty() ? allowedClusters : existingPulsarClusters) + .build(); + + if (!asyncApi) { + tenants.createTenant(tntName, tenantInfo); + if (logger.isDebugEnabled()) { + logger.debug("Successful sync creation of tenant \"{}\"", tntName); + } + } + else { + CompletableFuture future = tenants.createTenantAsync(tntName, tenantInfo); + future.whenComplete((unused, throwable) -> { + if (logger.isDebugEnabled()) { + logger.debug("Successful async creation of tenant \"{}\"", tntName); + } + }).exceptionally(ex -> { + if (logger.isDebugEnabled()) { + logger.error("Failed async creation of tenant \"{}\"", tntName); + } + return null; + }); + } + } + catch (PulsarAdminException.ConflictException ce) { + if (logger.isDebugEnabled()) { + logger.error("Tenant \"{}\" already exists - skip creation!", tntName); + } + } + catch (PulsarAdminException e) { + throw new PulsarAdapterUnexpectedException( + "Unexpected error when creating pulsar tenant \"" + tntName + "\""); + } + } + // Admin API - delete tenants and namespaces + else { + try { + Namespaces namespaces = pulsarAdmin.namespaces(); + int nsNum = namespaces.getNamespaces(tntName).size(); + + // Only delete a tenant when there is no underlying namespaces + if ( nsNum == 0 ) { + if (!asyncApi) { + tenants.deleteTenant(tntName); + if (logger.isDebugEnabled()) { + logger.debug("Successful sync deletion of tenant \"{}\"", tntName); + } + } + else { + CompletableFuture future = tenants.deleteTenantAsync(tntName); + future.whenComplete((unused, throwable) -> { + if (logger.isDebugEnabled()) { + logger.debug("Successful async deletion of tenant \"{}\"", tntName); + } + }).exceptionally(ex -> { + if (logger.isDebugEnabled()) { + logger.error("Failed async deletion of tenant \"{}\"", tntName); + } + return null; + }); + } + } + } + catch (PulsarAdminException.NotFoundException nfe) { + if (logger.isDebugEnabled()) { + logger.error("Tenant \"{}\" doesn't exists - skip deletion!", tntName); + } + } + catch (PulsarAdminException e) { + throw new PulsarAdapterUnexpectedException( + "Unexpected error when deleting pulsar tenant \"" + tntName + "\""); + } + } + } + + return null; + } +} diff --git a/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/ops/AdminTopicOp.java b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/ops/AdminTopicOp.java new file mode 100644 index 000000000..507c13199 --- /dev/null +++ b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/ops/AdminTopicOp.java @@ -0,0 +1,184 @@ +/* + * 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.pulsar.ops; + +import io.nosqlbench.adapter.pulsar.exception.PulsarAdapterUnexpectedException; +import io.nosqlbench.adapter.pulsar.util.PulsarAdapterMetrics; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.pulsar.client.admin.PulsarAdmin; +import org.apache.pulsar.client.admin.PulsarAdminException; +import org.apache.pulsar.client.admin.Topics; + +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +public class AdminTopicOp extends PulsarAdminOp { + + private final static Logger logger = LogManager.getLogger(AdminTopicOp.class); + + private final String topicName; + private final boolean enablePart; + private final int partNum; + + public AdminTopicOp(PulsarAdapterMetrics pulsarAdapterMetrics, + PulsarAdmin pulsarAdmin, + boolean asyncApi, + boolean adminDelOp, + String topicName, + boolean enablePart, + int partNum) { + super(pulsarAdapterMetrics, pulsarAdmin, asyncApi, adminDelOp); + + this.topicName = topicName; + this.enablePart = enablePart; + this.partNum = partNum; + } + + @Override + public Void apply(long value) { + + // Do nothing if the topic name is empty + if ( !StringUtils.isBlank(topicName) ) { + Topics topics = pulsarAdmin.topics(); + + try { + // Create the topic + if (!adminDelOp) { + if (!enablePart) { + if (!asyncApi) { + topics.createNonPartitionedTopic(topicName); + if (logger.isDebugEnabled()) { + logger.debug("Successful sync creation of non-partitioned topic \"{}\"", topicName); + } + } else { + CompletableFuture future = topics.createNonPartitionedTopicAsync(topicName); + future.whenComplete((unused, throwable) -> { + if (logger.isDebugEnabled()) { + logger.debug("Successful async creation of non-partitioned topic \"{}\"", topicName); + } + }).exceptionally(ex -> { + if (logger.isDebugEnabled()) { + logger.error("Failed async creation non-partitioned topic \"{}\"", topicName); + return null; + } + return null; + }); + } + } else { + if (!asyncApi) { + topics.createPartitionedTopic(topicName, partNum); + if (logger.isDebugEnabled()) { + logger.debug( + "Successful sync creation of partitioned topic \"{} (partition_num: {}\")", + topicName, partNum); + } + } else { + CompletableFuture future = topics.createPartitionedTopicAsync(topicName, partNum); + future.whenComplete((unused, throwable) -> { + if (logger.isDebugEnabled()) { + logger.debug( + "Successful async creation of partitioned topic \"{} (partition_num: {}\")", + topicName, partNum); + } + }) + .exceptionally(ex -> { + if (logger.isDebugEnabled()) { + logger.error( + "Successful async creation of partitioned topic \"{} (partition_num: {}\")", + topicName, partNum); + } + return null; + }); + } + } + } + // Delete the topic + else { + if (!enablePart) { + if (!asyncApi) { + topics.delete(topicName); + if (logger.isDebugEnabled()) { + logger.debug( + "Successful sync deletion of non-partitioned topic \"{}\"", + topicName); + } + } else { + CompletableFuture future = topics.deleteAsync(topicName); + future.whenComplete((unused, throwable) -> { + if (logger.isDebugEnabled()) { + logger.debug( + "Successful async deletion of non-partitioned topic \"{}\"", + topicName); + } + }) + .exceptionally(ex -> { + if (logger.isDebugEnabled()) { + logger.error( + "Failed async deletion of non-partitioned topic \"{}\"", + topicName); + } + return null; + }); + } + } else { + if (!asyncApi) { + topics.deletePartitionedTopic(topicName); + if (logger.isDebugEnabled()) { + logger.debug( + "Successful sync deletion of partitioned topic \"{} (partition_num: {}\")", + topicName, partNum); + } + } else { + CompletableFuture future = topics.deletePartitionedTopicAsync(topicName); + future.whenComplete((unused, throwable) -> { + if (logger.isDebugEnabled()) { + logger.debug( + "Successful async deletion of partitioned topic \"{} (partition_num: {}\")", + topicName, partNum); + } + }).exceptionally(ex -> { + if (logger.isDebugEnabled()) { + logger.error( + "Failed async deletion of partitioned topic \"{} (partition_num: {}\")", + topicName, partNum); + } + return null; + }); + } + } + } + } + catch (PulsarAdminException.NotFoundException nfe) { + if (logger.isDebugEnabled()) { + logger.error("Topic \"{}\" doesn't exists - skip deletion!", topicName); + } + } + catch (PulsarAdminException e) { + String errMsg = String.format("Unexpected error when %s pulsar topic: %s (partition enabled: %b; partition number: %d)", + (!adminDelOp ? "creating" : "deleting"), + topicName, + enablePart, + partNum); + throw new PulsarAdapterUnexpectedException(errMsg); + } + } + + return null; + } +} diff --git a/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/ops/MessageConsumerOp.java b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/ops/MessageConsumerOp.java new file mode 100644 index 000000000..40b81ceac --- /dev/null +++ b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/ops/MessageConsumerOp.java @@ -0,0 +1,300 @@ +/* + * 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.pulsar.ops; + +import com.codahale.metrics.Timer; +import io.nosqlbench.adapter.pulsar.exception.PulsarAdapterAsyncOperationFailedException; +import io.nosqlbench.adapter.pulsar.exception.PulsarAdapterUnexpectedException; +import io.nosqlbench.adapter.pulsar.util.*; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.pulsar.client.api.*; +import org.apache.pulsar.client.api.schema.GenericObject; +import org.apache.pulsar.client.api.schema.GenericRecord; +import org.apache.pulsar.client.api.transaction.Transaction; +import org.apache.pulsar.common.schema.KeyValue; +import org.apache.pulsar.shade.org.apache.avro.AvroRuntimeException; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Function; +import java.util.function.Supplier; + +public class MessageConsumerOp extends PulsarClientOp { + + private final static Logger logger = LogManager.getLogger(MessageConsumerOp.class); + + private final boolean useTransact; + private final boolean seqTracking; + private final Supplier transactSupplier; + private final String payloadRttField; + private final EndToEndStartingTimeSource e2eStartingTimeSrc; + private final Function receivedMessageSequenceTrackerForTopic; + private final Consumer consumer; + private final int consumerTimeoutInSec; + + public MessageConsumerOp(PulsarAdapterMetrics pulsarAdapterMetrics, + PulsarClient pulsarClient, + Schema pulsarSchema, + boolean asyncApi, + boolean useTransact, + boolean seqTracking, + Supplier transactSupplier, + String payloadRttField, + EndToEndStartingTimeSource e2eStartingTimeSrc, + Function receivedMessageSequenceTrackerForTopic, + Consumer consumer, + int consumerTimeoutInSec) { + super(pulsarAdapterMetrics, pulsarClient, pulsarSchema, asyncApi); + + this.useTransact = useTransact; + this.seqTracking = seqTracking; + this.transactSupplier = transactSupplier; + this.payloadRttField = payloadRttField; + this.e2eStartingTimeSrc = e2eStartingTimeSrc; + this.receivedMessageSequenceTrackerForTopic = receivedMessageSequenceTrackerForTopic; + this.consumer = consumer; + this.consumerTimeoutInSec = consumerTimeoutInSec; + } + + @Override + public Object apply(long value) { + final Transaction transaction; + if (useTransact) { + // if you are in a transaction you cannot set the schema per-message + transaction = transactSupplier.get(); + } + else { + transaction = null; + } + + if (!asyncApi) { + try { + Message message; + + if (consumerTimeoutInSec <= 0) { + // wait forever + message = consumer.receive(); + } + else { + message = consumer.receive(consumerTimeoutInSec, TimeUnit.SECONDS); + if (message == null) { + if ( logger.isDebugEnabled() ) { + logger.debug("Failed to sync-receive a message before time out ({} seconds)", consumerTimeoutInSec); + } + } + } + + handleMessage(transaction, message); + } + catch (Exception e) { + throw new PulsarAdapterUnexpectedException("" + + "Sync message receiving failed - timeout value: " + consumerTimeoutInSec + " seconds "); + } + } + else { + try { + CompletableFuture> msgRecvFuture = consumer.receiveAsync(); + if (useTransact) { + // add commit step + msgRecvFuture = msgRecvFuture.thenCompose(msg -> { + Timer.Context ctx = transactionCommitTimer.time(); + return transaction + .commit() + .whenComplete((m,e) -> ctx.close()) + .thenApply(v-> msg); + } + ); + } + + msgRecvFuture.thenAccept(message -> { + try { + handleMessage(transaction, message); + } catch (PulsarClientException | TimeoutException e) { + throw new PulsarAdapterAsyncOperationFailedException(e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (ExecutionException e) { + throw new PulsarAdapterAsyncOperationFailedException(e.getCause()); + } + }).exceptionally(ex -> { + throw new PulsarAdapterAsyncOperationFailedException(ex); + }); + } + catch (Exception e) { + throw new PulsarAdapterUnexpectedException(e); + } + } + + return null; + } + + private void handleMessage(Transaction transaction, Message message) + throws PulsarClientException, InterruptedException, ExecutionException, TimeoutException { + + // acknowledge the message as soon as possible + if (!useTransact) { + consumer.acknowledgeAsync(message.getMessageId()) + .get(consumerTimeoutInSec, TimeUnit.SECONDS); + } else { + consumer.acknowledgeAsync(message.getMessageId(), transaction) + .get(consumerTimeoutInSec, TimeUnit.SECONDS); + + // little problem: here we are counting the "commit" time + // inside the overall time spent for the execution of the consume operation + // we should refactor this operation as for PulsarProducerOp, and use the passed callback + // to track with precision the time spent for the operation and for the commit + try (Timer.Context ctx = transactionCommitTimer.time()) { + transaction.commit().get(); + } + } + + if (logger.isDebugEnabled()) { + Object decodedPayload = message.getValue(); + if (decodedPayload instanceof GenericObject) { + // GenericObject is a wrapper for Primitives, for AVRO/JSON structs and for KeyValu + // we fall here with a configured AVRO schema or with AUTO_CONSUME + GenericObject object = (GenericObject) decodedPayload; + logger.debug("({}) message received: msg-key={}; msg-properties={}; msg-payload={}", + consumer.getConsumerName(), + message.getKey(), + message.getProperties(), + object.getNativeObject() + ""); + } + else { + logger.debug("({}) message received: msg-key={}; msg-properties={}; msg-payload={}", + consumer.getConsumerName(), + message.getKey(), + message.getProperties(), + new String(message.getData())); + } + } + + if (!payloadRttField.isEmpty()) { + boolean done = false; + Object decodedPayload = message.getValue(); + Long extractedSendTime = null; + // if Pulsar is able to decode this it is better to let it do the work + // because Pulsar caches the Schema, handles Schema evolution + // as much efficiently as possible + if (decodedPayload instanceof GenericRecord) { // AVRO and AUTO_CONSUME + final GenericRecord pulsarGenericRecord = (GenericRecord) decodedPayload; + + Object field = null; + // KeyValue is a special wrapper in Pulsar to represent a pair of values + // a Key and a Value + Object nativeObject = pulsarGenericRecord.getNativeObject(); + if (nativeObject instanceof KeyValue) { + KeyValue keyValue = (KeyValue) nativeObject; + // look into the Key + if (keyValue.getKey() instanceof GenericRecord) { + GenericRecord keyPart = (GenericRecord) keyValue.getKey(); + try { + field = keyPart.getField(payloadRttField); + } catch (AvroRuntimeException err) { + // field is not in the key + logger.error("Cannot find {} in key {}: {}", payloadRttField, keyPart, err + ""); + } + } + // look into the Value + if (keyValue.getValue() instanceof GenericRecord && field == null) { + GenericRecord valuePart = (GenericRecord) keyValue.getValue(); + try { + field = valuePart.getField(payloadRttField); + } catch (AvroRuntimeException err) { + // field is not in the value + logger.error("Cannot find {} in value {}: {}", payloadRttField, valuePart, err + ""); + } + } + if (field == null) { + throw new RuntimeException("Cannot find field {}" + payloadRttField + " in " + keyValue.getKey() + " and " + keyValue.getValue()); + } + } else { + field = pulsarGenericRecord.getField(payloadRttField); + } + + if (field != null) { + if (field instanceof Number) { + extractedSendTime = ((Number) field).longValue(); + } else { + extractedSendTime = Long.valueOf(field.toString()); + } + } else { + logger.error("Cannot find {} in value {}", payloadRttField, pulsarGenericRecord); + } + done = true; + } + if (!done) { + org.apache.avro.Schema avroSchema = getAvroSchemaFromConfiguration(); + org.apache.avro.generic.GenericRecord avroGenericRecord = + PulsarAvroSchemaUtil.GetGenericRecord_ApacheAvro(avroSchema, message.getData()); + if (avroGenericRecord.hasField(payloadRttField)) { + extractedSendTime = (Long) avroGenericRecord.get(payloadRttField); + } + } + if (extractedSendTime != null) { + // fallout expects latencies in "ns" and not in "ms" + long delta = TimeUnit.MILLISECONDS + .toNanos(System.currentTimeMillis() - extractedSendTime); + payloadRttHistogram.update(delta); + } + } + + // keep track end-to-end message processing latency + if (e2eStartingTimeSrc != EndToEndStartingTimeSource.NONE) { + long startTimeStamp = 0L; + + switch (e2eStartingTimeSrc) { + case MESSAGE_PUBLISH_TIME: + startTimeStamp = message.getPublishTime(); + break; + case MESSAGE_EVENT_TIME: + startTimeStamp = message.getEventTime(); + break; + case MESSAGE_PROPERTY_E2E_STARTING_TIME: + String startingTimeProperty = message.getProperty("e2e_starting_time"); + startTimeStamp = startingTimeProperty != null ? Long.parseLong(startingTimeProperty) : 0L; + break; + } + + if (startTimeStamp != 0L) { + long e2eMsgLatency = System.currentTimeMillis() - startTimeStamp; + e2eMsgProcLatencyHistogram.update(e2eMsgLatency); + } + } + + // keep track of message errors and update error counters + if (seqTracking) checkAndUpdateMessageErrorCounter(message); + + int messageSize = message.getData().length; + messageSizeHistogram.update(messageSize); + } + + private void checkAndUpdateMessageErrorCounter(Message message) { + String msgSeqIdStr = message.getProperty(PulsarAdapterUtil.MSG_SEQUENCE_NUMBER); + + if ( !StringUtils.isBlank(msgSeqIdStr) ) { + long sequenceNumber = Long.parseLong(msgSeqIdStr); + ReceivedMessageSequenceTracker receivedMessageSequenceTracker = + receivedMessageSequenceTrackerForTopic.apply(message.getTopicName()); + receivedMessageSequenceTracker.sequenceNumberReceived(sequenceNumber); + } + } +} diff --git a/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/ops/MessageProducerOp.java b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/ops/MessageProducerOp.java new file mode 100644 index 000000000..0a226429c --- /dev/null +++ b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/ops/MessageProducerOp.java @@ -0,0 +1,280 @@ +/* + * 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.pulsar.ops; + +import com.codahale.metrics.Timer; +import io.nosqlbench.adapter.pulsar.exception.PulsarAdapterAsyncOperationFailedException; +import io.nosqlbench.adapter.pulsar.exception.PulsarAdapterUnexpectedException; +import io.nosqlbench.adapter.pulsar.util.MessageSequenceNumberSendingHandler; +import io.nosqlbench.adapter.pulsar.util.PulsarAdapterMetrics; +import io.nosqlbench.adapter.pulsar.util.PulsarAdapterUtil; +import io.nosqlbench.adapter.pulsar.util.PulsarAvroSchemaUtil; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.pulsar.client.api.*; +import org.apache.pulsar.client.api.schema.GenericRecord; +import org.apache.pulsar.client.api.schema.KeyValueSchema; +import org.apache.pulsar.client.api.transaction.Transaction; +import org.apache.pulsar.client.impl.schema.generic.GenericAvroSchema; +import org.apache.pulsar.common.schema.KeyValue; +import org.apache.pulsar.common.schema.SchemaType; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.function.Supplier; + +public class MessageProducerOp extends PulsarClientOp { + + private final static Logger logger = LogManager.getLogger("MessageProducerOp"); + + private final boolean useTransact; + private final boolean seqTracking; + private final Supplier transactSupplier; + private final Set errSimuTypeSet; + private final Producer producer; + private final String msgKey; + private final String msgPropRawJsonStr; + private final String msgValue; + + private final Map msgProperties = new HashMap<>(); + private final ThreadLocal> MessageSequenceNumberSendingHandlersThreadLocal = + ThreadLocal.withInitial(HashMap::new); + + public MessageProducerOp(PulsarAdapterMetrics pulsarAdapterMetrics, + PulsarClient pulsarClient, + Schema pulsarSchema, + boolean asyncApi, + boolean useTransact, + boolean seqTracking, + Supplier transactSupplier, + Set errSimuTypeSet, + Producer producer, + String msgKey, + String msgProp, + String msgValue) { + super(pulsarAdapterMetrics, pulsarClient, pulsarSchema, asyncApi); + + this.useTransact = useTransact; + this.seqTracking = seqTracking; + this.transactSupplier = transactSupplier; + this.errSimuTypeSet = errSimuTypeSet; + this.producer = producer; + this.msgKey = msgKey; + this.msgPropRawJsonStr = msgProp; + this.msgValue = msgValue; + + getMsgPropMapFromRawJsonStr(); + } + + private MessageSequenceNumberSendingHandler getMessageSequenceNumberSendingHandler(String topicName) { + return MessageSequenceNumberSendingHandlersThreadLocal.get() + .computeIfAbsent(topicName, k -> new MessageSequenceNumberSendingHandler()); + } + + // Check if msgPropJonStr is valid JSON string with a collection of key/value pairs + // - if Yes, convert it to a map + // - otherwise, log an error message and ignore message properties without throwing a runtime exception + private void getMsgPropMapFromRawJsonStr() { + if (!StringUtils.isBlank(msgPropRawJsonStr)) { + try { + msgProperties.putAll(PulsarAdapterUtil.convertJsonToMap(msgPropRawJsonStr)); + } + catch (Exception e) { + logger.error( + "Error parsing message property JSON string {}, ignore message properties!", + msgPropRawJsonStr); + } + } + + if (seqTracking) { + long nextSequenceNumber = getMessageSequenceNumberSendingHandler(producer.getTopic()) + .getNextSequenceNumber(errSimuTypeSet); + msgProperties.put(PulsarAdapterUtil.MSG_SEQUENCE_NUMBER, String.valueOf(nextSequenceNumber)); + } + } + + @Override + public Object apply(long value) { + + TypedMessageBuilder typedMessageBuilder; + + final Transaction transaction; + if (useTransact) { + // if you are in a transaction you cannot set the schema per-message + transaction = transactSupplier.get(); + typedMessageBuilder = producer.newMessage(transaction); + } + else { + transaction = null; + typedMessageBuilder = producer.newMessage(pulsarSchema); + } + + // set message key + if ( !StringUtils.isBlank(msgKey) && !(pulsarSchema instanceof KeyValueSchema) ) { + typedMessageBuilder = typedMessageBuilder.key(msgKey); + } + + // set message properties + if ( !msgPropRawJsonStr.isEmpty() ) { + typedMessageBuilder = typedMessageBuilder.properties(msgProperties); + } + + // set message payload + int messageSize; + SchemaType schemaType = pulsarSchema.getSchemaInfo().getType(); + if (pulsarSchema instanceof KeyValueSchema) { + KeyValueSchema keyValueSchema = (KeyValueSchema) pulsarSchema; + org.apache.avro.Schema avroSchema = getAvroSchemaFromConfiguration(); + GenericRecord payload = PulsarAvroSchemaUtil.GetGenericRecord_PulsarAvro( + (GenericAvroSchema) keyValueSchema.getValueSchema(), + avroSchema, + msgValue + ); + + org.apache.avro.Schema avroSchemaForKey = getKeyAvroSchemaFromConfiguration(); + GenericRecord key = PulsarAvroSchemaUtil.GetGenericRecord_PulsarAvro( + (GenericAvroSchema) keyValueSchema.getKeySchema(), + avroSchemaForKey, + msgKey + ); + + typedMessageBuilder = typedMessageBuilder.value(new KeyValue(key, payload)); + // TODO: add a way to calculate the message size for KEY_VALUE messages + messageSize = msgKey.length() + msgValue.length(); + } + else if (PulsarAdapterUtil.isAvroSchemaTypeStr(schemaType.name())) { + GenericRecord payload = PulsarAvroSchemaUtil.GetGenericRecord_PulsarAvro( + (GenericAvroSchema) pulsarSchema, + pulsarSchema.getSchemaInfo().getSchemaDefinition(), + msgValue + ); + typedMessageBuilder = typedMessageBuilder.value(payload); + // TODO: add a way to calculate the message size for AVRO messages + messageSize = msgValue.length(); + } else { + byte[] array = msgValue.getBytes(StandardCharsets.UTF_8); + typedMessageBuilder = typedMessageBuilder.value(array); + messageSize = array.length; + } + + messageSizeHistogram.update(messageSize); + + //TODO: add error handling with failed message production + if (!asyncApi) { + try { + logger.trace("Sending message"); + typedMessageBuilder.send(); + + if (useTransact) { + try (Timer.Context ctx = transactionCommitTimer.time()) { + transaction.commit().get(); + } + } + + if (logger.isDebugEnabled()) { + if (PulsarAdapterUtil.isAvroSchemaTypeStr(schemaType.name())) { + org.apache.avro.Schema avroSchema = getAvroSchemaFromConfiguration(); + org.apache.avro.generic.GenericRecord avroGenericRecord = + PulsarAvroSchemaUtil.GetGenericRecord_ApacheAvro(avroSchema, msgValue); + + logger.debug("({}) Sync message sent: msg-key={}; msg-properties={}; msg-payload={})", + producer.getProducerName(), + msgKey, + msgProperties, + avroGenericRecord.toString()); + } + else { + logger.debug("({}) Sync message sent; msg-key={}; msg-properties={}; msg-payload={}", + producer.getProducerName(), + msgKey, + msgProperties, + msgValue); + } + } + } + catch (PulsarClientException | ExecutionException | InterruptedException pce) { + String errMsg = + "Sync message sending failed: " + + "key - " + msgKey + "; " + + "properties - " + msgProperties + "; " + + "payload - " + msgValue; + + logger.trace(errMsg); + + throw new PulsarAdapterUnexpectedException(errMsg); + } + } + else { + try { + // we rely on blockIfQueueIsFull in order to throttle the request in this case + CompletableFuture future = typedMessageBuilder.sendAsync(); + + if (useTransact) { + // add commit step + future = future.thenCompose(msg -> { + Timer.Context ctx = transactionCommitTimer.time(); + return transaction + .commit() + .whenComplete((m,e) -> ctx.close()) + .thenApply(v-> msg); + } + ); + } + + future.whenComplete((messageId, error) -> { + if (logger.isDebugEnabled()) { + if (PulsarAdapterUtil.isAvroSchemaTypeStr(schemaType.name())) { + org.apache.avro.Schema avroSchema = getAvroSchemaFromConfiguration(); + org.apache.avro.generic.GenericRecord avroGenericRecord = + PulsarAvroSchemaUtil.GetGenericRecord_ApacheAvro(avroSchema, msgValue); + + logger.debug("({}) Aysnc message sent: msg-key={}; msg-properties={}; msg-payload={})", + producer.getProducerName(), + msgKey, + msgProperties, + avroGenericRecord.toString()); + } + else { + logger.debug("({}) Aysnc message sent: msg-key={}; msg-properties={}; msg-payload={}", + producer.getProducerName(), + msgKey, + msgProperties, + msgValue); + } + } + }).exceptionally(ex -> { + logger.error("Async message sending failed: " + + "key - " + msgKey + "; " + + "properties - " + msgProperties + "; " + + "payload - " + msgValue); + + throw new PulsarAdapterAsyncOperationFailedException(ex); + }); + } + catch (Exception e) { + throw new PulsarAdapterUnexpectedException(e); + } + } + + return null; + } +} diff --git a/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/ops/MessageReaderOp.java b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/ops/MessageReaderOp.java new file mode 100644 index 000000000..18ba4ac3e --- /dev/null +++ b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/ops/MessageReaderOp.java @@ -0,0 +1,48 @@ +/* + * 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.pulsar.ops; + +import io.nosqlbench.adapter.pulsar.util.PulsarAdapterMetrics; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.pulsar.client.api.PulsarClient; +import org.apache.pulsar.client.api.Reader; +import org.apache.pulsar.client.api.Schema; + +public class MessageReaderOp extends PulsarClientOp { + + private final static Logger logger = LogManager.getLogger(MessageReaderOp.class); + + private final Reader reader; + + public MessageReaderOp(PulsarAdapterMetrics pulsarAdapterMetrics, + PulsarClient pulsarClient, + Schema pulsarSchema, + boolean asyncApi, + Reader reader) { + super(pulsarAdapterMetrics, pulsarClient, pulsarSchema, asyncApi); + + this.reader = reader; + } + + @Override + public Object apply(long value) { + // TODO: implement the Pulsar reader logic when needed + // at the moment, the reader API support from the NB Pulsar driver is disabled + return null; + } +} diff --git a/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/ops/PulsarAdminOp.java b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/ops/PulsarAdminOp.java new file mode 100644 index 000000000..b1be645d2 --- /dev/null +++ b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/ops/PulsarAdminOp.java @@ -0,0 +1,36 @@ +/* + * 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.pulsar.ops; + +import io.nosqlbench.adapter.pulsar.util.PulsarAdapterMetrics; +import io.nosqlbench.engine.api.activityimpl.uniform.flowtypes.CycleOp; +import org.apache.pulsar.client.admin.PulsarAdmin; + +public abstract class PulsarAdminOp extends PulsarOp { + protected PulsarAdmin pulsarAdmin; + protected boolean adminDelOp; + + public PulsarAdminOp(PulsarAdapterMetrics pulsarAdapterMetrics, + PulsarAdmin pulsarAdmin, + boolean asyncApi, + boolean adminDelOp) { + super(pulsarAdapterMetrics, asyncApi); + + this.pulsarAdmin = pulsarAdmin; + this.adminDelOp = adminDelOp; + } +} diff --git a/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/ops/PulsarClientOp.java b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/ops/PulsarClientOp.java new file mode 100644 index 000000000..15ea44b15 --- /dev/null +++ b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/ops/PulsarClientOp.java @@ -0,0 +1,89 @@ +/* + * 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.pulsar.ops; + +import com.codahale.metrics.Histogram; +import com.codahale.metrics.Timer; +import io.nosqlbench.adapter.pulsar.util.PulsarAdapterMetrics; +import io.nosqlbench.adapter.pulsar.util.PulsarAvroSchemaUtil; +import org.apache.pulsar.client.api.PulsarClient; +import org.apache.pulsar.client.api.Schema; +import org.apache.pulsar.client.api.schema.KeyValueSchema; +import org.apache.pulsar.common.schema.SchemaType; + +public abstract class PulsarClientOp extends PulsarOp { + protected final PulsarClient pulsarClient; + protected final Schema pulsarSchema; + + // Pulsar KeyValue schema + private org.apache.avro.Schema avroSchema; + private org.apache.avro.Schema avroKeySchema; + + protected final Histogram messageSizeHistogram; + protected final Histogram payloadRttHistogram; + protected final Histogram e2eMsgProcLatencyHistogram; + + protected final Timer transactionCommitTimer; + + public PulsarClientOp(PulsarAdapterMetrics pulsarAdapterMetrics, + PulsarClient pulsarClient, + Schema pulsarScheam, + boolean asyncApi) { + super (pulsarAdapterMetrics, asyncApi); + + this.pulsarClient = pulsarClient; + this.pulsarSchema = pulsarScheam; + + this.messageSizeHistogram = pulsarAdapterMetrics.getMessageSizeHistogram(); + this.payloadRttHistogram = pulsarAdapterMetrics.getPayloadRttHistogram(); + this.e2eMsgProcLatencyHistogram = pulsarAdapterMetrics.getE2eMsgProcLatencyHistogram(); + this.transactionCommitTimer = pulsarAdapterMetrics.getCommitTransactionTimer(); + } + + protected org.apache.avro.Schema getAvroSchemaFromConfiguration() { + // no need for synchronization, this is only a cache + // in case of the race we will parse the string twice, not a big + if (avroSchema == null) { + if (pulsarSchema.getSchemaInfo().getType() == SchemaType.KEY_VALUE) { + KeyValueSchema kvSchema = (KeyValueSchema) pulsarSchema; + Schema valueSchema = kvSchema.getValueSchema(); + String avroDefStr = valueSchema.getSchemaInfo().getSchemaDefinition(); + avroSchema = PulsarAvroSchemaUtil.GetSchema_ApacheAvro(avroDefStr); + } else { + String avroDefStr = pulsarSchema.getSchemaInfo().getSchemaDefinition(); + avroSchema = PulsarAvroSchemaUtil.GetSchema_ApacheAvro(avroDefStr); + } + } + return avroSchema; + } + + protected org.apache.avro.Schema getKeyAvroSchemaFromConfiguration() { + // no need for synchronization, this is only a cache + // in case of the race we will parse the string twice, not a big + if (avroKeySchema == null) { + if (pulsarSchema.getSchemaInfo().getType() == SchemaType.KEY_VALUE) { + KeyValueSchema kvSchema = (KeyValueSchema) pulsarSchema; + Schema keySchema = kvSchema.getKeySchema(); + String avroDefStr = keySchema.getSchemaInfo().getSchemaDefinition(); + avroKeySchema = PulsarAvroSchemaUtil.GetSchema_ApacheAvro(avroDefStr); + } else { + throw new RuntimeException("We are not using KEY_VALUE schema, so no Schema for the Key!"); + } + } + return avroKeySchema; + } +} diff --git a/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/ops/PulsarOp.java b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/ops/PulsarOp.java new file mode 100644 index 000000000..a3e0b87b1 --- /dev/null +++ b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/ops/PulsarOp.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.pulsar.ops; + +import io.nosqlbench.adapter.pulsar.util.PulsarAdapterMetrics; +import io.nosqlbench.engine.api.activityimpl.uniform.flowtypes.CycleOp; + +public abstract class PulsarOp implements CycleOp { + protected final boolean asyncApi; + protected final PulsarAdapterMetrics pulsarAdapterMetrics; + + public PulsarOp(PulsarAdapterMetrics pulsarAdapterMetrics, boolean asyncApi) { + this.pulsarAdapterMetrics = pulsarAdapterMetrics; + this.asyncApi = asyncApi; + } +} diff --git a/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/util/EndToEndStartingTimeSource.java b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/util/EndToEndStartingTimeSource.java new file mode 100644 index 000000000..7a9f39680 --- /dev/null +++ b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/util/EndToEndStartingTimeSource.java @@ -0,0 +1,26 @@ +package io.nosqlbench.adapter.pulsar.util; + +/* + * 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. + */ + + +public enum EndToEndStartingTimeSource { + NONE, // no end-to-end latency calculation + MESSAGE_PUBLISH_TIME, // use message publish timestamp + MESSAGE_EVENT_TIME, // use message event timestamp + MESSAGE_PROPERTY_E2E_STARTING_TIME // use message property called "e2e_starting_time" as the timestamp +} diff --git a/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/util/MessageSequenceNumberSendingHandler.java b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/util/MessageSequenceNumberSendingHandler.java new file mode 100644 index 000000000..21208642a --- /dev/null +++ b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/util/MessageSequenceNumberSendingHandler.java @@ -0,0 +1,108 @@ +package io.nosqlbench.adapter.pulsar.util; + +/* + * 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. + */ + + +import org.apache.commons.lang3.RandomUtils; + +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Queue; +import java.util.Set; + +/** + * Handles adding a monotonic sequence number to message properties of sent messages + */ +public class MessageSequenceNumberSendingHandler { + static final int SIMULATED_ERROR_PROBABILITY_PERCENTAGE = 10; + long number = 1; + Queue outOfOrderNumbers; + + public long getNextSequenceNumber(Set simulatedErrorTypes) { + return getNextSequenceNumber(simulatedErrorTypes, SIMULATED_ERROR_PROBABILITY_PERCENTAGE); + } + + long getNextSequenceNumber(Set simulatedErrorTypes, int errorProbabilityPercentage) { + simulateError(simulatedErrorTypes, errorProbabilityPercentage); + return nextNumber(); + } + + private void simulateError(Set simulatedErrorTypes, int errorProbabilityPercentage) { + if (!simulatedErrorTypes.isEmpty() && shouldSimulateError(errorProbabilityPercentage)) { + int selectIndex = 0; + int numberOfErrorTypes = simulatedErrorTypes.size(); + if (numberOfErrorTypes > 1) { + // pick one of the simulated error type randomly + selectIndex = RandomUtils.nextInt(0, numberOfErrorTypes); + } + PulsarAdapterUtil.MSG_SEQ_ERROR_SIMU_TYPE errorType = simulatedErrorTypes.stream() + .skip(selectIndex) + .findFirst() + .get(); + switch (errorType) { + case OutOfOrder: + // simulate message out of order + injectMessagesOutOfOrder(); + break; + case MsgDup: + // simulate message duplication + injectMessageDuplication(); + break; + case MsgLoss: + // simulate message loss + injectMessageLoss(); + break; + } + } + } + + private boolean shouldSimulateError(int errorProbabilityPercentage) { + // Simulate error with the specified probability + return RandomUtils.nextInt(0, 100) < errorProbabilityPercentage; + } + + long nextNumber() { + if (outOfOrderNumbers != null) { + long nextNumber = outOfOrderNumbers.poll(); + if (outOfOrderNumbers.isEmpty()) { + outOfOrderNumbers = null; + } + return nextNumber; + } + return number++; + } + + void injectMessagesOutOfOrder() { + if (outOfOrderNumbers == null) { + outOfOrderNumbers = new ArrayDeque<>(Arrays.asList(number + 2, number, number + 1)); + number += 3; + } + } + + void injectMessageDuplication() { + if (outOfOrderNumbers == null) { + number--; + } + } + + void injectMessageLoss() { + if (outOfOrderNumbers == null) { + number++; + } + } +} diff --git a/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/util/PulsarAdapterMetrics.java b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/util/PulsarAdapterMetrics.java new file mode 100644 index 000000000..ae48803f4 --- /dev/null +++ b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/util/PulsarAdapterMetrics.java @@ -0,0 +1,231 @@ +/* + * 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.pulsar.util; + +import com.codahale.metrics.Counter; +import com.codahale.metrics.Gauge; +import com.codahale.metrics.Histogram; +import com.codahale.metrics.Timer; +import io.nosqlbench.adapter.pulsar.dispensers.PulsarBaseOpDispenser; +import io.nosqlbench.api.engine.metrics.ActivityMetrics; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.pulsar.client.api.Consumer; +import org.apache.pulsar.client.api.ConsumerStats; +import org.apache.pulsar.client.api.Producer; +import org.apache.pulsar.client.api.ProducerStats; + +import java.util.function.Function; + +public class PulsarAdapterMetrics { + + private final static Logger logger = LogManager.getLogger("PulsarAdapterMetrics"); + + private final PulsarBaseOpDispenser pulsarBaseOpDispenser; + private final String defaultAdapterMetricsPrefix; + + /** + * Pulsar adapter specific metrics + */ + // - message out of sequence error counter + private Counter msgErrOutOfSeqCounter; + // - message loss counter + private Counter msgErrLossCounter; + // - message duplicate (when dedup is enabled) error counter + private Counter msgErrDuplicateCounter; + + private Histogram messageSizeHistogram; + // end-to-end latency + private Histogram e2eMsgProcLatencyHistogram; + // A histogram that tracks payload round-trip-time, based on a user-defined field in some sender + // system which can be interpreted as millisecond epoch time in the system's local time zone. + // This is paired with a field name of the same type to be extracted and reported in a metric + // named 'payload-rtt'. + private Histogram payloadRttHistogram; + + private Timer bindTimer; + private Timer executeTimer; + private Timer createTransactionTimer; + private Timer commitTransactionTimer; + + public PulsarAdapterMetrics(PulsarBaseOpDispenser pulsarBaseOpDispenser, String defaultMetricsPrefix) { + this.pulsarBaseOpDispenser = pulsarBaseOpDispenser; + this.defaultAdapterMetricsPrefix = defaultMetricsPrefix; + } + + public void initPulsarAdapterInstrumentation() { + // Counter metrics + this.msgErrOutOfSeqCounter = + ActivityMetrics.counter( + pulsarBaseOpDispenser, + defaultAdapterMetricsPrefix + "err_msg_oos"); + this.msgErrLossCounter = + ActivityMetrics.counter( + pulsarBaseOpDispenser, + defaultAdapterMetricsPrefix + "err_msg_loss"); + this.msgErrDuplicateCounter = + ActivityMetrics.counter( + pulsarBaseOpDispenser, + defaultAdapterMetricsPrefix + "err_msg_dup"); + + // Histogram metrics + this.messageSizeHistogram = + ActivityMetrics.histogram( + pulsarBaseOpDispenser, + defaultAdapterMetricsPrefix + "message_size", + ActivityMetrics.DEFAULT_HDRDIGITS); + this.e2eMsgProcLatencyHistogram = + ActivityMetrics.histogram( + pulsarBaseOpDispenser, + defaultAdapterMetricsPrefix + "e2e_msg_latency", + ActivityMetrics.DEFAULT_HDRDIGITS); + this.payloadRttHistogram = + ActivityMetrics.histogram( + pulsarBaseOpDispenser, + defaultAdapterMetricsPrefix + "payload_rtt", + ActivityMetrics.DEFAULT_HDRDIGITS); + + // Timer metrics + this.bindTimer = + ActivityMetrics.timer( + pulsarBaseOpDispenser, + defaultAdapterMetricsPrefix + "bind", + ActivityMetrics.DEFAULT_HDRDIGITS); + this.executeTimer = + ActivityMetrics.timer( + pulsarBaseOpDispenser, + defaultAdapterMetricsPrefix + "execute", + ActivityMetrics.DEFAULT_HDRDIGITS); + this.createTransactionTimer = + ActivityMetrics.timer( + pulsarBaseOpDispenser, + defaultAdapterMetricsPrefix + "create_transaction", + ActivityMetrics.DEFAULT_HDRDIGITS); + this.commitTransactionTimer = + ActivityMetrics.timer( + pulsarBaseOpDispenser, + defaultAdapterMetricsPrefix + "commit_transaction", + ActivityMetrics.DEFAULT_HDRDIGITS); + } + + public Counter getMsgErrOutOfSeqCounter() { return this.msgErrOutOfSeqCounter; } + public Counter getMsgErrLossCounter() { return this.msgErrLossCounter; } + public Counter getMsgErrDuplicateCounter() { return this.msgErrDuplicateCounter; } + public Histogram getMessageSizeHistogram() { return this.messageSizeHistogram; } + public Histogram getE2eMsgProcLatencyHistogram() { return this.e2eMsgProcLatencyHistogram; } + public Histogram getPayloadRttHistogram() { return payloadRttHistogram; } + public Timer getBindTimer() { return bindTimer; } + public Timer getExecuteTimer() { return executeTimer; } + public Timer getCreateTransactionTimer() { return createTransactionTimer; } + public Timer getCommitTransactionTimer() { return commitTransactionTimer; } + + + ////////////////////////////////////// + // Pulsar client producer API metrics + ////////////////////////////////////// + // + private static class ProducerGaugeImpl implements Gauge { + private final Producer producer; + private final Function valueExtractor; + + ProducerGaugeImpl(Producer producer, Function valueExtractor) { + this.producer = producer; + this.valueExtractor = valueExtractor; + } + + @Override + public Object getValue() { + // see Pulsar bug https://github.com/apache/pulsar/issues/10100 + // we need to synchronize on producer otherwise we could receive corrupted data + synchronized(producer) { + return valueExtractor.apply(producer.getStats()); + } + } + } + private static Gauge producerSafeExtractMetric(Producer producer, Function valueExtractor) { + return new ProducerGaugeImpl(producer, valueExtractor); + } + + public void registerProducerApiMetrics(Producer producer, String pulsarApiMetricsPrefix) { + String metricsPrefix = defaultAdapterMetricsPrefix; + if (!StringUtils.isBlank(pulsarApiMetricsPrefix)) { + metricsPrefix = pulsarApiMetricsPrefix; + } + + ActivityMetrics.gauge(pulsarBaseOpDispenser, metricsPrefix + "total_bytes_sent", + producerSafeExtractMetric(producer, (s -> s.getTotalBytesSent() + s.getNumBytesSent()))); + ActivityMetrics.gauge(pulsarBaseOpDispenser, metricsPrefix + "total_msg_sent", + producerSafeExtractMetric(producer, (s -> s.getTotalMsgsSent() + s.getNumMsgsSent()))); + ActivityMetrics.gauge(pulsarBaseOpDispenser, metricsPrefix + "total_send_failed", + producerSafeExtractMetric(producer, (s -> s.getTotalSendFailed() + s.getNumSendFailed()))); + ActivityMetrics.gauge(pulsarBaseOpDispenser, metricsPrefix + "total_ack_received", + producerSafeExtractMetric(producer,(s -> s.getTotalAcksReceived() + s.getNumAcksReceived()))); + ActivityMetrics.gauge(pulsarBaseOpDispenser, metricsPrefix + "send_bytes_rate", + producerSafeExtractMetric(producer, ProducerStats::getSendBytesRate)); + ActivityMetrics.gauge(pulsarBaseOpDispenser, metricsPrefix + "send_msg_rate", + producerSafeExtractMetric(producer, ProducerStats::getSendMsgsRate)); + } + + + ////////////////////////////////////// + // Pulsar client consumer API metrics + ////////////////////////////////////// + // + private static class ConsumerGaugeImpl implements Gauge { + private final Consumer consumer; + private final Function valueExtractor; + + ConsumerGaugeImpl(Consumer consumer, Function valueExtractor) { + this.consumer = consumer; + this.valueExtractor = valueExtractor; + } + + @Override + public Object getValue() { + // see Pulsar bug https://github.com/apache/pulsar/issues/10100 + // - this is a bug report for producer stats. + // - assume this also applies to consumer stats. + synchronized(consumer) { + return valueExtractor.apply(consumer.getStats()); + } + } + } + static Gauge consumerSafeExtractMetric(Consumer consumer, Function valueExtractor) { + return new ConsumerGaugeImpl(consumer, valueExtractor); + } + + public void registerConsumerApiMetrics(Consumer consumer, String pulsarApiMetricsPrefix) { + String metricsPrefix = defaultAdapterMetricsPrefix; + if (!StringUtils.isBlank(pulsarApiMetricsPrefix)) { + metricsPrefix = pulsarApiMetricsPrefix; + } + + ActivityMetrics.gauge(pulsarBaseOpDispenser, metricsPrefix + "total_bytes_recv", + consumerSafeExtractMetric(consumer, (s -> s.getTotalBytesReceived() + s.getNumBytesReceived()))); + ActivityMetrics.gauge(pulsarBaseOpDispenser, metricsPrefix + "total_msg_recv", + consumerSafeExtractMetric(consumer, (s -> s.getTotalMsgsReceived() + s.getNumMsgsReceived()))); + ActivityMetrics.gauge(pulsarBaseOpDispenser, metricsPrefix + "total_recv_failed", + consumerSafeExtractMetric(consumer, (s -> s.getTotalReceivedFailed() + s.getNumReceiveFailed()))); + ActivityMetrics.gauge(pulsarBaseOpDispenser, metricsPrefix + "total_acks_sent", + consumerSafeExtractMetric(consumer,(s -> s.getTotalAcksSent() + s.getNumAcksSent()))); + ActivityMetrics.gauge(pulsarBaseOpDispenser, metricsPrefix + "recv_bytes_rate", + consumerSafeExtractMetric(consumer, ConsumerStats::getRateBytesReceived)); + ActivityMetrics.gauge(pulsarBaseOpDispenser, metricsPrefix + "recv_msg_rate", + consumerSafeExtractMetric(consumer, ConsumerStats::getRateMsgsReceived)); + } +} diff --git a/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/util/PulsarAdapterUtil.java b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/util/PulsarAdapterUtil.java new file mode 100644 index 000000000..e61f76eea --- /dev/null +++ b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/util/PulsarAdapterUtil.java @@ -0,0 +1,580 @@ +/* + * 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.pulsar.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.nosqlbench.adapter.pulsar.exception.PulsarAdapterInvalidParamException; +import io.nosqlbench.adapter.pulsar.exception.PulsarAdapterUnexpectedException; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.pulsar.client.api.Schema; + +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class PulsarAdapterUtil { + + private final static Logger logger = LogManager.getLogger(PulsarAdapterUtil.class); + + public static final String MSG_SEQUENCE_NUMBER = "sequence_number"; + + /////// + // Valid document level parameters for Pulsar NB yaml file + public enum DOC_LEVEL_PARAMS { + TOPIC_URI("topic_uri"), + ASYNC_API("async_api"), + USE_TRANSACTION("use_transaction"), + TRANSACT_BATCH_NUM("transact_batch_num"), + ADMIN_DELOP("admin_delop"), + SEQ_TRACKING("seq_tracking"), + SEQERR_SIMU("seqerr_simu"), + RTT_TRACKING_FIELD("payload_traking_field"), + MSG_DEDUP_BROKER("msg_dedup_broker"), + E2E_STARTING_TIME_SOURCE("e2e_starting_time_source"); + + public final String label; + + DOC_LEVEL_PARAMS(String label) { + this.label = label; + } + } + public static boolean isValidDocLevelParam(String param) { + return Arrays.stream(DOC_LEVEL_PARAMS.values()).anyMatch(t -> t.label.equals(param)); + } + + /////// + // Message processing sequence error simulation types + public enum MSG_SEQ_ERROR_SIMU_TYPE { + OutOfOrder("out_of_order"), + MsgLoss("msg_loss"), + MsgDup("msg_dup"); + + public final String label; + + MSG_SEQ_ERROR_SIMU_TYPE(String label) { + this.label = label; + } + + private static final Map MAPPING = new HashMap<>(); + + static { + for (MSG_SEQ_ERROR_SIMU_TYPE simuType : values()) { + MAPPING.put(simuType.label, simuType); + MAPPING.put(simuType.label.toLowerCase(), simuType); + MAPPING.put(simuType.label.toUpperCase(), simuType); + MAPPING.put(simuType.name(), simuType); + MAPPING.put(simuType.name().toLowerCase(), simuType); + MAPPING.put(simuType.name().toUpperCase(), simuType); + } + } + + public static Optional parseSimuType(String simuTypeString) { + return Optional.ofNullable(MAPPING.get(simuTypeString.trim())); + } + } + public static boolean isValidSeqErrSimuType(String item) { + return Arrays.stream(MSG_SEQ_ERROR_SIMU_TYPE.values()).anyMatch(t -> t.label.equals(item)); + } + public static String getValidSeqErrSimuTypeList() { + return Arrays.stream(MSG_SEQ_ERROR_SIMU_TYPE.values()).map(t -> t.label).collect(Collectors.joining(", ")); + } + + /////// + // Valid Pulsar API type + public enum PULSAR_API_TYPE { + PRODUCER("producer"), + CONSUMER("consumer"), + READER("reader"); + + public final String label; + + PULSAR_API_TYPE(String label) { + this.label = label; + } + } + public static boolean isValidPulsarApiType(String param) { + return Arrays.stream(PULSAR_API_TYPE.values()).anyMatch(t -> t.label.equals(param)); + } + public static String getValidPulsarApiTypeList() { + return Arrays.stream(PULSAR_API_TYPE.values()).map(t -> t.label).collect(Collectors.joining(", ")); + } + + + /////// + // Valid configuration categories + public enum CONF_GATEGORY { + Schema("schema"), + Client("client"), + Producer("producer"), + Consumer("consumer"), + Reader("reader"); + + public final String label; + + CONF_GATEGORY(String label) { + this.label = label; + } + } + public static boolean isValidConfCategory(String item) { + return Arrays.stream(CONF_GATEGORY.values()).anyMatch(t -> t.label.equals(item)); + } + public static String getValidConfCategoryList() { + return Arrays.stream(CONF_GATEGORY.values()).map(t -> t.label).collect(Collectors.joining(", ")); + } + + /////// + // Valid persistence type + public enum PERSISTENT_TYPES { + PERSISTENT("persistent"), + NON_PERSISTENT("non-persistent") + ; + + public final String label; + PERSISTENT_TYPES(String label) { + this.label = label; + } + } + public static boolean isValidPersistenceType(String type) { + return Arrays.stream(PERSISTENT_TYPES.values()).anyMatch(t -> t.label.equals(type)); + } + + /////// + // Valid Pulsar client configuration (activity-level settings) + // - https://pulsar.apache.org/docs/en/client-libraries-java/#client + public enum CLNT_CONF_KEY { + serviceUrl("serviceUrl"), + authPulginClassName("authPluginClassName"), + authParams("authParams"), + pperationTimeoutMs("operationTimeoutMs"), + statsIntervalSeconds("statsIntervalSeconds"), + numIoThreads("numIoThreads"), + numListenerThreads("numListenerThreads"), + useTcpNoDelay("useTcpNoDelay"), + enableTls("enableTls"), + tlsTrustCertsFilePath("tlsTrustCertsFilePath"), + tlsAllowInsecureConnection("tlsAllowInsecureConnection"), + tlsHostnameVerificationEnable("tlsHostnameVerificationEnable"), + concurrentLookupRequest("concurrentLookupRequest"), + maxLookupRequest("maxLookupRequest"), + maxNumberOfRejectedRequestPerConnection("maxNumberOfRejectedRequestPerConnection"), + keepAliveIntervalSeconds("keepAliveIntervalSeconds"), + connectionTimeoutMs("connectionTimeoutMs"), + requestTimeoutMs("requestTimeoutMs"), + defaultBackoffIntervalNanos("defaultBackoffIntervalNanos"), + maxBackoffIntervalNanos("maxBackoffIntervalNanos"), + socks5ProxyAddress("socks5ProxyAddress"), + socks5ProxyUsername("socks5ProxyUsername"), + socks5ProxyPassword("socks5ProxyPassword") + ; + + public final String label; + CLNT_CONF_KEY(String label) { + this.label = label; + } + } + public static boolean isValidClientConfItem(String item) { + return Arrays.stream(CLNT_CONF_KEY.values()).anyMatch(t -> t.label.equals(item)); + } + + /////// + // Standard producer configuration (activity-level settings) + // - https://pulsar.apache.org/docs/en/client-libraries-java/#configure-producer + public enum PRODUCER_CONF_STD_KEY { + topicName("topicName"), + producerName("producerName"), + sendTimeoutMs("sendTimeoutMs"), + blockIfQueueFull("blockIfQueueFull"), + maxPendingMessages("maxPendingMessages"), + maxPendingMessagesAcrossPartitions("maxPendingMessagesAcrossPartitions"), + messageRoutingMode("messageRoutingMode"), + hashingScheme("hashingScheme"), + cryptoFailureAction("cryptoFailureAction"), + batchingMaxPublishDelayMicros("batchingMaxPublishDelayMicros"), + batchingMaxMessages("batchingMaxMessages"), + batchingEnabled("batchingEnabled"), + compressionType("compressionType"); + + public final String label; + + PRODUCER_CONF_STD_KEY(String label) { + this.label = label; + } + } + public static boolean isStandardProducerConfItem(String item) { + return Arrays.stream(PRODUCER_CONF_STD_KEY.values()).anyMatch(t -> t.label.equals(item)); + } + + // compressionType + public enum COMPRESSION_TYPE { + NONE("NONE"), + LZ4("LZ4"), + ZLIB("ZLIB"), + ZSTD("ZSTD"), + SNAPPY("SNAPPY"); + + public final String label; + + COMPRESSION_TYPE(String label) { + this.label = label; + } + } + public static boolean isValidCompressionType(String item) { + return Arrays.stream(COMPRESSION_TYPE.values()).anyMatch(t -> t.label.equals(item)); + } + public static String getValidCompressionTypeList() { + return Arrays.stream(COMPRESSION_TYPE.values()).map(t -> t.label).collect(Collectors.joining(", ")); + } + + /////// + // Standard consumer configuration (activity-level settings) + // - https://pulsar.apache.org/docs/en/client-libraries-java/#consumer + public enum CONSUMER_CONF_STD_KEY { + topicNames("topicNames"), + topicsPattern("topicsPattern"), + subscriptionName("subscriptionName"), + subscriptionType("subscriptionType"), + receiverQueueSize("receiverQueueSize"), + acknowledgementsGroupTimeMicros("acknowledgementsGroupTimeMicros"), + negativeAckRedeliveryDelayMicros("negativeAckRedeliveryDelayMicros"), + maxTotalReceiverQueueSizeAcrossPartitions("maxTotalReceiverQueueSizeAcrossPartitions"), + consumerName("consumerName"), + ackTimeoutMillis("ackTimeoutMillis"), + tickDurationMillis("tickDurationMillis"), + priorityLevel("priorityLevel"), + cryptoFailureAction("cryptoFailureAction"), + properties("properties"), + readCompacted("readCompacted"), + subscriptionInitialPosition("subscriptionInitialPosition"), + patternAutoDiscoveryPeriod("patternAutoDiscoveryPeriod"), + regexSubscriptionMode("regexSubscriptionMode"), + deadLetterPolicy("deadLetterPolicy"), + autoUpdatePartitions("autoUpdatePartitions"), + replicateSubscriptionState("replicateSubscriptionState"), + negativeAckRedeliveryBackoff("negativeAckRedeliveryBackoff"), + ackTimeoutRedeliveryBackoff("ackTimeoutRedeliveryBackoff"), + autoAckOldestChunkedMessageOnQueueFull("autoAckOldestChunkedMessageOnQueueFull"), + maxPendingChunkedMessage("maxPendingChunkedMessage"), + expireTimeOfIncompleteChunkedMessageMillis("expireTimeOfIncompleteChunkedMessageMillis"); + + public final String label; + + CONSUMER_CONF_STD_KEY(String label) { + this.label = label; + } + } + public static boolean isStandardConsumerConfItem(String item) { + return Arrays.stream(CONSUMER_CONF_STD_KEY.values()).anyMatch(t -> t.label.equals(item)); + } + + /////// + // Custom consumer configuration (activity-level settings) + // - NOT part of https://pulsar.apache.org/docs/en/client-libraries-java/#consumer + // - NB Pulsar driver consumer operation specific + public enum CONSUMER_CONF_CUSTOM_KEY { + timeout("timeout"), + ranges("ranges"); + + public final String label; + + CONSUMER_CONF_CUSTOM_KEY(String label) { + this.label = label; + } + } + public static boolean isCustomConsumerConfItem(String item) { + return Arrays.stream(CONSUMER_CONF_CUSTOM_KEY.values()).anyMatch(t -> t.label.equals(item)); + } + + // subscriptionTyp + public enum SUBSCRIPTION_TYPE { + Exclusive("Exclusive"), + Failover("Failover"), + Shared("Shared"), + Key_Shared("Key_Shared"); + + public final String label; + + SUBSCRIPTION_TYPE(String label) { + this.label = label; + } + } + public static boolean isValidSubscriptionType(String item) { + return Arrays.stream(SUBSCRIPTION_TYPE.values()).anyMatch(t -> t.label.equals(item)); + } + public static String getValidSubscriptionTypeList() { + return Arrays.stream(SUBSCRIPTION_TYPE.values()).map(t -> t.label).collect(Collectors.joining(", ")); + } + + // subscriptionInitialPosition + public enum SUBSCRIPTION_INITIAL_POSITION { + Earliest("Earliest"), + Latest("Latest"); + + public final String label; + + SUBSCRIPTION_INITIAL_POSITION(String label) { + this.label = label; + } + } + public static boolean isValidSubscriptionInitialPosition(String item) { + return Arrays.stream(SUBSCRIPTION_INITIAL_POSITION.values()).anyMatch(t -> t.label.equals(item)); + } + public static String getValidSubscriptionInitialPositionList() { + return Arrays.stream(SUBSCRIPTION_INITIAL_POSITION.values()).map(t -> t.label).collect(Collectors.joining(", ")); + } + + // regexSubscriptionMode + public enum REGEX_SUBSCRIPTION_MODE { + Persistent("PersistentOnly"), + NonPersistent("NonPersistentOnly"), + All("AllTopics"); + + public final String label; + + REGEX_SUBSCRIPTION_MODE(String label) { + this.label = label; + } + } + public static boolean isValidRegexSubscriptionMode(String item) { + return Arrays.stream(REGEX_SUBSCRIPTION_MODE.values()).anyMatch(t -> t.label.equals(item)); + } + public static String getValidRegexSubscriptionModeList() { + return Arrays.stream(REGEX_SUBSCRIPTION_MODE.values()).map(t -> t.label).collect(Collectors.joining(", ")); + } + + /////// + // Standard reader configuration (activity-level settings) + // - https://pulsar.apache.org/docs/en/client-libraries-java/#reader + public enum READER_CONF_STD_KEY { + topicName("topicName"), + receiverQueueSize("receiverQueueSize"), + readerListener("readerListener"), + readerName("readerName"), + subscriptionRolePrefix("subscriptionRolePrefix"), + cryptoKeyReader("cryptoKeyReader"), + cryptoFailureAction("cryptoFailureAction"), + readCompacted("readCompacted"), + resetIncludeHead("resetIncludeHead"); + + public final String label; + + READER_CONF_STD_KEY(String label) { + this.label = label; + } + } + public static boolean isStandardReaderConfItem(String item) { + return Arrays.stream(READER_CONF_STD_KEY.values()).anyMatch(t -> t.label.equals(item)); + } + + /////// + // Custom reader configuration (activity-level settings) + // - NOT part of https://pulsar.apache.org/docs/en/client-libraries-java/#reader + // - NB Pulsar driver reader operation specific + public enum READER_CONF_CUSTOM_KEY { + startMessagePos("startMessagePos"); + + public final String label; + + READER_CONF_CUSTOM_KEY(String label) { + this.label = label; + } + } + public static boolean isCustomReaderConfItem(String item) { + return Arrays.stream(READER_CONF_CUSTOM_KEY.values()).anyMatch(t -> t.label.equals(item)); + } + + /////// + // Valid read positions for a Pulsar reader + public enum READER_MSG_POSITION_TYPE { + earliest("earliest"), + latest("latest"); + + public final String label; + + READER_MSG_POSITION_TYPE(String label) { + this.label = label; + } + } + public static boolean isValideReaderStartPosition(String item) { + return Arrays.stream(READER_MSG_POSITION_TYPE.values()).anyMatch(t -> t.label.equals(item)); + } + + /////// + // Primitive Schema type + public static boolean isPrimitiveSchemaTypeStr(String typeStr) { + boolean isPrimitive = false; + + // Use "BYTES" as the default type if the type string is not explicitly specified + if (StringUtils.isBlank(typeStr)) { + typeStr = "BYTES"; + } + + if (typeStr.equalsIgnoreCase("BOOLEAN") || typeStr.equalsIgnoreCase("INT8") || + typeStr.equalsIgnoreCase("INT16") || typeStr.equalsIgnoreCase("INT32") || + typeStr.equalsIgnoreCase("INT64") || typeStr.equalsIgnoreCase("FLOAT") || + typeStr.equalsIgnoreCase("DOUBLE") || typeStr.equalsIgnoreCase("BYTES") || + typeStr.equalsIgnoreCase("DATE") || typeStr.equalsIgnoreCase("TIME") || + typeStr.equalsIgnoreCase("TIMESTAMP") || typeStr.equalsIgnoreCase("INSTANT") || + typeStr.equalsIgnoreCase("LOCAL_DATE") || typeStr.equalsIgnoreCase("LOCAL_TIME") || + typeStr.equalsIgnoreCase("LOCAL_DATE_TIME")) { + isPrimitive = true; + } + + return isPrimitive; + } + public static Schema getPrimitiveTypeSchema(String typeStr) { + Schema schema; + + if (StringUtils.isBlank(typeStr)) { + typeStr = "BYTES"; + } + + switch (typeStr.toUpperCase()) { + case "BOOLEAN": + schema = Schema.BOOL; + break; + case "INT8": + schema = Schema.INT8; + break; + case "INT16": + schema = Schema.INT16; + break; + case "INT32": + schema = Schema.INT32; + break; + case "INT64": + schema = Schema.INT64; + break; + case "FLOAT": + schema = Schema.FLOAT; + break; + case "DOUBLE": + schema = Schema.DOUBLE; + break; + case "DATE": + schema = Schema.DATE; + break; + case "TIME": + schema = Schema.TIME; + break; + case "TIMESTAMP": + schema = Schema.TIMESTAMP; + break; + case "INSTANT": + schema = Schema.INSTANT; + break; + case "LOCAL_DATE": + schema = Schema.LOCAL_DATE; + break; + case "LOCAL_TIME": + schema = Schema.LOCAL_TIME; + break; + case "LOCAL_DATE_TIME": + schema = Schema.LOCAL_DATE_TIME; + break; + case "BYTES": + schema = Schema.BYTES; + break; + // Report an error if non-valid, non-empty schema type string is provided + default: + throw new PulsarAdapterInvalidParamException("Invalid Pulsar primitive schema type string : " + typeStr); + } + + return schema; + } + + /////// + // Complex strut type: Avro or Json + public static boolean isAvroSchemaTypeStr(String typeStr) { + return (StringUtils.isNotBlank(typeStr) && typeStr.equalsIgnoreCase("AVRO")); + } + + // automatic decode the type from the Registry + public static boolean isAutoConsumeSchemaTypeStr(String typeStr) { + return (StringUtils.isNotBlank(typeStr) && typeStr.equalsIgnoreCase("AUTO_CONSUME")); + } + public static Schema getAvroSchema(String typeStr, String definitionStr) { + String schemaDefinitionStr = definitionStr; + String filePrefix = "file://"; + Schema schema; + + // Check if payloadStr points to a file (e.g. "file:///path/to/a/file") + if (isAvroSchemaTypeStr(typeStr)) { + if (StringUtils.isBlank(schemaDefinitionStr)) { + throw new PulsarAdapterInvalidParamException( + "Schema definition must be provided for \"Avro\" schema type!"); + } + else if (schemaDefinitionStr.startsWith(filePrefix)) { + try { + Path filePath = Paths.get(URI.create(schemaDefinitionStr)); + schemaDefinitionStr = Files.readString(filePath, StandardCharsets.US_ASCII); + } + catch (IOException ioe) { + throw new PulsarAdapterUnexpectedException( + "Error reading the specified \"Avro\" schema definition file: " + definitionStr + ": " + ioe.getMessage()); + } + } + + schema = PulsarAvroSchemaUtil.GetSchema_PulsarAvro("NBAvro", schemaDefinitionStr); + } + else { + throw new PulsarAdapterInvalidParamException( + "Trying to create a \"Avro\" schema for a non-Avro schema type string: " + typeStr); + } + + return schema; + } + + /////// + // Generate effective key string + public static String buildCacheKey(String... keyParts) { + // Ignore blank keyPart + String joinedKeyStr = + Stream.of(keyParts) + .filter(s -> !StringUtils.isBlank(s)) + .collect(Collectors.joining(",")); + + return Base64.getEncoder().encodeToString(joinedKeyStr.getBytes()); + } + + /////// + // Convert JSON string to a key/value map + public static Map convertJsonToMap(String jsonStr) throws Exception { + ObjectMapper mapper = new ObjectMapper(); + return mapper.readValue(jsonStr, Map.class); + } + + /////// + // Get full namespace name (/) from a Pulsar topic URI + public static String getFullNamespaceName(String topicUri) { + // Get tenant/namespace string + // - topicUri : persistent://// + // - tmpStr : // + // - fullNsName : / + + String tmpStr = StringUtils.substringAfter(topicUri,"://"); + return StringUtils.substringBeforeLast(tmpStr, "/"); + } +} + diff --git a/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/util/PulsarAvroSchemaUtil.java b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/util/PulsarAvroSchemaUtil.java new file mode 100644 index 000000000..a60b1f0ff --- /dev/null +++ b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/util/PulsarAvroSchemaUtil.java @@ -0,0 +1,123 @@ +/* + * 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.pulsar.util; + +import org.apache.avro.io.DecoderFactory; +import org.apache.avro.io.JsonDecoder; +import org.apache.avro.io.BinaryDecoder; +import org.apache.pulsar.client.api.schema.Field; +import org.apache.pulsar.client.api.schema.GenericRecord; +import org.apache.pulsar.client.api.schema.GenericRecordBuilder; +import org.apache.pulsar.client.impl.schema.SchemaInfoImpl; +import org.apache.pulsar.client.impl.schema.generic.GenericAvroSchema; +import org.apache.pulsar.common.schema.SchemaInfo; +import org.apache.pulsar.common.schema.SchemaType; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; + +public class PulsarAvroSchemaUtil { + //////////////////////// + // Get an OSS Apache Avro schema from a string definition + public static org.apache.avro.Schema GetSchema_ApacheAvro(String avroSchemDef) { + return new org.apache.avro.Schema.Parser().parse(avroSchemDef); + } + + // Get an OSS Apache Avro schema record from a JSON string that matches a specific OSS Apache Avro schema + public static org.apache.avro.generic.GenericRecord GetGenericRecord_ApacheAvro(org.apache.avro.Schema schema, String jsonData) { + org.apache.avro.generic.GenericRecord record = null; + + try { + org.apache.avro.generic.GenericDatumReader reader; + reader = new org.apache.avro.generic.GenericDatumReader<>(schema); + + JsonDecoder decoder = DecoderFactory.get().jsonDecoder(schema, jsonData); + + record = reader.read(null, decoder); + } catch (IOException ioe) { + ioe.printStackTrace(); + } + + return record; + } + + // Get an OSS Apache Avro schema record from a byte array that matches a specific OSS Apache Avro schema + public static org.apache.avro.generic.GenericRecord GetGenericRecord_ApacheAvro(org.apache.avro.Schema schema, byte[] bytesData) { + org.apache.avro.generic.GenericRecord record = null; + + try { + org.apache.avro.generic.GenericDatumReader reader; + reader = new org.apache.avro.generic.GenericDatumReader<>(schema); + + BinaryDecoder decoder = DecoderFactory.get().binaryDecoder(bytesData, null); + + record = reader.read(null, decoder); + } catch (IOException ioe) { + ioe.printStackTrace(); + } + + return record; + } + + + //////////////////////// + // Get a Pulsar Avro schema from a string definition + public static GenericAvroSchema GetSchema_PulsarAvro(String schemaName, String avroSchemDef) { + SchemaInfo schemaInfo = SchemaInfoImpl.builder() + .schema(avroSchemDef.getBytes(StandardCharsets.UTF_8)) + .type(SchemaType.AVRO) + .properties(new HashMap<>()) + .name(schemaName) + .build(); + return new GenericAvroSchema(schemaInfo); + } + + // Get a Pulsar Avro record from an OSS Avro schema record, matching a specific Pulsar Avro schema + public static GenericRecord GetGenericRecord_PulsarAvro( + GenericAvroSchema pulsarGenericAvroSchema, + org.apache.avro.generic.GenericRecord apacheAvroGenericRecord) + { + GenericRecordBuilder recordBuilder = pulsarGenericAvroSchema.newRecordBuilder(); + + List fieldList = pulsarGenericAvroSchema.getFields(); + for (Field field : fieldList) { + String fieldName = field.getName(); + recordBuilder.set(fieldName, apacheAvroGenericRecord.get(fieldName)); + } + + return recordBuilder.build(); + } + + // Get a Pulsar Avro record (GenericRecord) from a JSON string that matches a specific Pulsar Avro schema + public static GenericRecord GetGenericRecord_PulsarAvro(GenericAvroSchema genericAvroSchema, String avroSchemDefStr, String jsonData) { + org.apache.avro.Schema avroSchema = GetSchema_ApacheAvro(avroSchemDefStr); + return GetGenericRecord_PulsarAvro(genericAvroSchema, avroSchema, jsonData); + } + + public static GenericRecord GetGenericRecord_PulsarAvro(GenericAvroSchema genericAvroSchema, org.apache.avro.Schema avroSchema, String jsonData) { + org.apache.avro.generic.GenericRecord apacheAvroRecord = GetGenericRecord_ApacheAvro(avroSchema, jsonData); + return GetGenericRecord_PulsarAvro(genericAvroSchema, apacheAvroRecord); + } + public static GenericRecord GetGenericRecord_PulsarAvro(String schemaName, String avroSchemDefStr, String jsonData) { + GenericAvroSchema genericAvroSchema = GetSchema_PulsarAvro(schemaName, avroSchemDefStr); + org.apache.avro.Schema avroSchema = GetSchema_ApacheAvro(avroSchemDefStr); + org.apache.avro.generic.GenericRecord apacheAvroRecord = GetGenericRecord_ApacheAvro(avroSchema, jsonData); + + return GetGenericRecord_PulsarAvro(genericAvroSchema, apacheAvroRecord); + } +} 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 new file mode 100644 index 000000000..4af681509 --- /dev/null +++ b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/util/PulsarClientConf.java @@ -0,0 +1,267 @@ +/* + * 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.pulsar.util; + +import org.apache.commons.configuration2.Configuration; +import org.apache.commons.configuration2.FileBasedConfiguration; +import org.apache.commons.configuration2.PropertiesConfiguration; +import org.apache.commons.configuration2.builder.FileBasedConfigurationBuilder; +import org.apache.commons.configuration2.builder.fluent.Parameters; +import org.apache.commons.configuration2.ex.ConfigurationException; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.math.NumberUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +public class PulsarClientConf { + + private final static Logger logger = LogManager.getLogger(PulsarClientConf.class); + + private String canonicalFilePath = ""; + + private final Map schemaConfMapRaw = new HashMap<>(); + private final Map clientConfMapRaw = new HashMap<>(); + + // "Raw" map is what is read from the config properties file + // "Tgt" map is what is really needed in the Pulsar producer/consumer/reader API + private final Map producerConfMapRaw = new HashMap<>(); + private final Map producerConfMapTgt = new HashMap<>(); + + private final Map consumerConfMapRaw = new HashMap<>(); + private final Map consumerConfMapTgt = new HashMap<>(); + + private final Map readerConfMapRaw = new HashMap<>(); + private final Map readerConfMapTgt = new HashMap<>(); + + public PulsarClientConf(String fileName) { + + ////////////////// + // Read related Pulsar client configuration settings from a file + readRawConfFromFile(fileName); + + + ////////////////// + // Ignores the following Pulsar client/producer/consumer configurations since + // they need to be specified either as the NB CLI parameters or as the NB yaml + // OpTemplate parameters. + clientConfMapRaw.remove("brokerServiceUrl"); + clientConfMapRaw.remove("webServiceUrl"); + + + ////////////////// + // 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 + } + + + public void readRawConfFromFile(String fileName) { + File file = new File(fileName); + + try { + canonicalFilePath = file.getCanonicalPath(); + + Parameters params = new Parameters(); + + FileBasedConfigurationBuilder builder = + new FileBasedConfigurationBuilder(PropertiesConfiguration.class) + .configure(params.properties() + .setFileName(fileName)); + + Configuration config = builder.getConfiguration(); + + for (Iterator it = config.getKeys(); it.hasNext(); ) { + String confKey = it.next(); + String confVal = config.getProperty(confKey).toString(); + + if (!StringUtils.isBlank(confVal)) { + + // Get schema specific configuration settings, removing "schema." prefix + if (StringUtils.startsWith(confKey, PulsarAdapterUtil.CONF_GATEGORY.Schema.label)) { + schemaConfMapRaw.put(confKey.substring(PulsarAdapterUtil.CONF_GATEGORY.Schema.label.length() + 1), confVal); + } + // Get client connection specific configuration settings, removing "client." prefix + // <<< https://pulsar.apache.org/docs/reference-configuration/#client >>> + else if (StringUtils.startsWith(confKey, PulsarAdapterUtil.CONF_GATEGORY.Client.label)) { + clientConfMapRaw.put(confKey.substring(PulsarAdapterUtil.CONF_GATEGORY.Client.label.length() + 1), confVal); + } + // Get producer specific configuration settings, removing "producer." prefix + // <<< https://pulsar.apache.org/docs/client-libraries-java/#configure-producer >>> + else if (StringUtils.startsWith(confKey, PulsarAdapterUtil.CONF_GATEGORY.Producer.label)) { + producerConfMapRaw.put(confKey.substring(PulsarAdapterUtil.CONF_GATEGORY.Producer.label.length() + 1), confVal); + } + // Get consumer specific configuration settings, removing "consumer." prefix + // <<< https://pulsar.apache.org/docs/client-libraries-java/#configure-consumer >>> + else if (StringUtils.startsWith(confKey, PulsarAdapterUtil.CONF_GATEGORY.Consumer.label)) { + consumerConfMapRaw.put(confKey.substring(PulsarAdapterUtil.CONF_GATEGORY.Consumer.label.length() + 1), confVal); + } + // Get reader specific configuration settings, removing "reader." prefix + // <<< https://pulsar.apache.org/docs/2.10.x/client-libraries-java/#configure-reader >>> + else if (StringUtils.startsWith(confKey, PulsarAdapterUtil.CONF_GATEGORY.Reader.label)) { + readerConfMapRaw.put(confKey.substring(PulsarAdapterUtil.CONF_GATEGORY.Reader.label.length() + 1), confVal); + } + } + } + } catch (IOException ioe) { + logger.error("Can't read the specified config properties file!"); + ioe.printStackTrace(); + } catch (ConfigurationException cex) { + logger.error("Error loading configuration items from the specified config properties file: " + canonicalFilePath); + cex.printStackTrace(); + } + } + + + public Map getSchemaConfMapRaw() { return this.schemaConfMapRaw; } + public Map getClientConfMapRaw() { return this.clientConfMapRaw; } + public Map getProducerConfMapRaw() { return this.producerConfMapRaw; } + public Map getProducerConfMapTgt() { return this.producerConfMapTgt; } + public Map getConsumerConfMapRaw() { return this.consumerConfMapRaw; } + public Map getConsumerConfMapTgt() { return this.consumerConfMapTgt; } + public Map getReaderConfMapRaw() { return this.readerConfMapRaw; } + public Map getReaderConfMapTgt() { return this.readerConfMapTgt; } + + + public String toString() { + return new ToStringBuilder(this). + append("schemaConfMapRaw", schemaConfMapRaw.toString()). + append("clientConfMapRaw", clientConfMapRaw.toString()). + append("producerConfMapRaw", producerConfMapRaw.toString()). + append("consumerConfMapRaw", consumerConfMapRaw.toString()). + append("readerConfMapRaw", readerConfMapRaw.toString()). + toString(); + } + + ////////////////// + // Get Schema related config + public boolean hasSchemaConfKey(String key) { + if (key.contains(PulsarAdapterUtil.CONF_GATEGORY.Schema.label)) + return schemaConfMapRaw.containsKey(key.substring(PulsarAdapterUtil.CONF_GATEGORY.Schema.label.length() + 1)); + else + return schemaConfMapRaw.containsKey(key); + } + public String getSchemaConfValueRaw(String key) { + if (hasSchemaConfKey(key)) { + if (key.contains(PulsarAdapterUtil.CONF_GATEGORY.Schema.label)) + return schemaConfMapRaw.get(key.substring(PulsarAdapterUtil.CONF_GATEGORY.Schema.label.length() + 1)); + else + return schemaConfMapRaw.get(key); + } + else { + return ""; + } + } + + + ////////////////// + // Get Pulsar client related config + public boolean hasClientConfKey(String key) { + if (key.contains(PulsarAdapterUtil.CONF_GATEGORY.Client.label)) + return clientConfMapRaw.containsKey(key.substring(PulsarAdapterUtil.CONF_GATEGORY.Client.label.length() + 1)); + else + return clientConfMapRaw.containsKey(key); + } + public String getClientConfValueRaw(String key) { + if (hasClientConfKey(key)) { + if (key.contains(PulsarAdapterUtil.CONF_GATEGORY.Client.label)) + return clientConfMapRaw.get(key.substring(PulsarAdapterUtil.CONF_GATEGORY.Client.label.length() + 1)); + else + return clientConfMapRaw.get(key); + } + else { + return ""; + } + } + + + ////////////////// + // Get Pulsar producer related config + public boolean hasProducerConfKey(String key) { + if (key.contains(PulsarAdapterUtil.CONF_GATEGORY.Producer.label)) + return producerConfMapRaw.containsKey(key.substring(PulsarAdapterUtil.CONF_GATEGORY.Producer.label.length() + 1)); + else + return producerConfMapRaw.containsKey(key); + } + public String getProducerConfValueRaw(String key) { + if (hasProducerConfKey(key)) { + if (key.contains(PulsarAdapterUtil.CONF_GATEGORY.Producer.label)) + return producerConfMapRaw.get(key.substring(PulsarAdapterUtil.CONF_GATEGORY.Producer.label.length()+1)); + else + return producerConfMapRaw.get(key); + } + else { + return ""; + } + } + + + ////////////////// + // Get Pulsar consumer related config + public boolean hasConsumerConfKey(String key) { + if (key.contains(PulsarAdapterUtil.CONF_GATEGORY.Consumer.label)) + return consumerConfMapRaw.containsKey(key.substring(PulsarAdapterUtil.CONF_GATEGORY.Consumer.label.length() + 1)); + else + return consumerConfMapRaw.containsKey(key); + } + public String getConsumerConfValueRaw(String key) { + if (hasConsumerConfKey(key)) { + if (key.contains(PulsarAdapterUtil.CONF_GATEGORY.Consumer.label)) + return consumerConfMapRaw.get(key.substring(PulsarAdapterUtil.CONF_GATEGORY.Consumer.label.length() + 1)); + else + return consumerConfMapRaw.get(key); + } + else { + return ""; + } + } + // NOTE: Below are not a standard Pulsar consumer configuration parameter as + // listed in "https://pulsar.apache.org/docs/en/client-libraries-java/#configure-consumer" + // They're custom-made configuration properties for NB pulsar driver consumer. + public int getConsumerTimeoutSeconds() { + String confValue = getConsumerConfValueRaw( + "consumer." + PulsarAdapterUtil.CONSUMER_CONF_CUSTOM_KEY.timeout.label); + return NumberUtils.toInt(confValue, -1); + } + + ////////////////// + // Get Pulsar reader related config + public boolean hasReaderConfKey(String key) { + if (key.contains(PulsarAdapterUtil.CONF_GATEGORY.Reader.label)) + return readerConfMapRaw.containsKey(key.substring(PulsarAdapterUtil.CONF_GATEGORY.Reader.label.length() + 1)); + else + return readerConfMapRaw.containsKey(key); + } + public String getReaderConfValueRaw(String key) { + if (hasReaderConfKey(key)) { + if (key.contains(PulsarAdapterUtil.CONF_GATEGORY.Reader.label)) + return readerConfMapRaw.get(key.substring(PulsarAdapterUtil.CONF_GATEGORY.Reader.label.length() + 1)); + else + return readerConfMapRaw.get(key); + } + else { + return ""; + } + } +} diff --git a/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/util/PulsarConfConverter.java b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/util/PulsarConfConverter.java new file mode 100644 index 000000000..e187e7b82 --- /dev/null +++ b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/util/PulsarConfConverter.java @@ -0,0 +1,428 @@ +/* + * 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.pulsar.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.nosqlbench.adapter.pulsar.exception.PulsarAdapterInvalidParamException; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.NumberUtils; +import org.apache.pulsar.client.api.*; +import org.apache.pulsar.client.impl.MultiplierRedeliveryBackoff; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class PulsarConfConverter { + + // <<< https://pulsar.apache.org/docs/client-libraries-java/#configure-producer >>> + private final static Map validStdProducerConfKeyTypeMap = Map.ofEntries( + Map.entry("topicName", "String"), + Map.entry("producerName","String"), + Map.entry("sendTimeoutMs","long"), + Map.entry("blockIfQueueFull","boolean"), + Map.entry("maxPendingMessages","int"), + Map.entry("maxPendingMessagesAcrossPartitions","int"), + Map.entry("messageRoutingMode","MessageRoutingMode"), + Map.entry("hashingScheme","HashingScheme"), + Map.entry("cryptoFailureAction","ProducerCryptoFailureAction"), + Map.entry("batchingMaxPublishDelayMicros","long"), + Map.entry("batchingMaxMessages","int"), + Map.entry("batchingEnabled","boolean"), + Map.entry("chunkingEnabled","boolean"), + Map.entry("compressionType","CompressionType"), + Map.entry("initialSubscriptionName","string") + ); + public static Map convertStdRawProducerConf(Map pulsarProducerConfMapRaw) { + Map producerConfObjMap = new HashMap<>(); + setConfObjMapForPrimitives(producerConfObjMap, pulsarProducerConfMapRaw, validStdProducerConfKeyTypeMap); + + /** + * Non-primitive type processing for Pulsar producer configuration items + */ + // TODO: Skip the following Pulsar configuration items for now because they're not really + // needed in the NB S4J testing at the moment. Add support for them when needed. + // * messageRoutingMode + // * hashingScheme + // * cryptoFailureAction + + // "compressionType" + // - expecting the following values: 'LZ4', 'ZLIB', 'ZSTD', 'SNAPPY' + String confKeyName = PulsarAdapterUtil.PRODUCER_CONF_STD_KEY.compressionType.label; + String confVal = pulsarProducerConfMapRaw.get(confKeyName); + String expectedVal = PulsarAdapterUtil.getValidCompressionTypeList(); + + if ( StringUtils.isNotBlank(confVal) ) { + if (StringUtils.containsIgnoreCase(expectedVal, confVal)) { + CompressionType compressionType = CompressionType.NONE; + + switch (StringUtils.upperCase(confVal)) { + case "LZ4": + compressionType = CompressionType.LZ4; + break; + case "ZLIB": + compressionType = CompressionType.ZLIB; + break; + case "ZSTD": + compressionType = CompressionType.ZSTD; + break; + case "SNAPPY": + compressionType = CompressionType.SNAPPY; + break; + } + + producerConfObjMap.put(confKeyName, compressionType); + } else { + throw new PulsarAdapterInvalidParamException( + getInvalidConfValStr(confKeyName, confVal, PulsarAdapterUtil.CONF_GATEGORY.Producer.label, expectedVal)); + } + } + + return producerConfObjMap; + } + + + // https://pulsar.apache.org/docs/client-libraries-java/#configure-consumer + private final static Map validStdConsumerConfKeyTypeMap = Map.ofEntries( + Map.entry("topicNames", "Set"), + Map.entry("topicsPattern","Pattern"), + Map.entry("subscriptionName","String"), + Map.entry("subscriptionType","SubscriptionType"), + Map.entry("receiverQueueSize","int"), + Map.entry("acknowledgementsGroupTimeMicros","long"), + Map.entry("negativeAckRedeliveryDelayMicros","long"), + Map.entry("maxTotalReceiverQueueSizeAcrossPartitions","int"), + Map.entry("consumerName","String"), + Map.entry("ackTimeoutMillis","long"), + Map.entry("tickDurationMillis","long"), + Map.entry("priorityLevel","int"), + Map.entry("cryptoFailureAction","ConsumerCryptoFailureAction"), + Map.entry("properties","SortedMap"), + Map.entry("readCompacted","boolean"), + Map.entry("subscriptionInitialPosition", "SubscriptionInitialPosition"), + Map.entry("patternAutoDiscoveryPeriod", "int"), + Map.entry("regexSubscriptionMode", "RegexSubscriptionMode"), + Map.entry("deadLetterPolicy", "DeadLetterPolicy"), + Map.entry("autoUpdatePartitions", "boolean"), + Map.entry("replicateSubscriptionState", "boolean"), + Map.entry("negativeAckRedeliveryBackoff", "RedeliveryBackoff"), + Map.entry("ackTimeoutRedeliveryBackoff", "RedeliveryBackoff"), + Map.entry("autoAckOldestChunkedMessageOnQueueFull", "boolean"), + Map.entry("maxPendingChunkedMessage", "int"), + Map.entry("expireTimeOfIncompleteChunkedMessageMillis", "long") + ); + public static Map convertStdRawConsumerConf(Map pulsarConsumerConfMapRaw) { + Map consumerConfObjMap = new HashMap<>(); + setConfObjMapForPrimitives(consumerConfObjMap, pulsarConsumerConfMapRaw, validStdConsumerConfKeyTypeMap); + + /** + * Non-primitive type processing for Pulsar consumer configuration items + */ + // NOTE: The following non-primitive type configuration items are excluded since + // they'll be handled in PulsarBasedOpDispenser.getConsumer() method directly + // * topicNames + // * topicPattern + // * subscriptionType + + // TODO: Skip the following Pulsar configuration items for now because they're not really + // needed in the NB S4J testing right now. Add the support for them when needed. + // * cryptoFailureAction + + + // "properties" has value type "SortedMap" + // - expecting the value string has the format: a JSON string that includes a set of key/value pairs + String confKeyName = PulsarAdapterUtil.CONSUMER_CONF_STD_KEY.properties.label; + String confVal = pulsarConsumerConfMapRaw.get(confKeyName); + String expectedVal = "{\"property1\":\"value1\", \"property2\":\"value2\"}, ..."; + + ObjectMapper mapper = new ObjectMapper(); + + if (StringUtils.isNotBlank(confVal)) { + try { + Map consumerProperties = mapper.readValue(confVal, Map.class); + + // Empty map value is considered as no value + if (!consumerProperties.isEmpty()) { + consumerConfObjMap.put(confKeyName, consumerProperties); + } + + } catch (Exception e) { + throw new PulsarAdapterInvalidParamException( + getInvalidConfValStr(confKeyName, confVal, PulsarAdapterUtil.CONF_GATEGORY.Consumer.label, expectedVal)); + } + } + + // "subscriptionInitialPosition" + // - expecting the following values: 'Latest' (default), + confKeyName = PulsarAdapterUtil.CONSUMER_CONF_STD_KEY.subscriptionInitialPosition.label; + confVal = pulsarConsumerConfMapRaw.get(confKeyName); + expectedVal = PulsarAdapterUtil.getValidSubscriptionInitialPositionList(); + + if (StringUtils.isNotBlank(confVal)) { + try { + SubscriptionInitialPosition subInitPos = SubscriptionInitialPosition.Latest; + if (!StringUtils.isBlank(confVal)) { + subInitPos = SubscriptionInitialPosition.valueOf(confVal); + } + consumerConfObjMap.put(confKeyName, subInitPos); + + } catch (Exception e) { + throw new PulsarAdapterInvalidParamException( + getInvalidConfValStr(confKeyName, confVal, PulsarAdapterUtil.CONF_GATEGORY.Consumer.label, expectedVal)); + } + } + + // "regexSubscriptionMode" + // - expecting the following values: 'PersistentOnly' (default), 'NonPersistentOnly', and 'AllTopics' + confKeyName = PulsarAdapterUtil.CONSUMER_CONF_STD_KEY.regexSubscriptionMode.label; + confVal = pulsarConsumerConfMapRaw.get(confKeyName); + expectedVal = PulsarAdapterUtil.getValidRegexSubscriptionModeList(); + + if (StringUtils.isNotBlank(confVal)) { + try { + RegexSubscriptionMode regexSubscriptionMode = RegexSubscriptionMode.PersistentOnly; + if (!StringUtils.isBlank(confVal)) { + regexSubscriptionMode = RegexSubscriptionMode.valueOf(confVal); + } + consumerConfObjMap.put(confKeyName, regexSubscriptionMode); + + } catch (Exception e) { + throw new PulsarAdapterInvalidParamException( + getInvalidConfValStr(confKeyName, confVal, PulsarAdapterUtil.CONF_GATEGORY.Consumer.label, expectedVal)); + } + } + + // "deadLetterPolicy" + // - expecting the value is a JSON string has the format: + // {"maxRedeliverCount":"","deadLetterTopic":"","initialSubscriptionName":""} + confKeyName = PulsarAdapterUtil.CONSUMER_CONF_STD_KEY.deadLetterPolicy.label; + confVal = pulsarConsumerConfMapRaw.get(confKeyName); + expectedVal = "{" + + "\"maxRedeliverCount\":\"\" (mandatory)," + + "\"retryLetterTopic\":\"\"," + + "\"deadLetterTopic\":\"\"," + + "\"initialSubscriptionName\":\"\"}"; + + if (StringUtils.isNotBlank(confVal)) { + try { + Map dlqPolicyMap = mapper.readValue(confVal, Map.class); + + // Empty map value is considered as no value + if (!dlqPolicyMap.isEmpty()) { + boolean valid = true; + + // The JSON key must be one of "maxRedeliverCount", "deadLetterTopic", "initialSubscriptionName" + for (String key : dlqPolicyMap.keySet()) { + if (!StringUtils.equalsAnyIgnoreCase(key, "maxRedeliverCount", + "retryLetterTopic", "deadLetterTopic", "initialSubscriptionName")) { + valid = false; + break; + } + } + + // DLQ.maxRedeliverCount is mandatory + if ( valid && !dlqPolicyMap.containsKey("maxRedeliverCount")) { + valid = false; + } + + String maxRedeliverCountStr = dlqPolicyMap.get("maxRedeliverCount"); + if (!NumberUtils.isCreatable(maxRedeliverCountStr)) { + valid = false; + } + + if (valid) { + DeadLetterPolicy.DeadLetterPolicyBuilder builder = DeadLetterPolicy.builder() + .maxRedeliverCount(NumberUtils.toInt(maxRedeliverCountStr)); + + String retryTopicName = dlqPolicyMap.get("retryLetterTopic"); + String dlqTopicName = dlqPolicyMap.get("deadLetterTopic"); + String initialSubName = dlqPolicyMap.get("initialSubscriptionName"); + + if (StringUtils.isNotBlank(retryTopicName)) + builder.retryLetterTopic(retryTopicName); + + if (StringUtils.isNotBlank(dlqTopicName)) + builder.deadLetterTopic(dlqTopicName); + + if (StringUtils.isNotBlank(initialSubName)) + builder.initialSubscriptionName(initialSubName); + + DeadLetterPolicy deadLetterPolicy = builder.build(); + consumerConfObjMap.put(confKeyName, deadLetterPolicy); + } else { + throw new PulsarAdapterInvalidParamException( + getInvalidConfValStr(confKeyName, confVal, PulsarAdapterUtil.CONF_GATEGORY.Consumer.label, expectedVal)); + } + } + } catch (Exception e) { + throw new PulsarAdapterInvalidParamException( + getInvalidConfValStr(confKeyName, confVal, PulsarAdapterUtil.CONF_GATEGORY.Consumer.label, expectedVal)); + } + } + + // "negativeAckRedeliveryBackoff" or "ackTimeoutRedeliveryBackoff" + // - expecting the value is a JSON string has the format: + // {"minDelayMs":"", "maxDelayMs":"", "multiplier":""} + String[] redeliveryBackoffConfigSet = { + PulsarAdapterUtil.CONSUMER_CONF_STD_KEY.negativeAckRedeliveryBackoff.label, + PulsarAdapterUtil.CONSUMER_CONF_STD_KEY.ackTimeoutRedeliveryBackoff.label + }; + expectedVal = "{" + + "\"minDelayMs\":\"\"," + + "\"maxDelayMs\":\"\"," + + "\"multiplier\":\"\"}"; + + for (String confKey : redeliveryBackoffConfigSet) { + confVal = pulsarConsumerConfMapRaw.get(confKey); + + if (StringUtils.isNotBlank(confVal)) { + try { + Map redliveryBackoffMap = mapper.readValue(confVal, Map.class); + + // Empty map value is considered as no value + if (! redliveryBackoffMap.isEmpty()) { + boolean valid = true; + + // The JSON key must be one of "maxRedeliverCount", "deadLetterTopic", "initialSubscriptionName" + for (String key : redliveryBackoffMap.keySet()) { + if (!StringUtils.equalsAnyIgnoreCase(key, + "minDelayMs", "maxDelayMs", "multiplier")) { + valid = false; + break; + } + } + + String minDelayMsStr = redliveryBackoffMap.get("minDelayMs"); + String maxDelayMsStr = redliveryBackoffMap.get("maxDelayMs"); + String multiplierStr = redliveryBackoffMap.get("multiplier"); + + if ((StringUtils.isNotBlank(minDelayMsStr) && !NumberUtils.isCreatable(minDelayMsStr)) || + (StringUtils.isNotBlank(maxDelayMsStr) && !NumberUtils.isCreatable(maxDelayMsStr)) || + (StringUtils.isNotBlank(multiplierStr) && !NumberUtils.isCreatable(multiplierStr))) { + valid = false; + } + + if (valid) { + RedeliveryBackoff redeliveryBackoff = MultiplierRedeliveryBackoff.builder() + .minDelayMs(NumberUtils.toLong(minDelayMsStr)) + .maxDelayMs(NumberUtils.toLong(maxDelayMsStr)) + .multiplier(NumberUtils.toDouble(multiplierStr)) + .build(); + + consumerConfObjMap.put(confKey, redeliveryBackoff); + + } else { + throw new PulsarAdapterInvalidParamException( + getInvalidConfValStr(confKey, confVal, PulsarAdapterUtil.CONF_GATEGORY.Consumer.label, expectedVal)); + } + } + + } catch (Exception e) { + throw new PulsarAdapterInvalidParamException( + getInvalidConfValStr(confKey, confVal, PulsarAdapterUtil.CONF_GATEGORY.Consumer.label, expectedVal)); + } + } + } + + // Remove non-standard consumer configuration items + for (String confKey : consumerConfObjMap.keySet()) { + if (PulsarAdapterUtil.isCustomConsumerConfItem(confKey)) { + consumerConfObjMap.remove(confKey); + } + } + + return consumerConfObjMap; + } + + + // Utility function + // - get configuration key names by the value type + private static List getStdConfKeyNameByValueType(Map confKeyTypeMap, String tgtValType) { + ArrayList confKeyNames = new ArrayList<>(); + + for (Map.Entry entry: confKeyTypeMap.entrySet()) { + if (StringUtils.equalsIgnoreCase(entry.getValue().toString(), tgtValType)) { + confKeyNames.add(entry.getKey().toString()); + } + } + + return confKeyNames; + } + + // Conversion from Map to Map for configuration items with primitive + // value types + private static void setConfObjMapForPrimitives( + Map tgtConfObjMap, + Map srcConfMapRaw, + Map validConfKeyTypeMap) + { + List confKeyList; + + // All configuration items with "String" as the value type + confKeyList = getStdConfKeyNameByValueType(validConfKeyTypeMap, "String"); + for (String confKey : confKeyList) { + if (srcConfMapRaw.containsKey(confKey)) { + String confVal = srcConfMapRaw.get(confKey); + if (StringUtils.isNotBlank(confVal)) { + tgtConfObjMap.put(confKey, confVal); + } + } + } + + // All configuration items with "long" as the value type + confKeyList = getStdConfKeyNameByValueType(validConfKeyTypeMap, "long"); + for (String confKey : confKeyList) { + if (srcConfMapRaw.containsKey(confKey)) { + String confVal = srcConfMapRaw.get(confKey); + if (StringUtils.isNotBlank(confVal)) { + tgtConfObjMap.put(confKey, Long.valueOf(confVal)); + } + } + } + + // All configuration items with "int" as the value type + confKeyList = getStdConfKeyNameByValueType(validConfKeyTypeMap, "int"); + for (String confKey : confKeyList) { + if (srcConfMapRaw.containsKey(confKey)) { + String confVal = srcConfMapRaw.get(confKey); + if (StringUtils.isNotBlank(confVal)) { + tgtConfObjMap.put(confKey, Integer.valueOf(confVal)); + } + } + } + + // All configuration items with "boolean" as the value type + confKeyList = getStdConfKeyNameByValueType(validConfKeyTypeMap, "boolean"); + for (String confKey : confKeyList) { + if (srcConfMapRaw.containsKey(confKey)) { + String confVal = srcConfMapRaw.get(confKey); + if (StringUtils.isNotBlank(confVal)) { + tgtConfObjMap.put(confKey, Boolean.valueOf(confVal)); + } + } + } + + // TODO: So far the above primitive types should be good enough. + // Add support for other types when needed + } + + private static String getInvalidConfValStr(String confKey, String confVal, String configCategory, String expectedVal) { + return "Incorrect value \"" + confVal + "\" for Pulsar " + configCategory + + " configuration item of \"" + confKey + "\". Expecting the following value (format): " + expectedVal; + } +} diff --git a/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/util/ReceivedMessageSequenceTracker.java b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/util/ReceivedMessageSequenceTracker.java new file mode 100644 index 000000000..f929ab25a --- /dev/null +++ b/adapter-pulsar/src/main/java/io/nosqlbench/adapter/pulsar/util/ReceivedMessageSequenceTracker.java @@ -0,0 +1,169 @@ +package io.nosqlbench.adapter.pulsar.util; + +/* + * 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. + */ + + +import com.codahale.metrics.Counter; + +import java.util.Iterator; +import java.util.SortedSet; +import java.util.TreeSet; + +/** + * Detects message loss, message duplication and out-of-order message delivery + * based on a monotonic sequence number that each received message contains. + *

+ * Out-of-order messages are detected with a maximum look behind of 1000 sequence number entries. + * This is currently defined as a constant, {@link ReceivedMessageSequenceTracker#DEFAULT_MAX_TRACK_OUT_OF_ORDER_SEQUENCE_NUMBERS}. + */ +public class ReceivedMessageSequenceTracker implements AutoCloseable { + private static final int DEFAULT_MAX_TRACK_OUT_OF_ORDER_SEQUENCE_NUMBERS = 1000; + private static final int DEFAULT_MAX_TRACK_SKIPPED_SEQUENCE_NUMBERS = 1000; + // message out-of-sequence error counter + private final Counter msgErrOutOfSeqCounter; + // duplicate message error counter + private final Counter msgErrDuplicateCounter; + // message loss error counter + private final Counter msgErrLossCounter; + private final SortedSet pendingOutOfSeqNumbers; + private final int maxTrackOutOfOrderSequenceNumbers; + private final SortedSet skippedSeqNumbers; + private final int maxTrackSkippedSequenceNumbers; + private long expectedNumber = -1; + + public ReceivedMessageSequenceTracker(Counter msgErrOutOfSeqCounter, Counter msgErrDuplicateCounter, Counter msgErrLossCounter) { + this(msgErrOutOfSeqCounter, msgErrDuplicateCounter, msgErrLossCounter, + DEFAULT_MAX_TRACK_OUT_OF_ORDER_SEQUENCE_NUMBERS, DEFAULT_MAX_TRACK_SKIPPED_SEQUENCE_NUMBERS); + } + + public ReceivedMessageSequenceTracker(Counter msgErrOutOfSeqCounter, Counter msgErrDuplicateCounter, Counter msgErrLossCounter, + int maxTrackOutOfOrderSequenceNumbers, int maxTrackSkippedSequenceNumbers) { + this.msgErrOutOfSeqCounter = msgErrOutOfSeqCounter; + this.msgErrDuplicateCounter = msgErrDuplicateCounter; + this.msgErrLossCounter = msgErrLossCounter; + this.maxTrackOutOfOrderSequenceNumbers = maxTrackOutOfOrderSequenceNumbers; + this.maxTrackSkippedSequenceNumbers = maxTrackSkippedSequenceNumbers; + this.pendingOutOfSeqNumbers = new TreeSet<>(); + this.skippedSeqNumbers = new TreeSet<>(); + } + + /** + * Notifies the tracker about a received sequence number + * + * @param sequenceNumber the sequence number of the received message + */ + public void sequenceNumberReceived(long sequenceNumber) { + if (expectedNumber == -1) { + expectedNumber = sequenceNumber + 1; + return; + } + + if (sequenceNumber < expectedNumber) { + if (skippedSeqNumbers.remove(sequenceNumber)) { + // late out-of-order delivery was detected + // decrease the loss counter + msgErrLossCounter.dec(); + // increment the out-of-order counter + msgErrOutOfSeqCounter.inc(); + } else { + msgErrDuplicateCounter.inc(); + } + return; + } + + boolean messagesSkipped = false; + if (sequenceNumber > expectedNumber) { + if (pendingOutOfSeqNumbers.size() == maxTrackOutOfOrderSequenceNumbers) { + messagesSkipped = processLowestPendingOutOfSequenceNumber(); + } + if (!pendingOutOfSeqNumbers.add(sequenceNumber)) { + msgErrDuplicateCounter.inc(); + } + } else { + // sequenceNumber == expectedNumber + expectedNumber++; + } + processPendingOutOfSequenceNumbers(messagesSkipped); + cleanUpTooFarBehindOutOfSequenceNumbers(); + } + + private boolean processLowestPendingOutOfSequenceNumber() { + // remove the lowest pending out of sequence number + Long lowestOutOfSeqNumber = pendingOutOfSeqNumbers.first(); + pendingOutOfSeqNumbers.remove(lowestOutOfSeqNumber); + if (lowestOutOfSeqNumber > expectedNumber) { + // skip the expected number ahead to the number after the lowest sequence number + // increment the counter with the amount of sequence numbers that got skipped + // keep track of the skipped sequence numbers to detect late out-of-order message delivery + for (long l = expectedNumber; l < lowestOutOfSeqNumber; l++) { + msgErrLossCounter.inc(); + skippedSeqNumbers.add(l); + if (skippedSeqNumbers.size() > maxTrackSkippedSequenceNumbers) { + skippedSeqNumbers.remove(skippedSeqNumbers.first()); + } + } + expectedNumber = lowestOutOfSeqNumber + 1; + return true; + } else { + msgErrLossCounter.inc(); + } + return false; + } + + private void processPendingOutOfSequenceNumbers(boolean messagesSkipped) { + // check if there are previously received out-of-order sequence number that have been received + while (pendingOutOfSeqNumbers.remove(expectedNumber)) { + expectedNumber++; + if (!messagesSkipped) { + msgErrOutOfSeqCounter.inc(); + } + } + } + + private void cleanUpTooFarBehindOutOfSequenceNumbers() { + // remove sequence numbers that are too far behind + for (Iterator iterator = pendingOutOfSeqNumbers.iterator(); iterator.hasNext(); ) { + Long number = iterator.next(); + if (number < expectedNumber - maxTrackOutOfOrderSequenceNumbers) { + msgErrLossCounter.inc(); + iterator.remove(); + } else { + break; + } + } + } + + /** + * Handles the possible pending out of sequence numbers. Mainly needed in unit tests to assert the + * counter values. + */ + @Override + public void close() { + while (!pendingOutOfSeqNumbers.isEmpty()) { + processPendingOutOfSequenceNumbers(processLowestPendingOutOfSequenceNumber()); + } + } + + public int getMaxTrackOutOfOrderSequenceNumbers() { + return maxTrackOutOfOrderSequenceNumbers; + } + + public int getMaxTrackSkippedSequenceNumbers() { + return maxTrackSkippedSequenceNumbers; + } +} diff --git a/adapter-pulsar/src/main/resources/bindingtest.yaml b/adapter-pulsar/src/main/resources/bindingtest.yaml new file mode 100644 index 000000000..e687f5fa4 --- /dev/null +++ b/adapter-pulsar/src/main/resources/bindingtest.yaml @@ -0,0 +1,4 @@ +bindings: + tenant: Mod(100); Div(10); ToString(); Prefix("tnt") + namespace: Mod(10); Div(5); ToString(); Prefix("ns") + topic: Mod(5); ToString(); Prefix("tp") diff --git a/adapter-pulsar/src/main/resources/config.properties b/adapter-pulsar/src/main/resources/config.properties new file mode 100644 index 000000000..7afb0252b --- /dev/null +++ b/adapter-pulsar/src/main/resources/config.properties @@ -0,0 +1,43 @@ +9### Schema related configurations - schema.xxx +# valid types: +# - primitive type (https://pulsar.apache.org/docs/en/schema-understand/#primitive-type) +# - keyvalue (https://pulsar.apache.org/docs/en/schema-understand/#keyvalue) +# - strut (complex type) (https://pulsar.apache.org/docs/en/schema-understand/#struct) +# avro, json, protobuf +# +# TODO: as a starting point, only supports the following types +# 1) primitive types, including bytearray (byte[]) which is default, for messages without schema +# 2) Avro for messages with schema +#schema.key.type=avro +#schema.key.definition=file:///Users/yabinmeng/DataStax/MyNBMain/nosqlbench/adapter-pulsar/src/main/resources/iot-key-example.avsc +#schema.type=avro +#schema.definition=file:///Users/yabinmeng/DataStax/MyNBMain/nosqlbench/adapter-pulsar/src/main/resources/iot-value-example.avsc +schema.type= +schema.definition= + + +### Pulsar client related configurations - client.xxx +# http://pulsar.apache.org/docs/en/client-libraries-java/#client +client.connectionTimeoutMs=5000 +client.authPluginClassName=org.apache.pulsar.client.impl.auth.AuthenticationToken +# Cluster admin +client.authParams= + + +### Producer related configurations (global) - producer.xxx +# http://pulsar.apache.org/docs/en/client-libraries-java/#configure-producer +producer.sendTimeoutMs= +producer.blockIfQueueFull=true + + +### Consumer related configurations (global) - consumer.xxx +# http://pulsar.apache.org/docs/en/client-libraries-java/#configure-consumer +consumer.subscriptionInitialPosition=Earliest +consumer.ackTimeoutMillis=10000 +consumer.regexSubscriptionMode=AllTopics +consumer.deadLetterPolicy={"maxRedeliverCount":"5","retryLetterTopic":"public/default/retry","deadLetterTopic":"public/default/dlq","initialSubscriptionName":"dlq-sub"} +consumer.ackTimeoutRedeliveryBackoff={"minDelayMs":"10","maxDelayMs":"20","multiplier":"1.2"} + + +### Reader related configurations (global) - reader.xxx +# https://pulsar.apache.org/docs/en/client-libraries-java/#reader diff --git a/adapter-pulsar/src/main/resources/iot-key-example.avsc b/adapter-pulsar/src/main/resources/iot-key-example.avsc new file mode 100644 index 000000000..f36b52bc3 --- /dev/null +++ b/adapter-pulsar/src/main/resources/iot-key-example.avsc @@ -0,0 +1,9 @@ +{ + "type": "record", + "name": "IotSensorKey", + "namespace": "TestNS", + "fields" : [ + {"name": "Location", "type": "string"}, + {"name": "WellID", "type": "string"} + ] +} diff --git a/adapter-pulsar/src/main/resources/iot-value-example.avsc b/adapter-pulsar/src/main/resources/iot-value-example.avsc new file mode 100644 index 000000000..20bb894fd --- /dev/null +++ b/adapter-pulsar/src/main/resources/iot-value-example.avsc @@ -0,0 +1,11 @@ +{ + "type": "record", + "name": "IotSensor", + "namespace": "TestNS", + "fields" : [ + {"name": "SensorID", "type": "string"}, + {"name": "SensorType", "type": "string"}, + {"name": "ReadingTime", "type": "string"}, + {"name": "ReadingValue", "type": "float"} + ] +} diff --git a/adapter-pulsar/src/main/resources/pulsar.md b/adapter-pulsar/src/main/resources/pulsar.md new file mode 100644 index 000000000..17b57dc6e --- /dev/null +++ b/adapter-pulsar/src/main/resources/pulsar.md @@ -0,0 +1,253 @@ +- [1. Overview](#1-overview) + - [1.1. Issues Tracker](#11-issues-tracker) +- [2. Execute the NB Pulsar Driver Workload](#2-execute-the-nb-pulsar-driver-workload) + - [2.1. NB Pulsar Driver Yaml File High Level Structure](#21-nb-pulsar-driver-yaml-file-high-level-structure) + - [2.2. NB Pulsar Driver Configuration Parameters](#22-nb-pulsar-driver-configuration-parameters) + - [2.2.1. Global Level Parameters](#221-global-level-parameters) + - [2.2.2. Document Level Parameters](#222-document-level-parameters) +- [3. NB Pulsar Driver OpTemplates](#3-nb-pulsar-driver-optemplates) +- [4. Message Generation and Schema Support](#4-message-generation-and-schema-support) + - [4.1. Message Generation](#41-message-generation) + - [4.2. Schema Support](#42-schema-support) + +# 1. Overview + +This driver allows you to simulate and run different types of workloads (as below) against a Pulsar cluster through NoSQLBench (NB). +* Admin API - create/delete tenants +* Admin API - create/delete namespaces +* Admin API - create/delete topics + * Topics can be partitioned or non-partitioned +* Producer - publish messages with schema support + * Default schema type is byte[] + * Avro schema and KeyValue schema are also supported +* Consumer - consume messages with schema support and the following support + * Different subscription types + * Multi-topic subscription (including Topic patterns) + * Subscription initial position + * Dead letter topic policy + * Negative acknowledgement and acknowledgement timeout redelivery backoff policy + + +## 1.1. Issues Tracker + +If you have issues or new requirements for this driver, please add them at the [pulsar issues tracker](https://github.com/nosqlbench/nosqlbench/issues/new?labels=pulsar). + +# 2. Execute the NB Pulsar Driver Workload + +In order to run a NB Pulsar driver workload, it follows similar command as other NB driver types. But it does have its unique execution parameters. The general command has the following format: + +```shell + run driver=pulsar threads= cycles= web_url= service_url= config= yaml= [] +``` + +In the above command, make sure the driver type is **pulsar** and provide the following Pulsar driver specific parameters: +* ***web_url***: Pulsar web service url and default to "http://localhost:8080" +* ***service_url***: Pulsar native protocol service url and default to "pulsar://localhost:6650" +* ***config***: Pulsar schema/client/producer/consumer related configuration (as a property file) + +## 2.1. NB Pulsar Driver Yaml File High Level Structure + +Just like other NB driver types, the actual NB Pulsar workload is defined in a YAML file with the following high level structure: + +```yaml +description: | + ... + +bindings: + ... + +params: + ... + +blocks: + : + ops: + op1: + : "" + : "" + : "" + ... + + : + ... +``` + +* ***description***: This is an (optional) section where to provide general description of the Pulsar NB workload defined in this file. +* ***bindings***: This section defines all NB bindings that are required in all OpTemplate blocks +* ***params***: This section defines **Document level** configuration parameters that apply to all OpTemplate blocks. +* ***blocks***: This section defines the OpTemplate blocks that are needed to execute Pulsar specific workloads. Each OpTemplate block may contain multiple OpTemplates. + +## 2.2. NB Pulsar Driver Configuration Parameters + +The NB Pulsar driver configuration parameters can be set at 3 different levels: +* Global level +* Document level + * The parameters at this level are those within a NB yaml file that impact all OpTemplates +* Op level (or Cycle level) + * The parameters at this level are those within a NB yaml file that are associated with each individual OpTemplate + +Please **NOTE** that when a parameter is specified at multiple levels, the one at the lowest level takes precedence. + +### 2.2.1. Global Level Parameters + +The parameters at this level are those listed in the command line config properties file. + +The NB Pulsar driver relies on Pulsar's [Java Client API](https://pulsar.apache.org/docs/en/client-libraries-java/) complete its workloads such as creating/deleting tenants/namespaces/topics, generating messages, creating producers to send messages, and creating consumers to receive messages. The Pulsar client API has different configuration parameters to control the execution behavior. For example, [this document](https://pulsar.apache.org/docs/en/client-libraries-java/#configure-producer) lists all possible configuration parameters for how a Pulsar producer can be created. + +All these Pulsar "native" parameters are supported by the NB Pulsar driver, via the global configuration properties file (e.g. **config.properties**). An example of the structure of this file looks like below: + +```properties +### Schema related configurations - MUST start with prefix "schema." +#schema.key.type=avro +#schema.key.definition= +schema.type=avro +schema.definition= + +### Pulsar client related configurations - MUST start with prefix "client." +# http://pulsar.apache.org/docs/en/client-libraries-java/#client +client.connectionTimeoutMs=5000 +client.authPluginClassName=org.apache.pulsar.client.impl.auth.AuthenticationToken +client.authParams= +# ... + +### Producer related configurations (global) - MUST start with prefix "producer." +# http://pulsar.apache.org/docs/en/client-libraries-java/#configure-producer +producer.sendTimeoutMs= +producer.blockIfQueueFull=true +# ... + +### Consumer related configurations (global) - MUST start with prefix "consumer." +# http://pulsar.apache.org/docs/en/client-libraries-java/#configure-consumer +consumer.subscriptionInitialPosition=Earliest +consumer.deadLetterPolicy={"maxRedeliverCount":"5","retryLetterTopic":"public/default/retry","deadLetterTopic":"public/default/dlq","initialSubscriptionName":"dlq-sub"} +consumer.ackTimeoutRedeliveryBackoff={"minDelayMs":"10","maxDelayMs":"20","multiplier":"1.2"} +# ... +``` + +There are multiple sections in this file that correspond to different +categories of the configuration parameters: +* **`Pulsar Schema` related settings**: + * All settings under this section starts with **schema.** prefix. + * At the moment, there are 3 schema types supported + * Default raw ***byte[]*** + * Avro schema for the message payload + * KeyValue based Avro schema for both message key and message payload +* **`Pulsar Client` related settings**: + * All settings under this section starts with **client.** prefix. + * This section defines all configuration parameters that are related with defining a PulsarClient object. + * See [Pulsar Doc Reference](https://pulsar.apache.org/docs/en/client-libraries-java/#default-broker-urls-for-standalone-clusters) +* **`Pulsar Producer` related settings**: + * All settings under this section starts with **producer** prefix. + * This section defines all configuration parameters that are related with defining a Pulsar Producer object. + * See [Pulsar Doc Reference](https://pulsar.apache.org/docs/en/client-libraries-java/#configure-producer) +* **`Pulsar Consumer` related settings**: + * All settings under this section starts with **consumer** prefix. + * This section defines all configuration parameters that are related with defining a Pulsar Consumer object. + * See [Pulsar Doc Reference](http://pulsar.apache.org/docs/en/client-libraries-java/#configure-consumer) + +### 2.2.2. Document Level Parameters + +For the Pulsar NB driver, Document level parameters can only be statically bound; and currently, the following Document level configuration parameters are supported: + +* ***async_api*** (boolean): + * When true, use async Pulsar client API. +* ***use_transaction*** (boolean): + * When true, use Pulsar transaction. +* ***admin_delop*** (boolean): + * When true, delete Tenants/Namespaces/Topics. Otherwise, create them. + * Only applicable to administration related operations +* ***seq_tracking*** (boolean): + * When true, a sequence number is created as part of each message's properties + * This parameter is used in conjunction with the next one in order to simulate abnormal message processing errors and then be able to detect such errors successfully. +* ***seqerr_simu***: + * A list of error simulation types separated by comma (,) + * Valid error simulation types + * `out_of_order`: simulate message out of sequence + * `msg_loss`: simulate message loss + * `msg_dup`: simulate message duplication +* ***e2e_starting_time_source***: + * Starting timestamp for end-to-end operation. When specified, will update the `e2e_msg_latency` histogram with the calculated end-to-end latency. The latency is calculated by subtracting the starting time from the current time. The starting time is determined from a configured starting time source. The unit of the starting time is milliseconds since epoch. + * The possible values for `e2e_starting_time_source`: + * `message_publish_time` : uses the message publishing timestamp as the starting time + * `message_event_time` : uses the message event timestamp as the starting time + * `message_property_e2e_starting_time` : uses a message property `e2e_starting_time` as the starting time. + +# 3. NB Pulsar Driver OpTemplates + +For the NB Pulsar driver, each OpTemplate has the following format: +```yaml +blocks: + : + ops: + : + : + : "" + : "" + ... +``` + +The `OpTypeIdentifier` determines which NB Pulsar workload type (`OpType`) to run, and it has the following value: +```java +public enum PulsarOpType { + AdminTenant, + AdminNamespace, + AdminTopic, + MessageProduce, + MessageConsume; +} +``` + +Its value is mandatory and depending on the actual identifier, its value can be one of the following: +* ***Tenant name***: for `AdminTenant` type +* ***Namespace name***: for `AdminNamespace` type and in format "/" +* ***Topic name***: for the rest of the types and in format [(persistent|non-persistent)://]// + is mandatory for each NB Pulsar operation type + +Each Pulsar `OpType` may have optional Op specific parameters. Please refer to [here](yaml_examples) for the example NB Pulsar YAML files for each OpType + +# 4. Message Generation and Schema Support + +## 4.1. Message Generation + +A Pulsar message has three main components: message key, message properties, and message payload. Among them, message payload is mandatory when creating a message. + +When running the "message producing" workload, the NB Pulsar driver is able to generate a message with its full content via the following OpTemplate level parameters: +* `msg_key`: defines message key value +* `msg_property`: defines message property values +* `msg_value`: defines message payload value + +The actual values of them can be static or dynamic (which are determined by NB data binding rules) + +For `msg_key`, its value can be either +* a plain text string, or +* a JSON string that follows the specified "key" Avro schema (when KeyValue schema is used) + +For `msg_property`, its value needs to be a JSON string that contains a list of key-value pairs. An example is as below. Please **NOTE** that if the provided value is not a valid JSON string, the NB Pulsar driver will ignore it and treat the message as having no properties. +``` + msg_property: | + { + "prop1": "{myprop1}", + "prop2": "{myprop2}" + } +``` + +For `msg_value`, its value can be either +* a plain simple text, or +* a JSON string that follows the specified "value" Avro schema (when Avro schema or KeyValue schema is used) + +## 4.2. Schema Support + +The NB Pulsar driver supports the following Pulsar schema types: +* Primitive schema types +* Avro schema type (only for message payload - `msg_value`) +* KeyValue schema type (with both key and value follows an Avro schema) + +The following 2 global configuration parameters define the required schema type +* `schema.key.type`: defines message key type +* `schema.type`: defines message value type + For them, if the parameter value is not specified, it means using the default `byte[]/BYTES` type as the schema type. Otherwise, if it is specified as "avro", it means using Avro as the schema type. + +The following 2 global configuration parameters define the schema specification (**ONLY** needed when Avro is the schema type) +* `schema.key.definition`: a file path that defines the message key Avro schema specification +* `schema.definition`: a file path the message value Avro schema specification + The NB Pulsar driver will throw an error if the schema type is Avro but no schema specification definition file is not provided or is not valid. diff --git a/adapter-pulsar/src/main/resources/yaml_examples/admin_namespace.yaml b/adapter-pulsar/src/main/resources/yaml_examples/admin_namespace.yaml new file mode 100644 index 000000000..b99073d31 --- /dev/null +++ b/adapter-pulsar/src/main/resources/yaml_examples/admin_namespace.yaml @@ -0,0 +1,14 @@ +bindings: + # 20 topics: 10 tenants, 2 namespaces/tenant + tenant: Mod(20); Div(2); ToString(); Prefix("tnt") + namespace: Mod(2); ToString(); Prefix("ns") + +params: + async_api: "false" + admin_delop: "false" + +blocks: + admin-namespace-block: + ops: + op1: + AdminNamespace: "{tenant}/{namespace}" diff --git a/adapter-pulsar/src/main/resources/yaml_examples/admin_tenant.yaml b/adapter-pulsar/src/main/resources/yaml_examples/admin_tenant.yaml new file mode 100644 index 000000000..8564fb2f0 --- /dev/null +++ b/adapter-pulsar/src/main/resources/yaml_examples/admin_tenant.yaml @@ -0,0 +1,15 @@ +bindings: + # 10 tenants + tenant: Mod(10); ToString(); Prefix("tnt") + +params: + async_api: "false" + admin_delop: "false" + +blocks: + admin-tenant-block: + ops: + op1: + AdminTopic: "{tenant}" + admin_roles: "" + allowed_clusters: "" diff --git a/adapter-pulsar/src/main/resources/yaml_examples/admin_topic.yaml b/adapter-pulsar/src/main/resources/yaml_examples/admin_topic.yaml new file mode 100644 index 000000000..ce742a782 --- /dev/null +++ b/adapter-pulsar/src/main/resources/yaml_examples/admin_topic.yaml @@ -0,0 +1,17 @@ +bindings: + # 100 topics: 10 tenants, 2 namespaces/tenant, 5 topics/namespace + tenant: Mod(100); Div(10); ToString(); Prefix("tnt") + namespace: Mod(10); Div(5); ToString(); Prefix("ns") + topic: Mod(5); ToString(); Prefix("tp") + +params: + async_api: "false" + admin_delop: "false" + +blocks: + admin-topic-block: + ops: + op1: + AdminTopic: "{tenant}/{namespace}/{topic}" + enable_partition: "false" + partition_num: "5" diff --git a/adapter-pulsar/src/main/resources/yaml_examples/msg_recv.yaml b/adapter-pulsar/src/main/resources/yaml_examples/msg_recv.yaml new file mode 100644 index 000000000..364de373c --- /dev/null +++ b/adapter-pulsar/src/main/resources/yaml_examples/msg_recv.yaml @@ -0,0 +1,11 @@ +params: + async_api: "true" + +blocks: + msg-consume-block: + ops: + op1: + MessageConsume: "tnt0/ns0/tp0" + consumerName: "" + subscriptionName: "mynbsub" + subscriptionType: "shared" diff --git a/adapter-pulsar/src/main/resources/yaml_examples/msg_send_avro.yaml b/adapter-pulsar/src/main/resources/yaml_examples/msg_send_avro.yaml new file mode 100644 index 000000000..b0a585ccd --- /dev/null +++ b/adapter-pulsar/src/main/resources/yaml_examples/msg_send_avro.yaml @@ -0,0 +1,29 @@ +bindings: + location: Cities(); + well_id: ToUUID();ToString(); + sensor_id: ToUUID();ToString(); + reading_time: ToDateTime(); + reading_value: ToFloat(100); + +params: + async_api: "true" + +blocks: + msg-produce-block: + ops: + op1: + MessageProduce: "tnt0/ns0/tp1" + producerName: "" + msg_key: | + { + "Location": "{location}", + "WellID": "{well_id}" + } + msg_properties: "" + msg_value: | + { + "SensorID": "{sensor_id}", + "SensorType": "Temperature", + "ReadingTime": "{reading_time}", + "ReadingValue": {reading_value} + } diff --git a/adapter-pulsar/src/main/resources/yaml_examples/msg_send_kvraw.yaml b/adapter-pulsar/src/main/resources/yaml_examples/msg_send_kvraw.yaml new file mode 100644 index 000000000..0886948cc --- /dev/null +++ b/adapter-pulsar/src/main/resources/yaml_examples/msg_send_kvraw.yaml @@ -0,0 +1,23 @@ +bindings: + # message key, property and value + mykey: NumberNameToString() + int_prop_val: ToString(); Prefix("IntProp_") + text_prop_val: AlphaNumericString(5); Prefix("TextProp_") + myvalue: AlphaNumericString(20) + +# document level parameters that apply to all Pulsar client types: +params: + async_api: "true" + +blocks: + msg-produce-block: + ops: + op1: + MessageProduce: "tnt0/ns0/tp0" + msg_key: "{mykey}" + msg_prop: | + { + "prop1": "{int_prop_val}", + "prop2": "{text_prop_val}" + } + msg_value: "{myvalue}" diff --git a/adapters-api/src/main/java/io/nosqlbench/engine/api/activityimpl/BaseOpDispenser.java b/adapters-api/src/main/java/io/nosqlbench/engine/api/activityimpl/BaseOpDispenser.java index 740ef3348..d3a592e76 100644 --- a/adapters-api/src/main/java/io/nosqlbench/engine/api/activityimpl/BaseOpDispenser.java +++ b/adapters-api/src/main/java/io/nosqlbench/engine/api/activityimpl/BaseOpDispenser.java @@ -83,12 +83,16 @@ public abstract class BaseOpDispenser implements OpDispenser @Override public abstract T apply(long cycle); + protected String getDefaultMetricsPrefix(ParsedOp pop) { + return pop.getStaticConfigOr("alias", "UNKNOWN") + "-" + pop.getName() + "--"; + } + private void configureInstrumentation(ParsedOp pop) { this.instrument = pop.takeStaticConfigOr("instrument", false); if (instrument) { - this.successTimer = ActivityMetrics.timer(pop.getStaticConfigOr("alias", "UNKNOWN") + "-" + pop.getName() + "--success"); - this.errorTimer = ActivityMetrics.timer(pop.getStaticConfigOr("alias", "UNKNOWN") + "-" + pop.getName() + "--error"); - this.resultSizeHistogram = ActivityMetrics.histogram(pop.getStaticConfigOr("alias", "UNKNOWN") + "-" + pop.getName() + "--resultset-size"); + this.successTimer = ActivityMetrics.timer(getDefaultMetricsPrefix(pop) + "success"); + this.errorTimer = ActivityMetrics.timer(getDefaultMetricsPrefix(pop) + "error"); + this.resultSizeHistogram = ActivityMetrics.histogram(getDefaultMetricsPrefix(pop) + "resultset-size"); } } diff --git a/adapters-api/src/main/java/io/nosqlbench/engine/api/activityimpl/uniform/BaseDriverAdapter.java b/adapters-api/src/main/java/io/nosqlbench/engine/api/activityimpl/uniform/BaseDriverAdapter.java index 1eba69f2a..3e4ffef9d 100644 --- a/adapters-api/src/main/java/io/nosqlbench/engine/api/activityimpl/uniform/BaseDriverAdapter.java +++ b/adapters-api/src/main/java/io/nosqlbench/engine/api/activityimpl/uniform/BaseDriverAdapter.java @@ -20,6 +20,8 @@ import io.nosqlbench.api.config.standard.*; import io.nosqlbench.engine.api.activityimpl.uniform.fieldmappers.FieldDestructuringMapper; import io.nosqlbench.engine.api.activityimpl.uniform.flowtypes.Op; import io.nosqlbench.engine.api.templating.ParsedOp; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import java.util.ArrayList; import java.util.List; @@ -29,7 +31,8 @@ import java.util.function.Function; import java.util.function.LongFunction; import java.util.stream.Collectors; -public abstract class BaseDriverAdapter implements DriverAdapter, NBConfigurable, NBReconfigurable { +public abstract class BaseDriverAdapter implements DriverAdapter, NBConfigurable, NBReconfigurable { + private final static Logger logger = LogManager.getLogger("ADAPTER"); private DriverSpaceCache spaceCache; private NBConfiguration cfg; @@ -43,22 +46,22 @@ public abstract class BaseDriverAdapter implements DriverAdapter */ @Override public final Function, Map> getPreprocessor() { - List,Map>> mappers = new ArrayList<>(); - List,Map>> stmtRemappers = + List, Map>> mappers = new ArrayList<>(); + List, Map>> stmtRemappers = getOpStmtRemappers().stream() - .map(m -> new FieldDestructuringMapper("stmt",m)) + .map(m -> new FieldDestructuringMapper("stmt", m)) .collect(Collectors.toList()); mappers.addAll(stmtRemappers); mappers.addAll(getOpFieldRemappers()); - if (mappers.size()==0) { + if (mappers.size() == 0) { return (i) -> i; } - Function,Map> remapper = null; + Function, Map> remapper = null; for (int i = 0; i < mappers.size(); i++) { - if (i==0) { - remapper=mappers.get(i); + if (i == 0) { + remapper = mappers.get(i); } else { remapper = remapper.andThen(mappers.get(i)); } @@ -102,7 +105,7 @@ public abstract class BaseDriverAdapter implements DriverAdapter * * @return A list of optionally applied remapping functions. */ - public List>>> getOpStmtRemappers() { + public List>>> getOpStmtRemappers() { return List.of(); } @@ -112,14 +115,14 @@ public abstract class BaseDriverAdapter implements DriverAdapter * @return */ @Override - public List,Map>> getOpFieldRemappers() { + public List, Map>> getOpFieldRemappers() { return List.of(); } @Override public synchronized final DriverSpaceCache getSpaceCache() { - if (spaceCache==null) { - spaceCache=new DriverSpaceCache<>(getSpaceInitializer(getConfiguration())); + if (spaceCache == null) { + spaceCache = new DriverSpaceCache<>(getSpaceInitializer(getConfiguration())); } return spaceCache; } @@ -149,7 +152,7 @@ public abstract class BaseDriverAdapter implements DriverAdapter public NBConfigModel getConfigModel() { return ConfigModel.of(BaseDriverAdapter.class) .add(Param.optional("alias")) - .add(Param.defaultTo("strict",true,"strict op field mode, which requires that provided op fields are recognized and used")) + .add(Param.defaultTo("strict", true, "strict op field mode, which requires that provided op fields are recognized and used")) .add(Param.optional(List.of("op", "stmt", "statement"), String.class, "op template in statement form")) .add(Param.optional("tags", String.class, "tags to be used to filter operations")) .add(Param.defaultTo("errors", "stop", "error handler configuration")) @@ -162,7 +165,7 @@ public abstract class BaseDriverAdapter implements DriverAdapter .add(Param.optional("seq", String.class, "sequencing algorithm")) .add(Param.optional("instrument", Boolean.class)) .add(Param.optional(List.of("workload", "yaml"), String.class, "location of workload yaml file")) - .add(Param.optional("driver",String.class)) + .add(Param.optional("driver", String.class)) .asReadOnly(); } diff --git a/adapters-api/src/main/java/io/nosqlbench/engine/api/activityimpl/uniform/DriverAdapter.java b/adapters-api/src/main/java/io/nosqlbench/engine/api/activityimpl/uniform/DriverAdapter.java index 6f42f8cf4..14bfae422 100644 --- a/adapters-api/src/main/java/io/nosqlbench/engine/api/activityimpl/uniform/DriverAdapter.java +++ b/adapters-api/src/main/java/io/nosqlbench/engine/api/activityimpl/uniform/DriverAdapter.java @@ -148,7 +148,15 @@ public interface DriverAdapter { DriverSpaceCache getSpaceCache(); /** - * @return A function which can initialize a new S + * This method allows each driver adapter to create named state which is automatically + * cached and re-used by name. For each (driver,space) combination in an activity, + * a distinct space instance will be created. In general, adapter developers will + * use the space type associated with an adapter to wrap native driver instances + * one-to-one. As such, if the space implementation is a {@link AutoCloseable}, + * it will be explicitly shutdown as part of the activity shutdown. + * + * @return A function which can initialize a new Space, which is a place to hold + * object state related to retained objects for the lifetime of a native driver. */ default Function getSpaceInitializer(NBConfiguration cfg) { return n -> null; diff --git a/adapters-api/src/main/java/io/nosqlbench/engine/api/activityimpl/uniform/DriverSpaceCache.java b/adapters-api/src/main/java/io/nosqlbench/engine/api/activityimpl/uniform/DriverSpaceCache.java index 2ecf5bd48..f8bc6e4f4 100644 --- a/adapters-api/src/main/java/io/nosqlbench/engine/api/activityimpl/uniform/DriverSpaceCache.java +++ b/adapters-api/src/main/java/io/nosqlbench/engine/api/activityimpl/uniform/DriverSpaceCache.java @@ -16,6 +16,8 @@ package io.nosqlbench.engine.api.activityimpl.uniform; +import java.util.Collections; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; @@ -64,4 +66,8 @@ public class DriverSpaceCache { return cache.computeIfAbsent(name, newSpaceFunction); } + public Map getElements() { + return Collections.unmodifiableMap(cache); + } + } diff --git a/adapters-api/src/main/resources/workload_definition/README.md b/adapters-api/src/main/resources/workload_definition/README.md index 7bcd0ba9c..c0464384e 100644 --- a/adapters-api/src/main/resources/workload_definition/README.md +++ b/adapters-api/src/main/resources/workload_definition/README.md @@ -63,17 +63,21 @@ The process of loading a workload definition occurs in several discrete steps du session: 1. The workload file is loaded. -2. Template variables are interposed. +2. Template variables from the activity parameters are interposed into the raw contents of the + file. 3. The file is deserialized from its native form into a raw data structure. 4. The raw data structure is transformed into a normalized data structure according to the Op Template normalization rules. -5. The data is provided to the ParsedOp API for use by the developer. -6. The DriverAdapter is loaded which understands the op fields provided in the op template. -7. The DriverAdapter uses its documented rules to determine which types of native driver operations +5. Each op template is then denormalized as a self-contained data + structure, containing all the provided bindings, params, and tags from the upper layers of the + doc structure. +6. The data is provided to the ParsedOp API for use by the developer. +7. The DriverAdapter is loaded which understands the op fields provided in the op template. +8. The DriverAdapter uses its documented rules to determine which types of native driver operations each op template is intended to represent. This is called **Op Mapping**. -8. The DriverAdapter uses the identified types to create dispensers of native driver operations. - This is called **Op Dispensing**. -9. The op dispensers are arranged into an indexed bank of op sources according to the specified +9. The DriverAdapter (via the selected Op Mapper) uses the identified types to create dispensers of + native driver operations. This is called **Op Dispensing**. +10. The op dispensers are arranged into an indexed bank of op sources according to the specified ratios and or sequencing strategy. From this point on, NoSQLBench has the ability to construct an operation for any given cycle at high speed. diff --git a/docsys/pom.xml b/docsys/pom.xml index 17538bfee..5ea4dcbf4 100644 --- a/docsys/pom.xml +++ b/docsys/pom.xml @@ -50,7 +50,7 @@ org.eclipse.jetty jetty-server - 11.0.11 + 11.0.12 org.eclipse.jetty diff --git a/driver-jms/src/main/java/io/nosqlbench/driver/jms/JmsAction.java b/driver-jms/src/main/java/io/nosqlbench/driver/jms/JmsAction.java deleted file mode 100644 index 3a3ef9b2a..000000000 --- a/driver-jms/src/main/java/io/nosqlbench/driver/jms/JmsAction.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * 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.driver.jms; - -import com.codahale.metrics.Timer; -import io.nosqlbench.driver.jms.ops.JmsOp; -import io.nosqlbench.engine.api.activityapi.core.SyncAction; -import io.nosqlbench.engine.api.activityapi.errorhandling.modular.ErrorDetail; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.util.function.LongFunction; - -public class JmsAction implements SyncAction { - - private final static Logger logger = LogManager.getLogger(JmsAction.class); - - private final JmsActivity activity; - private final int slot; - - int maxTries; - - public JmsAction(JmsActivity activity, int slot) { - this.activity = activity; - this.slot = slot; - this.maxTries = activity.getActivityDef().getParams().getOptionalInteger("maxtries").orElse(10); - } - - @Override - public void init() { } - - @Override - public int runCycle(long cycle) { - // let's fail the action if some async operation failed - activity.failOnAsyncOperationFailure(); - - long start = System.nanoTime(); - - JmsOp jmsOp; - try (Timer.Context ctx = activity.getBindTimer().time()) { - LongFunction readyJmsOp = activity.getSequencer().apply(cycle); - jmsOp = readyJmsOp.apply(cycle); - } catch (Exception bindException) { - // if diagnostic mode ... - activity.getErrorhandler().handleError(bindException, cycle, 0); - throw new RuntimeException( - "while binding request in cycle " + cycle + ": " + bindException.getMessage(), bindException - ); - } - - for (int i = 0; i < maxTries; i++) { - Timer.Context ctx = activity.getExecuteTimer().time(); - try { - // it is up to the jmsOp to call Context#close when the activity is executed - // this allows us to track time for async operations - jmsOp.run(ctx::close); - break; - } catch (RuntimeException err) { - ErrorDetail errorDetail = activity - .getErrorhandler() - .handleError(err, cycle, System.nanoTime() - start); - if (!errorDetail.isRetryable()) { - break; - } - } - } - - return 0; - } -} diff --git a/driver-jms/src/main/java/io/nosqlbench/driver/jms/JmsActivity.java b/driver-jms/src/main/java/io/nosqlbench/driver/jms/JmsActivity.java deleted file mode 100644 index 2da3a440c..000000000 --- a/driver-jms/src/main/java/io/nosqlbench/driver/jms/JmsActivity.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * 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.driver.jms; - -import com.codahale.metrics.Counter; -import com.codahale.metrics.Histogram; -import com.codahale.metrics.Timer; -import com.datastax.oss.pulsar.jms.PulsarConnectionFactory; -import io.nosqlbench.driver.jms.conn.JmsConnInfo; -import io.nosqlbench.driver.jms.conn.JmsPulsarConnInfo; -import io.nosqlbench.driver.jms.ops.JmsOp; -import io.nosqlbench.driver.jms.util.JmsUtil; -import io.nosqlbench.driver.jms.util.PulsarConfig; -import io.nosqlbench.engine.api.activityapi.errorhandling.modular.NBErrorHandler; -import io.nosqlbench.engine.api.activityapi.planning.OpSequence; -import io.nosqlbench.api.engine.activityimpl.ActivityDef; -import io.nosqlbench.engine.api.activityimpl.OpDispenser; -import io.nosqlbench.engine.api.activityimpl.SimpleActivity; -import io.nosqlbench.api.engine.metrics.ActivityMetrics; -import org.apache.commons.lang3.StringUtils; - -import javax.jms.Destination; -import javax.jms.JMSContext; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; - - -public class JmsActivity extends SimpleActivity { - - private final ConcurrentHashMap jmsDestinations = new ConcurrentHashMap<>(); - - private String jmsProviderType; - private JmsConnInfo jmsConnInfo; - - private JMSContext jmsContext; - - private OpSequence> sequence; - private volatile Throwable asyncOperationFailure; - private NBErrorHandler errorhandler; - - private Timer bindTimer; - private Timer executeTimer; - private Counter bytesCounter; - private Histogram messagesizeHistogram; - - public JmsActivity(ActivityDef activityDef) { - super(activityDef); - } - - @Override - public void initActivity() { - super.initActivity(); - - // default JMS type: Pulsar - // - currently this is the only supported JMS provider - jmsProviderType = - activityDef.getParams() - .getOptionalString(JmsUtil.JMS_PROVIDER_TYPE_KEY_STR) - .orElse(JmsUtil.JMS_PROVIDER_TYPES.PULSAR.label); - - // "Pulsar" as the JMS provider - if (StringUtils.equalsIgnoreCase(jmsProviderType, JmsUtil.JMS_PROVIDER_TYPES.PULSAR.label )) { - - String webSvcUrl = - activityDef.getParams() - .getOptionalString(JmsUtil.JMS_PULSAR_PROVIDER_WEB_URL_KEY_STR) - .orElse("http://localhost:8080"); - String pulsarSvcUrl = - activityDef.getParams() - .getOptionalString(JmsUtil.JMS_PULSAR_PROVIDER_SVC_URL_KEY_STR) - .orElse("pulsar://localhost:6650"); - - if (StringUtils.isAnyBlank(webSvcUrl, pulsarSvcUrl)) { - throw new RuntimeException("For \"" + JmsUtil.JMS_PROVIDER_TYPES.PULSAR.label + "\" type, " + - "\"" + JmsUtil.JMS_PULSAR_PROVIDER_WEB_URL_KEY_STR + "\" and " + - "\"" + JmsUtil.JMS_PULSAR_PROVIDER_SVC_URL_KEY_STR + "\" parameters are manadatory!"); - } - - // Check if extra Pulsar config. file is in place - // - default file: "pulsar_config.properties" under the current directory - String pulsarCfgFile = - activityDef.getParams() - .getOptionalString(JmsUtil.JMS_PULSAR_PROVIDER_CFG_FILE_KEY_STR) - .orElse(JmsUtil.JMS_PULSAR_PROVIDER_DFT_CFG_FILE_NAME); - - PulsarConfig pulsarConfig = new PulsarConfig(pulsarCfgFile); - - jmsConnInfo = new JmsPulsarConnInfo(jmsProviderType, webSvcUrl, pulsarSvcUrl, pulsarConfig); - } - else { - throw new RuntimeException("Unsupported JMS driver type : " + jmsProviderType); - } - - PulsarConnectionFactory factory; - factory = new PulsarConnectionFactory(jmsConnInfo.getJmsConnConfig()); - this.jmsContext = factory.createContext(); - - bindTimer = ActivityMetrics.timer(activityDef, "bind", this.getHdrDigits()); - executeTimer = ActivityMetrics.timer(activityDef, "execute", this.getHdrDigits()); - bytesCounter = ActivityMetrics.counter(activityDef, "bytes"); - messagesizeHistogram = ActivityMetrics.histogram(activityDef, "messagesize", this.getHdrDigits()); - - if (StringUtils.equalsIgnoreCase(jmsProviderType, JmsUtil.JMS_PROVIDER_TYPES.PULSAR.label )) { - this.sequence = createOpSequence((ot) -> new ReadyPulsarJmsOp(ot, this), false, Optional.empty()); - } - - setDefaultsFromOpSequence(sequence); - onActivityDefUpdate(activityDef); - - this.errorhandler = new NBErrorHandler( - () -> activityDef.getParams().getOptionalString("errors").orElse("stop"), - this::getExceptionMetrics - ); - } - - private static String buildCacheKey(String... keyParts) { - return String.join("::", keyParts); - } - - /** - * If the JMS destination that corresponds to a topic exists, reuse it; Otherwise, create it - */ - public Destination getOrCreateJmsDestination(String jmsDestinationType, String destName) { - String destinationCacheKey = buildCacheKey(jmsDestinationType, destName); - Destination destination = jmsDestinations.get(destinationCacheKey); - - if ( destination == null ) { - // TODO: should we match Persistent/Non-peristent JMS Delivery mode with - // Pulsar Persistent/Non-prsistent topic? - if (StringUtils.equalsIgnoreCase(jmsDestinationType, JmsUtil.JMS_DESTINATION_TYPES.QUEUE.label)) { - destination = jmsContext.createQueue(destName); - } else if (StringUtils.equalsIgnoreCase(jmsDestinationType, JmsUtil.JMS_DESTINATION_TYPES.TOPIC.label)) { - destination = jmsContext.createTopic(destName); - } - - jmsDestinations.put(destinationCacheKey, destination); - } - - return destination; - } - - @Override - public synchronized void onActivityDefUpdate(ActivityDef activityDef) { super.onActivityDefUpdate(activityDef); } - public OpSequence> getSequencer() { return sequence; } - - public String getJmsProviderType() { return jmsProviderType; } - public JmsConnInfo getJmsConnInfo() { return jmsConnInfo; } - public JMSContext getJmsContext() { return jmsContext; } - - public Timer getBindTimer() { return bindTimer; } - public Timer getExecuteTimer() { return this.executeTimer; } - public Counter getBytesCounter() { return bytesCounter; } - public Histogram getMessagesizeHistogram() { return messagesizeHistogram; } - - public NBErrorHandler getErrorhandler() { return errorhandler; } - - public void failOnAsyncOperationFailure() { - if (asyncOperationFailure != null) { - throw new RuntimeException(asyncOperationFailure); - } - } - public void asyncOperationFailed(Throwable ex) { - this.asyncOperationFailure = ex; - } -} diff --git a/driver-jms/src/main/java/io/nosqlbench/driver/jms/JmsActivityType.java b/driver-jms/src/main/java/io/nosqlbench/driver/jms/JmsActivityType.java deleted file mode 100644 index b964516ea..000000000 --- a/driver-jms/src/main/java/io/nosqlbench/driver/jms/JmsActivityType.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.driver.jms; - -import io.nosqlbench.engine.api.activityapi.core.Action; -import io.nosqlbench.engine.api.activityapi.core.ActionDispenser; -import io.nosqlbench.engine.api.activityapi.core.ActivityType; -import io.nosqlbench.api.engine.activityimpl.ActivityDef; -import io.nosqlbench.nb.annotations.Service; - -@Service(value = ActivityType.class, selector = "jms") -public class JmsActivityType implements ActivityType { - @Override - public ActionDispenser getActionDispenser(JmsActivity activity) { - return new PulsarJmsActionDispenser(activity); - } - - @Override - public JmsActivity getActivity(ActivityDef activityDef) { - return new JmsActivity(activityDef); - } - - private static class PulsarJmsActionDispenser implements ActionDispenser { - private final JmsActivity activity; - public PulsarJmsActionDispenser(JmsActivity activity) { - this.activity = activity; - } - - @Override - public Action getAction(int slot) { - return new JmsAction(activity, slot); - } - } -} diff --git a/driver-jms/src/main/java/io/nosqlbench/driver/jms/ReadyJmsOp.java b/driver-jms/src/main/java/io/nosqlbench/driver/jms/ReadyJmsOp.java deleted file mode 100644 index bd532033f..000000000 --- a/driver-jms/src/main/java/io/nosqlbench/driver/jms/ReadyJmsOp.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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.driver.jms; - -import io.nosqlbench.driver.jms.ops.JmsOp; -import io.nosqlbench.driver.jms.util.JmsUtil; -import io.nosqlbench.engine.api.activityconfig.yaml.OpTemplate; -import io.nosqlbench.engine.api.activityimpl.BaseOpDispenser; -import io.nosqlbench.engine.api.templating.CommandTemplate; -import org.apache.commons.lang3.BooleanUtils; - -import java.util.function.LongFunction; - -abstract public class ReadyJmsOp extends BaseOpDispenser { - - protected final OpTemplate optpl; - protected final CommandTemplate cmdTpl; - protected final JmsActivity jmsActivity; - - protected final String stmtOpType; - protected LongFunction asyncApiFunc; - protected LongFunction jmsDestinationTypeFunc; - - protected final LongFunction opFunc; - - public ReadyJmsOp(OpTemplate opTemplate, JmsActivity jmsActivity) { - super(opTemplate); - this.optpl = opTemplate; - this.cmdTpl = new CommandTemplate(optpl); - this.jmsActivity = jmsActivity; - - if (!cmdTpl.containsKey("optype") || !cmdTpl.isStatic("optype")) { - throw new RuntimeException("Statement parameter \"optype\" must be static and have a valid value!"); - } - this.stmtOpType = cmdTpl.getStatic("optype"); - - // Global/Doc-level parameter: async_api - if (cmdTpl.containsKey(JmsUtil.ASYNC_API_KEY_STR)) { - if (cmdTpl.isStatic(JmsUtil.ASYNC_API_KEY_STR)) { - boolean value = BooleanUtils.toBoolean(cmdTpl.getStatic(JmsUtil.ASYNC_API_KEY_STR)); - this.asyncApiFunc = (l) -> value; - } else { - throw new RuntimeException("\"" + JmsUtil.ASYNC_API_KEY_STR + "\" parameter cannot be dynamic!"); - } - } - - // Global/Doc-level parameter: jms_desitation_type - // - queue: point-to-point - // - topic: pub/sub - if (cmdTpl.containsKey(JmsUtil.JMS_DESTINATION_TYPE_KEY_STR)) { - if (cmdTpl.isStatic(JmsUtil.JMS_DESTINATION_TYPE_KEY_STR)) { - jmsDestinationTypeFunc = (l) -> cmdTpl.getStatic(JmsUtil.JMS_DESTINATION_TYPE_KEY_STR); - } else { - throw new RuntimeException("\"" + JmsUtil.JMS_DESTINATION_TYPE_KEY_STR + "\" parameter cannot be dynamic!"); - } - } - - this.opFunc = resolveJms(); - } - - public JmsOp apply(long value) { return opFunc.apply(value); } - - abstract LongFunction resolveJms(); -} diff --git a/driver-jms/src/main/java/io/nosqlbench/driver/jms/ReadyPulsarJmsOp.java b/driver-jms/src/main/java/io/nosqlbench/driver/jms/ReadyPulsarJmsOp.java deleted file mode 100644 index 72c397a97..000000000 --- a/driver-jms/src/main/java/io/nosqlbench/driver/jms/ReadyPulsarJmsOp.java +++ /dev/null @@ -1,264 +0,0 @@ -/* - * 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.driver.jms; - -import io.nosqlbench.driver.jms.ops.JmsMsgReadMapper; -import io.nosqlbench.driver.jms.ops.JmsMsgSendMapper; -import io.nosqlbench.driver.jms.ops.JmsOp; -import io.nosqlbench.driver.jms.util.JmsHeaderLongFunc; -import io.nosqlbench.driver.jms.util.JmsUtil; -import io.nosqlbench.engine.api.activityconfig.yaml.OpTemplate; -import org.apache.commons.lang3.BooleanUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.math.NumberUtils; - -import javax.jms.DeliveryMode; -import javax.jms.Destination; -import javax.jms.JMSRuntimeException; -import javax.jms.Message; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.function.LongFunction; -import java.util.stream.Collectors; - -public class ReadyPulsarJmsOp extends ReadyJmsOp { - - public ReadyPulsarJmsOp(OpTemplate opTemplate, JmsActivity jmsActivity) { - super(opTemplate, jmsActivity); - } - - public LongFunction resolveJms() { - // Global/Doc-level parameter: topic_uri - LongFunction topicUriFunc = (l) -> null; - if (cmdTpl.containsKey(JmsUtil.PULSAR_JMS_TOPIC_URI_KEY_STR)) { - if (cmdTpl.isStatic(JmsUtil.PULSAR_JMS_TOPIC_URI_KEY_STR)) { - topicUriFunc = (l) -> cmdTpl.getStatic(JmsUtil.PULSAR_JMS_TOPIC_URI_KEY_STR); - } else { - topicUriFunc = (l) -> cmdTpl.getDynamic(JmsUtil.PULSAR_JMS_TOPIC_URI_KEY_STR, l); - } - } - - // Global: JMS destination - LongFunction jmsDestinationFunc; - try { - LongFunction finalTopicUriFunc = topicUriFunc; - jmsDestinationFunc = (l) -> jmsActivity.getOrCreateJmsDestination( - jmsDestinationTypeFunc.apply(l), - finalTopicUriFunc.apply(l)); - } - catch (JMSRuntimeException ex) { - throw new RuntimeException("Unable to create JMS destination!"); - } - - if (StringUtils.equalsIgnoreCase(stmtOpType, JmsUtil.OP_TYPES.MSG_SEND.label)) { - return resolveMsgSend(asyncApiFunc, jmsDestinationFunc); - } else if (StringUtils.equalsIgnoreCase(stmtOpType, JmsUtil.OP_TYPES.MSG_READ.label)) { - return resolveMsgRead(asyncApiFunc, jmsDestinationFunc); - } else { - throw new RuntimeException("Unsupported JMS operation type"); - } - } - - private LongFunction resolveMsgSend( - LongFunction async_api_func, - LongFunction jmsDestinationFunc - ) { - JmsHeaderLongFunc jmsHeaderLongFunc = new JmsHeaderLongFunc(); - - // JMS header: delivery mode - LongFunction msgDeliveryModeFunc = (l) -> DeliveryMode.PERSISTENT; - if (cmdTpl.containsKey(JmsUtil.JMS_MSG_HEADER_KEYS.DELIVERY_MODE.label)) { - if (cmdTpl.isStatic(JmsUtil.JMS_MSG_HEADER_KEYS.DELIVERY_MODE.label)) { - msgDeliveryModeFunc = (l) -> NumberUtils.toInt(cmdTpl.getStatic(JmsUtil.JMS_MSG_HEADER_KEYS.DELIVERY_MODE.label)); - } - else { - msgDeliveryModeFunc = (l) -> NumberUtils.toInt(cmdTpl.getDynamic(JmsUtil.JMS_MSG_HEADER_KEYS.DELIVERY_MODE.label, l)); - } - } - jmsHeaderLongFunc.setDeliveryModeFunc(msgDeliveryModeFunc); - - // JMS header: message priority - LongFunction msgPriorityFunc = (l) -> Message.DEFAULT_PRIORITY; - if (cmdTpl.containsKey(JmsUtil.JMS_MSG_HEADER_KEYS.PRIORITY.label)) { - if (cmdTpl.isStatic(JmsUtil.JMS_MSG_HEADER_KEYS.PRIORITY.label)) { - msgPriorityFunc = (l) -> NumberUtils.toInt(cmdTpl.getStatic(JmsUtil.JMS_MSG_HEADER_KEYS.PRIORITY.label)); - } - else { - msgPriorityFunc = (l) -> NumberUtils.toInt(cmdTpl.getDynamic(JmsUtil.JMS_MSG_HEADER_KEYS.PRIORITY.label, l)); - } - } - jmsHeaderLongFunc.setMsgPriorityFunc(msgPriorityFunc); - - // JMS header: message TTL - LongFunction msgTtlFunc = (l) -> Message.DEFAULT_TIME_TO_LIVE; - if (cmdTpl.containsKey(JmsUtil.JMS_MSG_HEADER_KEYS.TTL.label)) { - if (cmdTpl.isStatic(JmsUtil.JMS_MSG_HEADER_KEYS.TTL.label)) { - msgTtlFunc = (l) -> NumberUtils.toLong(cmdTpl.getStatic(JmsUtil.JMS_MSG_HEADER_KEYS.TTL.label)); - } - else { - msgTtlFunc = (l) -> NumberUtils.toLong(cmdTpl.getDynamic(JmsUtil.JMS_MSG_HEADER_KEYS.TTL.label, l)); - } - } - jmsHeaderLongFunc.setMsgTtlFunc(msgTtlFunc); - - // JMS header: message delivery delay - LongFunction msgDeliveryDelayFunc = (l) -> Message.DEFAULT_DELIVERY_DELAY; - if (cmdTpl.containsKey(JmsUtil.JMS_MSG_HEADER_KEYS.DELIVERY_DELAY.label)) { - if (cmdTpl.isStatic(JmsUtil.JMS_MSG_HEADER_KEYS.DELIVERY_DELAY.label)) { - msgDeliveryDelayFunc = (l) -> NumberUtils.toLong(cmdTpl.getStatic(JmsUtil.JMS_MSG_HEADER_KEYS.DELIVERY_DELAY.label)); - } - else { - msgDeliveryDelayFunc = (l) -> NumberUtils.toLong(cmdTpl.getDynamic(JmsUtil.JMS_MSG_HEADER_KEYS.DELIVERY_DELAY.label, l)); - } - } - jmsHeaderLongFunc.setMsgDeliveryDelayFunc(msgDeliveryDelayFunc); - - // JMS header: disable message timestamp - LongFunction disableMsgTimestampFunc = (l) -> false; - if (cmdTpl.containsKey(JmsUtil.JMS_MSG_HEADER_KEYS.DISABLE_TIMESTAMP.label)) { - if (cmdTpl.isStatic(JmsUtil.JMS_MSG_HEADER_KEYS.DISABLE_TIMESTAMP.label)) { - disableMsgTimestampFunc = (l) -> BooleanUtils.toBoolean(cmdTpl.getStatic(JmsUtil.JMS_MSG_HEADER_KEYS.DISABLE_TIMESTAMP.label)); - } - else { - disableMsgTimestampFunc = (l) -> BooleanUtils.toBoolean(cmdTpl.getDynamic(JmsUtil.JMS_MSG_HEADER_KEYS.DISABLE_TIMESTAMP.label, l)); - } - } - jmsHeaderLongFunc.setDisableMsgTimestampFunc(disableMsgTimestampFunc); - - // JMS header: disable message ID - LongFunction disableMsgIdFunc = (l) -> false; - if (cmdTpl.containsKey(JmsUtil.JMS_MSG_HEADER_KEYS.DISABLE_ID.label)) { - if (cmdTpl.isStatic(JmsUtil.JMS_MSG_HEADER_KEYS.DISABLE_ID.label)) { - disableMsgIdFunc = (l) -> BooleanUtils.toBoolean(cmdTpl.getStatic(JmsUtil.JMS_MSG_HEADER_KEYS.DISABLE_ID.label)); - } - else { - disableMsgIdFunc = (l) -> BooleanUtils.toBoolean(cmdTpl.getDynamic(JmsUtil.JMS_MSG_HEADER_KEYS.DISABLE_ID.label, l)); - } - } - jmsHeaderLongFunc.setDisableMsgIdFunc(disableMsgIdFunc); - - // JMS message properties - String jmsMsgPropertyListStr = ""; - if (cmdTpl.containsKey(JmsUtil.JMS_PRODUCER_MSG_PROPERTY_KEY_STR)) { - if (cmdTpl.isStatic(JmsUtil.JMS_PRODUCER_MSG_PROPERTY_KEY_STR)) { - jmsMsgPropertyListStr = cmdTpl.getStatic(JmsUtil.JMS_PRODUCER_MSG_PROPERTY_KEY_STR); - } else { - throw new RuntimeException("\"" + JmsUtil.JMS_PRODUCER_MSG_PROPERTY_KEY_STR + "\" parameter cannot be dynamic!"); - } - } - - Map jmsMsgProperties = new HashMap<>(); - if ( !StringUtils.isEmpty(jmsMsgPropertyListStr) ) { - jmsMsgProperties = Arrays.stream(jmsMsgPropertyListStr.split(";")) - .map(s -> s.split("=", 2)) - .collect(Collectors.toMap(a -> a[0], a -> a.length > 1 ? a[1] : "")); - } - - LongFunction msgBodyFunc; - if (cmdTpl.containsKey(JmsUtil.JMS_PRODUCER_MSG_BODY_KEY_STR)) { - if (cmdTpl.isStatic(JmsUtil.JMS_PRODUCER_MSG_BODY_KEY_STR)) { - msgBodyFunc = (l) -> cmdTpl.getStatic(JmsUtil.JMS_PRODUCER_MSG_BODY_KEY_STR); - } else if (cmdTpl.isDynamic(JmsUtil.JMS_PRODUCER_MSG_BODY_KEY_STR)) { - msgBodyFunc = (l) -> cmdTpl.getDynamic(JmsUtil.JMS_PRODUCER_MSG_BODY_KEY_STR, l); - } else { - msgBodyFunc = (l) -> null; - } - } else { - throw new RuntimeException("JMS message send:: \"msg_body\" field must be specified!"); - } - - return new JmsMsgSendMapper( - jmsActivity, - async_api_func, - jmsDestinationFunc, - jmsHeaderLongFunc, - jmsMsgProperties, - msgBodyFunc); - } - - private LongFunction resolveMsgRead( - LongFunction async_api_func, - LongFunction jmsDestinationFunc - ) { - // For Pulsar JMS, make "durable" as the default - LongFunction jmsConsumerDurableFunc = (l) -> true; - if (cmdTpl.containsKey(JmsUtil.JMS_CONSUMER_DURABLE_KEY_STR)) { - if (cmdTpl.isStatic(JmsUtil.JMS_CONSUMER_DURABLE_KEY_STR)) { - jmsConsumerDurableFunc = (l) -> BooleanUtils.toBoolean(cmdTpl.getStatic(JmsUtil.JMS_CONSUMER_DURABLE_KEY_STR)); - } else if (cmdTpl.isDynamic(JmsUtil.JMS_CONSUMER_DURABLE_KEY_STR)) { - jmsConsumerDurableFunc = (l) -> BooleanUtils.toBoolean(cmdTpl.getDynamic(JmsUtil.JMS_CONSUMER_DURABLE_KEY_STR, l)); - } - } - - LongFunction jmsConsumerSharedFunc = (l) -> true; - if (cmdTpl.containsKey(JmsUtil.JMS_CONSUMER_SHARED_KEY_STR)) { - if (cmdTpl.isStatic(JmsUtil.JMS_CONSUMER_SHARED_KEY_STR)) { - jmsConsumerSharedFunc = (l) -> BooleanUtils.toBoolean(cmdTpl.getStatic(JmsUtil.JMS_CONSUMER_SHARED_KEY_STR)); - } else if (cmdTpl.isDynamic(JmsUtil.JMS_CONSUMER_SHARED_KEY_STR)) { - jmsConsumerSharedFunc = (l) -> BooleanUtils.toBoolean(cmdTpl.getDynamic(JmsUtil.JMS_CONSUMER_SHARED_KEY_STR, l)); - } - } - - LongFunction jmsMsgSubscriptionFunc = (l) -> ""; - if (cmdTpl.containsKey(JmsUtil.JMS_CONSUMER_MSG_SUBSCRIPTIOn_KEY_STR)) { - if (cmdTpl.isStatic(JmsUtil.JMS_CONSUMER_MSG_SUBSCRIPTIOn_KEY_STR)) { - jmsMsgSubscriptionFunc = (l) -> cmdTpl.getStatic(JmsUtil.JMS_CONSUMER_MSG_SUBSCRIPTIOn_KEY_STR); - } else if (cmdTpl.isDynamic(JmsUtil.JMS_CONSUMER_MSG_SUBSCRIPTIOn_KEY_STR)) { - jmsMsgSubscriptionFunc = (l) -> cmdTpl.getDynamic(JmsUtil.JMS_CONSUMER_MSG_SUBSCRIPTIOn_KEY_STR, l); - } - } - - LongFunction jmsMsgReadSelectorFunc = (l) -> ""; - if (cmdTpl.containsKey(JmsUtil.JMS_CONSUMER_MSG_READ_SELECTOR_KEY_STR)) { - if (cmdTpl.isStatic(JmsUtil.JMS_CONSUMER_MSG_READ_SELECTOR_KEY_STR)) { - jmsMsgReadSelectorFunc = (l) -> cmdTpl.getStatic(JmsUtil.JMS_CONSUMER_MSG_READ_SELECTOR_KEY_STR); - } else if (cmdTpl.isDynamic(JmsUtil.JMS_CONSUMER_MSG_READ_SELECTOR_KEY_STR)) { - jmsMsgReadSelectorFunc = (l) -> cmdTpl.getDynamic(JmsUtil.JMS_CONSUMER_MSG_READ_SELECTOR_KEY_STR, l); - } - } - - LongFunction jmsMsgNoLocalFunc = (l) -> true; - if (cmdTpl.containsKey(JmsUtil.JMS_CONSUMER_MSG_NOLOCAL_KEY_STR)) { - if (cmdTpl.isStatic(JmsUtil.JMS_CONSUMER_MSG_NOLOCAL_KEY_STR)) { - jmsMsgNoLocalFunc = (l) -> BooleanUtils.toBoolean(cmdTpl.getStatic(JmsUtil.JMS_CONSUMER_MSG_NOLOCAL_KEY_STR)); - } else if (cmdTpl.isDynamic(JmsUtil.JMS_CONSUMER_MSG_NOLOCAL_KEY_STR)) { - jmsMsgNoLocalFunc = (l) -> BooleanUtils.toBoolean(cmdTpl.getDynamic(JmsUtil.JMS_CONSUMER_MSG_NOLOCAL_KEY_STR, l)); - } - } - - LongFunction jmsReadTimeoutFunc = (l) -> 0L; - if (cmdTpl.containsKey(JmsUtil.JMS_CONSUMER_READ_TIMEOUT_KEY_STR)) { - if (cmdTpl.isStatic(JmsUtil.JMS_CONSUMER_READ_TIMEOUT_KEY_STR)) { - jmsReadTimeoutFunc = (l) -> NumberUtils.toLong(cmdTpl.getStatic(JmsUtil.JMS_CONSUMER_READ_TIMEOUT_KEY_STR)); - } else if (cmdTpl.isDynamic(JmsUtil.JMS_CONSUMER_READ_TIMEOUT_KEY_STR)) { - jmsReadTimeoutFunc = (l) -> NumberUtils.toLong(cmdTpl.getDynamic(JmsUtil.JMS_CONSUMER_READ_TIMEOUT_KEY_STR, l)); - } - } - - return new JmsMsgReadMapper( - jmsActivity, - async_api_func, - jmsDestinationFunc, - jmsConsumerDurableFunc, - jmsConsumerSharedFunc, - jmsMsgSubscriptionFunc, - jmsMsgReadSelectorFunc, - jmsMsgNoLocalFunc, - jmsReadTimeoutFunc); - } -} diff --git a/driver-jms/src/main/java/io/nosqlbench/driver/jms/conn/JmsConnInfo.java b/driver-jms/src/main/java/io/nosqlbench/driver/jms/conn/JmsConnInfo.java deleted file mode 100644 index 738f83678..000000000 --- a/driver-jms/src/main/java/io/nosqlbench/driver/jms/conn/JmsConnInfo.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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.driver.jms.conn; - -import java.util.HashMap; -import java.util.Map; - -public class JmsConnInfo { - - protected final String jmsProviderType; - protected final Map jmsConnConfig; - - protected JmsConnInfo(String jmsProviderType) { - this.jmsProviderType = jmsProviderType; - this.jmsConnConfig = new HashMap<>(); - } - - public Map getJmsConnConfig() { return this.jmsConnConfig; } - public void resetJmsConnConfig() { this.jmsConnConfig.clear(); } - public void addJmsConnConfigItems(Map cfgItems) { this.jmsConnConfig.putAll(cfgItems); } - public void addJmsConnConfigItem(String key, Object value) { this.jmsConnConfig.put(key, value); } - public void removeJmsConnConfigItem(String key) { this.jmsConnConfig.remove(key); } -} diff --git a/driver-jms/src/main/java/io/nosqlbench/driver/jms/conn/JmsPulsarConnInfo.java b/driver-jms/src/main/java/io/nosqlbench/driver/jms/conn/JmsPulsarConnInfo.java deleted file mode 100644 index f1e09fe78..000000000 --- a/driver-jms/src/main/java/io/nosqlbench/driver/jms/conn/JmsPulsarConnInfo.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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.driver.jms.conn; - -import io.nosqlbench.driver.jms.util.PulsarConfig; - -import java.util.Map; - -public class JmsPulsarConnInfo extends JmsConnInfo { - - private final String webSvcUrl; - private final String pulsarSvcUrl; - private final PulsarConfig extraPulsarConfig; - - public JmsPulsarConnInfo(String jmsProviderType, String webSvcUrl, String pulsarSvcUrl, PulsarConfig pulsarConfig) { - super(jmsProviderType); - - this.webSvcUrl = webSvcUrl; - this.pulsarSvcUrl = pulsarSvcUrl; - this.extraPulsarConfig = pulsarConfig; - - this.addJmsConnConfigItem("webServiceUrl", this.webSvcUrl); - this.addJmsConnConfigItem("brokerServiceUrl", this.pulsarSvcUrl); - - Map clientCfgMap = this.extraPulsarConfig.getClientConfMap(); - if (!clientCfgMap.isEmpty()) { - this.addJmsConnConfigItems(clientCfgMap); - } - - Map producerCfgMap = this.extraPulsarConfig.getProducerConfMap(); - if (!producerCfgMap.isEmpty()) { - this.addJmsConnConfigItem("producerConfig", producerCfgMap); - } - - Map consumerCfgMap = this.extraPulsarConfig.getConsumerConfMap(); - if (!consumerCfgMap.isEmpty()) { - this.addJmsConnConfigItem("consumerConfig", consumerCfgMap); - } - } - - public String getWebSvcUrl() { return this.webSvcUrl; } - public String getPulsarSvcUrl() { return this.pulsarSvcUrl; } - public PulsarConfig getExtraPulsarConfig() { return this.extraPulsarConfig; } -} diff --git a/driver-jms/src/main/java/io/nosqlbench/driver/jms/ops/JmsMsgReadMapper.java b/driver-jms/src/main/java/io/nosqlbench/driver/jms/ops/JmsMsgReadMapper.java deleted file mode 100644 index 0108c0608..000000000 --- a/driver-jms/src/main/java/io/nosqlbench/driver/jms/ops/JmsMsgReadMapper.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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.driver.jms.ops; - -import io.nosqlbench.driver.jms.JmsActivity; - -import javax.jms.Destination; -import java.util.function.LongFunction; - -/** - * This maps a set of specifier functions to a pulsar operation. The pulsar operation contains - * enough state to define a pulsar operation such that it can be executed, measured, and possibly - * retried if needed. - * - * This function doesn't act *as* the operation. It merely maps the construction logic into - * a simple functional type, given the component functions. - * - * For additional parameterization, the command template is also provided. - */ -public class JmsMsgReadMapper extends JmsOpMapper { - - private final LongFunction jmsConsumerDurableFunc; - private final LongFunction jmsConsumerSharedFunc; - private final LongFunction jmsMsgSubscriptionFunc; - private final LongFunction jmsMsgReadSelectorFunc; - private final LongFunction jmsMsgNoLocalFunc; - private final LongFunction jmsReadTimeoutFunc; - - public JmsMsgReadMapper(JmsActivity jmsActivity, - LongFunction asyncApiFunc, - LongFunction jmsDestinationFunc, - LongFunction jmsConsumerDurableFunc, - LongFunction jmsConsumerSharedFunc, - LongFunction jmsMsgSubscriptionFunc, - LongFunction jmsMsgReadSelectorFunc, - LongFunction jmsMsgNoLocalFunc, - LongFunction jmsReadTimeoutFunc) { - super(jmsActivity, asyncApiFunc, jmsDestinationFunc); - - this.jmsConsumerDurableFunc = jmsConsumerDurableFunc; - this.jmsConsumerSharedFunc = jmsConsumerSharedFunc; - this.jmsMsgSubscriptionFunc = jmsMsgSubscriptionFunc; - this.jmsMsgReadSelectorFunc = jmsMsgReadSelectorFunc; - this.jmsMsgNoLocalFunc = jmsMsgNoLocalFunc; - this.jmsReadTimeoutFunc = jmsReadTimeoutFunc; - } - - @Override - public JmsOp apply(long value) { - boolean asyncApi = asyncApiFunc.apply(value); - Destination jmsDestination = jmsDestinationFunc.apply(value); - boolean jmsConsumerDurable = jmsConsumerDurableFunc.apply(value); - boolean jmsConsumerShared = jmsConsumerSharedFunc.apply(value); - String jmsMsgSubscription = jmsMsgSubscriptionFunc.apply(value); - String jmsMsgReadSelector = jmsMsgReadSelectorFunc.apply(value); - boolean jmsMsgNoLocal = jmsMsgNoLocalFunc.apply(value); - long jmsReadTimeout = jmsReadTimeoutFunc.apply(value); - - // Default to NO read timeout - if (jmsReadTimeout < 0) jmsReadTimeout = 0; - - return new JmsMsgReadOp( - jmsActivity, - asyncApi, - jmsDestination, - jmsConsumerDurable, - jmsConsumerShared, - jmsMsgSubscription, - jmsMsgReadSelector, - jmsMsgNoLocal, - jmsReadTimeout - ); - } -} diff --git a/driver-jms/src/main/java/io/nosqlbench/driver/jms/ops/JmsMsgReadOp.java b/driver-jms/src/main/java/io/nosqlbench/driver/jms/ops/JmsMsgReadOp.java deleted file mode 100644 index de92f73c5..000000000 --- a/driver-jms/src/main/java/io/nosqlbench/driver/jms/ops/JmsMsgReadOp.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * 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.driver.jms.ops; - -import com.codahale.metrics.Counter; -import com.codahale.metrics.Histogram; -import io.nosqlbench.driver.jms.JmsActivity; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import javax.jms.*; - -public class JmsMsgReadOp extends JmsTimeTrackOp { - - private final static Logger logger = LogManager.getLogger(JmsMsgReadOp.class); - - private final JmsActivity jmsActivity; - private final boolean asyncJmsOp; - private final Destination jmsDestination; - - private final JMSContext jmsContext; - private final JMSConsumer jmsConsumer; - private final boolean jmsConsumerDurable; - private final boolean jmsConsumerShared; - private final String jmsMsgSubscrption; - private final String jmsMsgReadSelector; - private final boolean jmsMsgNoLocal; - private final long jmsReadTimeout; - - private final Counter bytesCounter; - private final Histogram messagesizeHistogram; - - public JmsMsgReadOp(JmsActivity jmsActivity, - boolean asyncJmsOp, - Destination jmsDestination, - boolean jmsConsumerDurable, - boolean jmsConsumerShared, - String jmsMsgSubscrption, - String jmsMsgReadSelector, - boolean jmsMsgNoLocal, - long jmsReadTimeout) { - this.jmsActivity = jmsActivity; - this.asyncJmsOp = asyncJmsOp; - this.jmsDestination = jmsDestination; - this.jmsConsumerDurable = jmsConsumerDurable; - this.jmsConsumerShared = jmsConsumerShared; - this.jmsMsgReadSelector = jmsMsgReadSelector; - this.jmsMsgSubscrption = jmsMsgSubscrption; - this.jmsMsgNoLocal = jmsMsgNoLocal; - this.jmsReadTimeout = jmsReadTimeout; - - this.jmsContext = jmsActivity.getJmsContext(); - this.jmsConsumer = createJmsConsumer(); - - this.bytesCounter = jmsActivity.getBytesCounter(); - this.messagesizeHistogram = jmsActivity.getMessagesizeHistogram(); - } - - private JMSConsumer createJmsConsumer() { - JMSConsumer jmsConsumer; - - try { - if (jmsConsumerDurable) { - if (jmsConsumerShared) - jmsConsumer = jmsContext.createSharedDurableConsumer((Topic) jmsDestination, jmsMsgSubscrption, jmsMsgReadSelector); - else - jmsConsumer = jmsContext.createDurableConsumer((Topic) jmsDestination, jmsMsgSubscrption, jmsMsgReadSelector, jmsMsgNoLocal); - } else { - if (jmsConsumerShared) - jmsConsumer = jmsContext.createSharedConsumer((Topic) jmsDestination, jmsMsgSubscrption, jmsMsgReadSelector); - else - jmsConsumer = jmsContext.createConsumer(jmsDestination, jmsMsgReadSelector, jmsMsgNoLocal); - } - } - catch (InvalidDestinationRuntimeException invalidDestinationRuntimeException) { - throw new RuntimeException("Failed to create JMS consumer: invalid destination!"); - } - catch (InvalidSelectorRuntimeException invalidSelectorRuntimeException) { - throw new RuntimeException("Failed to create JMS consumer: invalid message selector!"); - } - catch (JMSRuntimeException jmsRuntimeException) { - jmsRuntimeException.printStackTrace(); - throw new RuntimeException("Failed to create JMS consumer: runtime internal error!"); - } - - // TODO: async consumer -// if (this.asyncJmsOp) { -// jmsConsumer.setMessageListener(); -// } - - return jmsConsumer; - } - - @Override - public void run() { - // FIXME: jmsReadTimeout being 0 behaves like receiveNoWait() instead of waiting indefinitley - Message receivedMsg = jmsConsumer.receive(jmsReadTimeout); - try { - if (receivedMsg != null) { - receivedMsg.acknowledge(); - byte[] receivedMsgBody = receivedMsg.getBody(byte[].class); - - if (logger.isDebugEnabled()) { - logger.debug("received msg-payload={}", new String(receivedMsgBody)); - } - - int messagesize = receivedMsgBody.length; - bytesCounter.inc(messagesize); - messagesizeHistogram.update(messagesize); - } - } catch (JMSException e) { - e.printStackTrace(); - throw new RuntimeException("Failed to acknowledge the received JMS message."); - } - } -} diff --git a/driver-jms/src/main/java/io/nosqlbench/driver/jms/ops/JmsMsgSendMapper.java b/driver-jms/src/main/java/io/nosqlbench/driver/jms/ops/JmsMsgSendMapper.java deleted file mode 100644 index fb649f013..000000000 --- a/driver-jms/src/main/java/io/nosqlbench/driver/jms/ops/JmsMsgSendMapper.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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.driver.jms.ops; - -import io.nosqlbench.driver.jms.JmsActivity; -import io.nosqlbench.driver.jms.util.JmsHeader; -import io.nosqlbench.driver.jms.util.JmsHeaderLongFunc; - -import javax.jms.Destination; -import java.util.Map; -import java.util.function.LongFunction; - -/** - * This maps a set of specifier functions to a pulsar operation. The pulsar operation contains - * enough state to define a pulsar operation such that it can be executed, measured, and possibly - * retried if needed. - * - * This function doesn't act *as* the operation. It merely maps the construction logic into - * a simple functional type, given the component functions. - * - * For additional parameterization, the command template is also provided. - */ -public class JmsMsgSendMapper extends JmsOpMapper { - private final JmsHeaderLongFunc jmsHeaderLongFunc; - private final Map jmsMsgProperties; - private final LongFunction msgBodyFunc; - - public JmsMsgSendMapper(JmsActivity jmsActivity, - LongFunction asyncApiFunc, - LongFunction jmsDestinationFunc, - JmsHeaderLongFunc jmsHeaderLongFunc, - Map jmsMsgProperties, - LongFunction msgBodyFunc) { - super(jmsActivity, asyncApiFunc, jmsDestinationFunc); - - this.jmsHeaderLongFunc = jmsHeaderLongFunc; - this.jmsMsgProperties = jmsMsgProperties; - this.msgBodyFunc = msgBodyFunc; - } - - @Override - public JmsOp apply(long value) { - boolean asyncApi = asyncApiFunc.apply(value); - Destination jmsDestination = jmsDestinationFunc.apply(value); - JmsHeader jmsHeader = (JmsHeader)jmsHeaderLongFunc.apply(value); - String msgBody = msgBodyFunc.apply(value); - - return new JmsMsgSendOp( - jmsActivity, - asyncApi, - jmsDestination, - jmsHeader, - jmsMsgProperties, - msgBody - ); - } -} diff --git a/driver-jms/src/main/java/io/nosqlbench/driver/jms/ops/JmsMsgSendOp.java b/driver-jms/src/main/java/io/nosqlbench/driver/jms/ops/JmsMsgSendOp.java deleted file mode 100644 index 2f432502c..000000000 --- a/driver-jms/src/main/java/io/nosqlbench/driver/jms/ops/JmsMsgSendOp.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * 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.driver.jms.ops; - -import com.codahale.metrics.Counter; -import com.codahale.metrics.Histogram; -import io.nosqlbench.driver.jms.JmsActivity; -import io.nosqlbench.driver.jms.util.JmsHeader; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import javax.jms.*; -import java.nio.charset.StandardCharsets; -import java.util.Iterator; -import java.util.Map; - -public class JmsMsgSendOp extends JmsTimeTrackOp { - - private final static Logger logger = LogManager.getLogger(JmsMsgSendOp.class); - - private final JmsActivity jmsActivity; - private final boolean asyncJmsOp; - private final Destination jmsDestination; - private final JmsHeader jmsHeader; - private final Map jmsMsgProperties; - - private final JMSContext jmsContext; - private final JMSProducer jmsProducer; - private final String msgBody; - - private final Counter bytesCounter; - private final Histogram messagesizeHistogram; - - public JmsMsgSendOp(JmsActivity jmsActivity, - boolean asyncJmsOp, - Destination jmsDestination, - JmsHeader jmsHeader, - Map jmsMsgProperties, - String msgBody) { - this.jmsActivity = jmsActivity; - this.asyncJmsOp = asyncJmsOp; - this.jmsDestination = jmsDestination; - - this.jmsHeader = jmsHeader; - this.jmsMsgProperties = jmsMsgProperties; - this.msgBody = msgBody; - - if (!jmsHeader.isValidHeader()) { - throw new RuntimeException(jmsHeader.getInvalidJmsHeaderMsgText()); - } - - if ((msgBody == null) || msgBody.isEmpty()) { - throw new RuntimeException("JMS message body can't be empty!"); - } - - this.jmsContext = jmsActivity.getJmsContext(); - this.jmsProducer = createJmsProducer(); - - this.bytesCounter = jmsActivity.getBytesCounter(); - this.messagesizeHistogram = jmsActivity.getMessagesizeHistogram(); - } - - private JMSProducer createJmsProducer() { - JMSProducer jmsProducer = this.jmsContext.createProducer(); - - jmsProducer.setDeliveryMode(this.jmsHeader.getDeliveryMode()); - jmsProducer.setPriority(this.jmsHeader.getMsgPriority()); - jmsProducer.setDeliveryDelay(this.jmsHeader.getMsgDeliveryDelay()); - jmsProducer.setDisableMessageTimestamp(this.jmsHeader.isDisableMsgTimestamp()); - jmsProducer.setDisableMessageID(this.jmsHeader.isDisableMsgId()); - - if (this.asyncJmsOp) { - jmsProducer.setAsync(new CompletionListener() { - @Override - public void onCompletion(Message msg) { - try { - byte[] msgBody = msg.getBody(byte[].class); - if (logger.isTraceEnabled()) { - logger.trace("Async message send success - message body: " + new String(msgBody)); - } - } - catch (JMSException jmsException) { - jmsException.printStackTrace(); - logger.warn("Unexpected error when parsing message body: " + jmsException.getMessage()); - } - } - - @Override - public void onException(Message msg, Exception e) { - try { - byte[] msgBody = msg.getBody(byte[].class); - if (logger.isTraceEnabled()) { - logger.trace("Async message send failure - message body: " + new String(msgBody)); - } - } - catch (JMSException jmsException) { - jmsException.printStackTrace(); - logger.warn("Unexpected error when parsing message body: " + jmsException.getMessage()); - } - } - }); - } - - for (Map.Entry entry : jmsMsgProperties.entrySet()) { - jmsProducer.setProperty(entry.getKey(), entry.getValue()); - } - - return jmsProducer; - } - - @Override - public void run() { - try { - byte[] msgBytes = msgBody.getBytes(StandardCharsets.UTF_8); - int messageSize = msgBytes.length; - jmsProducer.send(jmsDestination, msgBytes); - - messagesizeHistogram.update(messageSize); - bytesCounter.inc(messageSize); - } - catch (Exception ex) { - logger.error("Failed to send JMS message - " + msgBody); - } - } -} diff --git a/driver-jms/src/main/java/io/nosqlbench/driver/jms/ops/JmsOp.java b/driver-jms/src/main/java/io/nosqlbench/driver/jms/ops/JmsOp.java deleted file mode 100644 index b8f227ffd..000000000 --- a/driver-jms/src/main/java/io/nosqlbench/driver/jms/ops/JmsOp.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.driver.jms.ops; - -/** - * Base type of all Pulsar Operations including Producers and Consumers. - */ -public interface JmsOp { - - /** - * Execute the operation, invoke the timeTracker when the operation ended. - * The timeTracker can be invoked in a separate thread, it is only used for metrics. - */ - void run(Runnable timeTracker); -} diff --git a/driver-jms/src/main/java/io/nosqlbench/driver/jms/ops/JmsOpMapper.java b/driver-jms/src/main/java/io/nosqlbench/driver/jms/ops/JmsOpMapper.java deleted file mode 100644 index b52385cdd..000000000 --- a/driver-jms/src/main/java/io/nosqlbench/driver/jms/ops/JmsOpMapper.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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.driver.jms.ops; - -import io.nosqlbench.driver.jms.JmsActivity; -import io.nosqlbench.driver.jms.util.JmsHeaderLongFunc; - -import javax.jms.Destination; -import java.util.Map; -import java.util.function.LongFunction; - -public abstract class JmsOpMapper implements LongFunction { - protected final JmsActivity jmsActivity; - protected final LongFunction asyncApiFunc; - protected final LongFunction jmsDestinationFunc; - - public JmsOpMapper(JmsActivity jmsActivity, - LongFunction asyncApiFunc, - LongFunction jmsDestinationFunc) - { - this.jmsActivity = jmsActivity; - this.asyncApiFunc = asyncApiFunc; - this.jmsDestinationFunc = jmsDestinationFunc; - } -} diff --git a/driver-jms/src/main/java/io/nosqlbench/driver/jms/util/JmsHeader.java b/driver-jms/src/main/java/io/nosqlbench/driver/jms/util/JmsHeader.java deleted file mode 100644 index e7d889d8a..000000000 --- a/driver-jms/src/main/java/io/nosqlbench/driver/jms/util/JmsHeader.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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.driver.jms.util; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.Setter; -import lombok.ToString; -import org.apache.commons.lang.StringUtils; - -import javax.jms.DeliveryMode; - -@Setter -@Getter -@AllArgsConstructor -@ToString -public class JmsHeader { - private int deliveryMode; - private int msgPriority; - private long msgTtl; - private long msgDeliveryDelay; - private boolean disableMsgTimestamp; - private boolean disableMsgId; - - public boolean isValidDeliveryMode() { - return (deliveryMode == DeliveryMode.NON_PERSISTENT) || (deliveryMode == DeliveryMode.PERSISTENT); - } - - public boolean isValidPriority() { - return (msgPriority >= 0) && (msgPriority <= 9); - } - - public boolean isValidTtl() { - return msgTtl >= 0; - } - - public boolean isValidDeliveryDelay() { - return msgTtl >= 0; - } - - public boolean isValidHeader() { - return isValidDeliveryMode() - && isValidPriority() - && isValidTtl() - && isValidDeliveryDelay(); - } - - public String getInvalidJmsHeaderMsgText() { - StringBuilder sb = new StringBuilder(); - - if (!isValidDeliveryMode()) - sb.append("delivery mode - " + deliveryMode + "; "); - if (!isValidPriority()) - sb.append("message priority - " + msgPriority + "; "); - if (!isValidTtl()) - sb.append("message TTL - " + msgTtl + "; "); - if (!isValidDeliveryDelay()) - sb.append("message delivery delay - " + msgDeliveryDelay + "; "); - - String invalidMsgText = sb.toString(); - if (StringUtils.length(invalidMsgText) > 0) - invalidMsgText = StringUtils.substringBeforeLast(invalidMsgText, ";"); - else - invalidMsgText = "none"; - - return "Invalid JMS header values: " + invalidMsgText; - } -} diff --git a/driver-jms/src/main/java/io/nosqlbench/driver/jms/util/JmsHeaderLongFunc.java b/driver-jms/src/main/java/io/nosqlbench/driver/jms/util/JmsHeaderLongFunc.java deleted file mode 100644 index 75616091a..000000000 --- a/driver-jms/src/main/java/io/nosqlbench/driver/jms/util/JmsHeaderLongFunc.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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.driver.jms.util; - -import lombok.*; - -import javax.jms.DeliveryMode; -import javax.jms.Message; -import java.util.function.LongFunction; - -@Setter -@Getter -@NoArgsConstructor -public class JmsHeaderLongFunc implements LongFunction { - private LongFunction deliveryModeFunc; - private LongFunction msgPriorityFunc; - private LongFunction msgTtlFunc; - private LongFunction msgDeliveryDelayFunc; - private LongFunction disableMsgTimestampFunc; - private LongFunction disableMsgIdFunc; - - @Override - public Object apply(long value) { - return new JmsHeader( - (deliveryModeFunc != null) ? deliveryModeFunc.apply(value) : DeliveryMode.PERSISTENT, - (msgPriorityFunc != null) ? msgPriorityFunc.apply(value) : Message.DEFAULT_PRIORITY, - (msgTtlFunc != null) ? msgTtlFunc.apply(value) : Message.DEFAULT_TIME_TO_LIVE, - (msgTtlFunc != null) ? msgTtlFunc.apply(value) : Message.DEFAULT_DELIVERY_DELAY, - (disableMsgTimestampFunc != null) ? disableMsgTimestampFunc.apply(value) : false, - (disableMsgIdFunc != null) ? disableMsgIdFunc.apply(value) : false - ); - } -} diff --git a/driver-jms/src/main/java/io/nosqlbench/driver/jms/util/JmsUtil.java b/driver-jms/src/main/java/io/nosqlbench/driver/jms/util/JmsUtil.java deleted file mode 100644 index 278cb189b..000000000 --- a/driver-jms/src/main/java/io/nosqlbench/driver/jms/util/JmsUtil.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * 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.driver.jms.util; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.util.Arrays; - -public class JmsUtil { - - private final static Logger logger = LogManager.getLogger(JmsUtil.class); - - // Supported JMS provider type - public enum JMS_PROVIDER_TYPES { - PULSAR("pulsar"); - - public final String label; - JMS_PROVIDER_TYPES(String label) { - this.label = label; - } - } - public static boolean isValidJmsProviderType(String type) { - return Arrays.stream(JMS_PROVIDER_TYPES.values()).anyMatch(t -> t.label.equals(type)); - } - - ///// - // NB command line parameters - // - JMS provider type - public final static String JMS_PROVIDER_TYPE_KEY_STR = "provider_type"; - - /// Only applicable when the provider is "Pulsar" - // - Pulsar configuration properties file - public final static String JMS_PULSAR_PROVIDER_CFG_FILE_KEY_STR = "pulsar_cfg_file"; - public final static String JMS_PULSAR_PROVIDER_DFT_CFG_FILE_NAME = "pulsar_config.properties"; - // - Pulsar web url - public final static String JMS_PULSAR_PROVIDER_WEB_URL_KEY_STR = "web_url"; - // - Pulsar service url - public final static String JMS_PULSAR_PROVIDER_SVC_URL_KEY_STR = "service_url"; - - - public final static String ASYNC_API_KEY_STR = "async_api"; - public final static String JMS_DESTINATION_TYPE_KEY_STR = "jms_desitation_type"; - - ///// JMS Producer - // Supported JMS provider type - public enum JMS_MSG_HEADER_KEYS { - DELIVERY_MODE("jms_producer_header_msg_delivery_mode"), - PRIORITY("jms_producer_header_msg_priority"), - TTL("jms_producer_header_msg_ttl"), - DELIVERY_DELAY("jms_producer_header_msg_delivery_delay"), - DISABLE_TIMESTAMP("jms_producer_header_disable_msg_timestamp"), - DISABLE_ID("jms_producer_header_disable_msg_id"); - - public final String label; - JMS_MSG_HEADER_KEYS(String label) { - this.label = label; - } - } - public static boolean isValidJmsHeaderKey(String type) { - return Arrays.stream(JMS_MSG_HEADER_KEYS.values()).anyMatch(t -> t.label.equals(type)); - } - public final static String JMS_PRODUCER_MSG_PROPERTY_KEY_STR = "jms_producer_msg_properties"; - public final static String JMS_PRODUCER_MSG_BODY_KEY_STR = "msg_body"; - - ///// JMS Consumer - public final static String JMS_CONSUMER_DURABLE_KEY_STR = "jms_consumer_msg_durable"; - public final static String JMS_CONSUMER_SHARED_KEY_STR = "jms_consumer_msg_shared"; - public final static String JMS_CONSUMER_MSG_SUBSCRIPTIOn_KEY_STR = "jms_consumer_subscription"; - public final static String JMS_CONSUMER_MSG_READ_SELECTOR_KEY_STR = "jms_consumer_msg_read_selector"; - public final static String JMS_CONSUMER_MSG_NOLOCAL_KEY_STR = "jms_consumer_msg_nolocal"; - public final static String JMS_CONSUMER_READ_TIMEOUT_KEY_STR = "jms_consumer_msg_read_timeout"; - - - // Only applicable to Pulsar JMS provider - public final static String PULSAR_JMS_TOPIC_URI_KEY_STR = "pulsar_topic_uri"; - - // Supported message operation types - public enum OP_TYPES { - MSG_SEND("msg_send"), - MSG_READ("msg_read"); - - public final String label; - OP_TYPES(String label) { - this.label = label; - } - } - public static boolean isValidClientType(String type) { - return Arrays.stream(OP_TYPES.values()).anyMatch(t -> t.label.equals(type)); - } - - // JMS Destination Types - public enum JMS_DESTINATION_TYPES { - QUEUE("queue"), - TOPIC("topic"); - - public final String label; - JMS_DESTINATION_TYPES(String label) { - this.label = label; - } - } - public static boolean isValidJmsDestinationType(String type) { - return Arrays.stream(JMS_DESTINATION_TYPES.values()).anyMatch(t -> t.label.equals(type)); - } -} - diff --git a/driver-jms/src/main/java/io/nosqlbench/driver/jms/util/PulsarConfig.java b/driver-jms/src/main/java/io/nosqlbench/driver/jms/util/PulsarConfig.java deleted file mode 100644 index 5a015cddf..000000000 --- a/driver-jms/src/main/java/io/nosqlbench/driver/jms/util/PulsarConfig.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * 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.driver.jms.util; - -import org.apache.commons.configuration2.Configuration; -import org.apache.commons.configuration2.FileBasedConfiguration; -import org.apache.commons.configuration2.PropertiesConfiguration; -import org.apache.commons.configuration2.builder.FileBasedConfigurationBuilder; -import org.apache.commons.configuration2.builder.fluent.Parameters; -import org.apache.commons.configuration2.ex.ConfigurationException; -import org.apache.commons.lang3.StringUtils; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.io.File; -import java.io.IOException; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; - -public class PulsarConfig { - private final static Logger logger = LogManager.getLogger(PulsarConfig.class); - - public static final String SCHEMA_CONF_PREFIX = "schema"; - public static final String CLIENT_CONF_PREFIX = "client"; - public static final String PRODUCER_CONF_PREFIX = "producer"; - public static final String CONSUMER_CONF_PREFIX = "consumer"; - - private final Map schemaConfMap = new HashMap<>(); - private final Map clientConfMap = new HashMap<>(); - private final Map producerConfMap = new HashMap<>(); - private final Map consumerConfMap = new HashMap<>(); - - public PulsarConfig(String fileName) { - File file = new File(fileName); - - try { - String canonicalFilePath = file.getCanonicalPath(); - - Parameters params = new Parameters(); - - FileBasedConfigurationBuilder builder = - new FileBasedConfigurationBuilder(PropertiesConfiguration.class) - .configure(params.properties() - .setFileName(fileName)); - - Configuration config = builder.getConfiguration(); - - // Get schema specific configuration settings - for (Iterator it = config.getKeys(SCHEMA_CONF_PREFIX); it.hasNext(); ) { - String confKey = it.next(); - String confVal = config.getProperty(confKey).toString(); - if (!StringUtils.isBlank(confVal)) - schemaConfMap.put(confKey.substring(SCHEMA_CONF_PREFIX.length() + 1), config.getProperty(confKey)); - } - - // Get client connection specific configuration settings - for (Iterator it = config.getKeys(CLIENT_CONF_PREFIX); it.hasNext(); ) { - String confKey = it.next(); - String confVal = config.getProperty(confKey).toString(); - if (!StringUtils.isBlank(confVal)) - clientConfMap.put(confKey.substring(CLIENT_CONF_PREFIX.length() + 1), config.getProperty(confKey)); - } - - // Get producer specific configuration settings - for (Iterator it = config.getKeys(PRODUCER_CONF_PREFIX); it.hasNext(); ) { - String confKey = it.next(); - String confVal = config.getProperty(confKey).toString(); - if (!StringUtils.isBlank(confVal)) - producerConfMap.put(confKey.substring(PRODUCER_CONF_PREFIX.length() + 1), config.getProperty(confKey)); - } - - // Get consumer specific configuration settings - for (Iterator it = config.getKeys(CONSUMER_CONF_PREFIX); it.hasNext(); ) { - String confKey = it.next(); - String confVal = config.getProperty(confKey).toString(); - if (!StringUtils.isBlank(confVal)) - consumerConfMap.put(confKey.substring(CONSUMER_CONF_PREFIX.length() + 1), config.getProperty(confKey)); - } - } catch (IOException ioe) { - logger.error("Can't read the specified config properties file: " + fileName); - ioe.printStackTrace(); - } catch (ConfigurationException cex) { - logger.error("Error loading configuration items from the specified config properties file: " + fileName + ":" + cex.getMessage()); - cex.printStackTrace(); - } - } - - public Map getSchemaConfMap() { - return this.schemaConfMap; - } - public Map getClientConfMap() { - return this.clientConfMap; - } - public Map getProducerConfMap() { - return this.producerConfMap; - } - public Map getConsumerConfMap() { - return this.consumerConfMap; - } -} diff --git a/driver-jms/src/main/resources/jms.md b/driver-jms/src/main/resources/jms.md deleted file mode 100644 index 07dd0c5c7..000000000 --- a/driver-jms/src/main/resources/jms.md +++ /dev/null @@ -1 +0,0 @@ -# Overview diff --git a/driver-jms/src/main/resources/pulsar_config.properties b/driver-jms/src/main/resources/pulsar_config.properties deleted file mode 100644 index f711535ac..000000000 --- a/driver-jms/src/main/resources/pulsar_config.properties +++ /dev/null @@ -1,33 +0,0 @@ -### Schema related configurations - schema.xxx -# valid types: -# - primitive type (https://pulsar.apache.org/docs/en/schema-understand/#primitive-type) -# - keyvalue (https://pulsar.apache.org/docs/en/schema-understand/#keyvalue) -# - strut (complex type) (https://pulsar.apache.org/docs/en/schema-understand/#struct) -# avro, json, protobuf -# -# NOTE: for JMS client, Pulsar "schema" is NOT supported yet -schema.type= -schema.definition= - - -### Pulsar client related configurations - client.xxx -# http://pulsar.apache.org/docs/en/client-libraries-java/#client -client.connectionTimeoutMs=5000 -#client.authPluginClassName=org.apache.pulsar.client.impl.auth.AuthenticationToken -#client.authParams= -#client.tlsAllowInsecureConnection=true -client.numIoThreads=10 -client.numListenerThreads=10 - - -### Producer related configurations (global) - producer.xxx -# http://pulsar.apache.org/docs/en/client-libraries-java/#configure-producer -producer.sendTimeoutMs= -producer.blockIfQueueFull=true -producer.maxPendingMessages=10000 -producer.batchingMaxMessages=10000 - - -### Consumer related configurations (global) - consumer.xxx -# http://pulsar.apache.org/docs/en/client-libraries-java/#configure-consumer -consumer.receiverQueueSize=2000 diff --git a/driver-jms/src/main/resources/pulsar_jms.yaml b/driver-jms/src/main/resources/pulsar_jms.yaml deleted file mode 100644 index 755f05ed6..000000000 --- a/driver-jms/src/main/resources/pulsar_jms.yaml +++ /dev/null @@ -1,89 +0,0 @@ -bindings: - payload: NumberNameToString() #AlphaNumericString(20) - tenant: Mod(10000); Div(10L); ToString(); Prefix("tnt") - namespace: Mod(10); Div(5L); ToString(); Prefix("ns") - core_topic_name: Mod(5); ToString(); Prefix("t") - -# document level parameters that apply to all Pulsar client types: -params: - ### static only - async_api: "true" - - ### Static only - # Valid values: queue (point-to-point) or topic (pub-sub) - jms_desitation_type: "topic" - - ### Static Only - # NOTE: ONLY relevant when the JMS provider is Pulsar - #pulsar_topic_uri: "persistent://{tenant}/{namespace}/{core_topic_name}" - #pulsar_topic_uri: "persistent://public/default/pt100" - #pulsar_topic_uri: "persistent://public/default/t0" - pulsar_topic_uri: "persistent://public/default/pt100_10" - #pulsar_topic_uri: "persistent://public/default/pt200_10" - #pulsar_topic_uri: "persistent://public/default/pt300_10" - #pulsar_topic_uri: "persistent://public/default/pt400_10" - -blocks: - - name: "producer-block" - tags: - phase: "jms_producer" - statements: - - name: "s1" - optype: "msg_send" - - ### JMS PRODUCER message header - ### https://docs.oracle.com/javaee/7/api/constant-values.html#javax.jms.DeliveryMode.NON_PERSISTENT - # - static or dynamic - # - Producer only - # Valid values: non-persistent(1), or persistent(2) - default - jms_producer_header_msg_delivery_mode: "2" - # Valid values: 0~9 (4 as default) - jms_producer_header_msg_priority: "4" - # Valid values: non-negative long; default 0 (never expires) - jms_producer_header_msg_ttl: "0" - # Valid values: non-negative long; default 0 (no delay) - jms_producer_header_msg_delivery_delay: "0" - # Valid values: true/false; default false (message timestamp is enabled) - jms_producer_header_disable_msg_timestamp: "false" - # Valid values: true/false; default false (message ID is enabled) - jms_producer_header_disable_msg_id: "false" - - ### JMS PRODUCER message properties - # - static only - # - Producer only - # - In format: "key1=value1;key2=value2;..." - jms_producer_msg_properties: "key1=value1;key2=value2" - - ### JMS PRODUCER message body - msg_body: "{payload}" - - - name: "consumer-block" - tags: - phase: "jms_consumer" - statements: - - name: "s1" - optype: "msg_read" - - ### JMS CONSUMER durable and shared - jms_consumer_msg_durable: "true" - jms_consumer_msg_shared: "true" - - ### JMS CONSUMER subscription name - # - only relevant for durable consumer - jms_consumer_subscription: "mysub" - - ### JMS CONSUMER subscription name - # - only relevant for unshared consumer - jms_consumer_nolocal: "false" - - ### JMS CONSUMER message read timeout - # - unit: milliseconds - # - 0 means call blocks indefinitely - # - FIXME: 0 supposes to wait indefinitly; but - # it actually behaves like no wait at all - jms_consumer_msg_read_timeout: "10000" - - ### JMS CONSUMER message selector - # - empty string means no message selector - # - https://docs.oracle.com/cd/E19798-01/821-1841/bncer/index.html - jms_consumer_msg_read_selector: "" diff --git a/engine-api/src/main/java/io/nosqlbench/engine/api/activityimpl/uniform/StandardAction.java b/engine-api/src/main/java/io/nosqlbench/engine/api/activityimpl/uniform/StandardAction.java index a9b61b51c..0e1612afd 100644 --- a/engine-api/src/main/java/io/nosqlbench/engine/api/activityimpl/uniform/StandardAction.java +++ b/engine-api/src/main/java/io/nosqlbench/engine/api/activityimpl/uniform/StandardAction.java @@ -53,11 +53,13 @@ public class StandardAction, R extends Op> impl private final Timer bindTimer; private final NBErrorHandler errorHandler; private final OpSequence> opsequence; + private final int maxTries; public StandardAction(A activity, int slot) { this.activity = activity; this.opsequence = activity.getOpSequence(); this.slot = slot; + this.maxTries = activity.getMaxTries(); bindTimer = activity.getInstrumentation().getOrCreateBindTimer(); executeTimer = activity.getInstrumentation().getOrCreateExecuteTimer(); triesHistogram = activity.getInstrumentation().getOrCreateTriesHistogram(); @@ -84,7 +86,7 @@ public class StandardAction, R extends Op> impl while (op != null) { int tries = 0; - while (tries++ <= activity.getMaxTries()) { + while (tries++ <= maxTries) { Throwable error = null; long startedAt = System.nanoTime(); diff --git a/engine-api/src/main/java/io/nosqlbench/engine/api/activityimpl/uniform/StandardActivity.java b/engine-api/src/main/java/io/nosqlbench/engine/api/activityimpl/uniform/StandardActivity.java index 13f873991..0b47d30c2 100644 --- a/engine-api/src/main/java/io/nosqlbench/engine/api/activityimpl/uniform/StandardActivity.java +++ b/engine-api/src/main/java/io/nosqlbench/engine/api/activityimpl/uniform/StandardActivity.java @@ -58,12 +58,11 @@ public class StandardActivity extends SimpleActivity implements Optional yaml_loc = activityDef.getParams().getOptionalString("yaml", "workload"); if (yaml_loc.isPresent()) { - Map disposable = new LinkedHashMap<>(activityDef.getParams()); + Map disposable = new LinkedHashMap<>(activityDef.getParams()); StmtsDocList workload = StatementsLoader.loadPath(logger, yaml_loc.get(), disposable, "activities"); yamlmodel = workload.getConfigModel(); - } - else { - yamlmodel= ConfigModel.of(StandardActivity.class).asReadOnly(); + } else { + yamlmodel = ConfigModel.of(StandardActivity.class).asReadOnly(); } ServiceLoader adapterLoader = ServiceLoader.load(DriverAdapter.class); @@ -77,7 +76,7 @@ public class StandardActivity extends SimpleActivity implements List adapterlist = new ArrayList<>(); for (OpTemplate ot : opTemplates) { ParsedOp incompleteOpDef = new ParsedOp(ot, NBConfiguration.empty(), List.of()); - String driverName = incompleteOpDef.takeOptionalStaticValue("driver",String.class) + String driverName = incompleteOpDef.takeOptionalStaticValue("driver", String.class) .or(() -> activityDef.getParams().getOptionalString("driver")) .orElseThrow(() -> new OpConfigError("Unable to identify driver name for op template:\n" + ot)); @@ -99,13 +98,13 @@ public class StandardActivity extends SimpleActivity implements combinedConfig = combinedModel.matchConfig(activityDef.getParams()); configurable.applyConfig(combinedConfig); } - adapters.put(driverName,adapter); - mappers.put(driverName,adapter.getOpMapper()); + adapters.put(driverName, adapter); + mappers.put(driverName, adapter.getOpMapper()); } DriverAdapter adapter = adapters.get(driverName); adapterlist.add(adapter); - ParsedOp pop = new ParsedOp(ot,adapter.getConfiguration(),List.of(adapter.getPreprocessor())); + ParsedOp pop = new ParsedOp(ot, adapter.getConfiguration(), List.of(adapter.getPreprocessor())); Optional discard = pop.takeOptionalStaticValue("driver", String.class); pops.add(pop); } @@ -152,13 +151,13 @@ public class StandardActivity extends SimpleActivity implements if (adapter instanceof NBReconfigurable configurable) { NBConfigModel cfgModel = configurable.getReconfigModel(); NBConfiguration cfg = cfgModel.matchConfig(activityDef.getParams()); - NBReconfigurable.applyMatching(cfg,List.of(configurable)); + NBReconfigurable.applyMatching(cfg, List.of(configurable)); } } } @Override - public List getSyntheticOpTemplates(StmtsDocList stmtsDocList, Map cfg) { + public List getSyntheticOpTemplates(StmtsDocList stmtsDocList, Map cfg) { List opTemplates = new ArrayList<>(); for (DriverAdapter adapter : adapters.values()) { if (adapter instanceof SyntheticOpTemplateProvider sotp) { @@ -169,4 +168,26 @@ public class StandardActivity extends SimpleActivity implements return opTemplates; } + /** + * This is done here since driver adapters are intended to keep all of their state within + * dedicated state space types. Any space which implements {@link io.nosqlbench.engine.api.activityapi.core.Shutdownable} + * will be closed when this activity shuts down. + */ + @Override + public void shutdownActivity() { + for (Map.Entry entry : adapters.entrySet()) { + String adapterName = entry.getKey(); + DriverAdapter adapter = entry.getValue(); + adapter.getSpaceCache().getElements().forEach((spaceName, space) -> { + if (space instanceof AutoCloseable autocloseable) { + try { + autocloseable.close(); + } catch (Exception e) { + throw new RuntimeException("Error while shutting down state space for " + + "adapter=" + adapterName + ", space=" + spaceName + ": " + e, e); + } + } + }); + } + } } diff --git a/engine-cli/src/main/java/io/nosqlbench/engine/cli/NBCLI.java b/engine-cli/src/main/java/io/nosqlbench/engine/cli/NBCLI.java index 76092e9aa..1111d4507 100644 --- a/engine-cli/src/main/java/io/nosqlbench/engine/cli/NBCLI.java +++ b/engine-cli/src/main/java/io/nosqlbench/engine/cli/NBCLI.java @@ -63,10 +63,10 @@ import java.util.stream.Collectors; public class NBCLI implements Function { private static Logger logger; - private static LoggerConfig loggerConfig; - private static int EXIT_OK = 0; - private static int EXIT_WARNING = 1; - private static int EXIT_ERROR = 2; + private static final LoggerConfig loggerConfig; + private static final int EXIT_OK = 0; + private static final int EXIT_WARNING = 1; + private static final int EXIT_ERROR = 2; static { loggerConfig = new LoggerConfig(); @@ -83,6 +83,7 @@ public class NBCLI implements Function { * Only call System.exit with the body of main. This is so that other scenario * invocations are handled functionally by {@link #apply(String[])}, which allows * for scenario encapsulation and concurrent testing. + * * @param args Command Line Args */ public static void main(String[] args) { @@ -91,15 +92,17 @@ public class NBCLI implements Function { int statusCode = cli.apply(args); System.exit(statusCode); } catch (Exception e) { - + System.out.println("Not expected issue in main: " + e.getMessage()); } } + /** - * return null; - * } + * return null; + * } * - * public static void main(String[] args) { - * @param strings + * public static void main(String[] args) { + * + * @param args * @return */ @Override @@ -114,10 +117,11 @@ public class NBCLI implements Function { if (arg.toLowerCase(Locale.ROOT).startsWith("-v") || (arg.toLowerCase(Locale.ROOT).equals("--show-stacktraces"))) { showStackTraces = true; + break; } } - String error = ScenarioErrorHandler.handle(e, showStackTraces); + String error = NBCLIErrorHandler.handle(e, showStackTraces); // Commented for now, as the above handler should do everything needed. if (error != null) { System.err.println("Scenario stopped due to error. See logs for details."); @@ -150,7 +154,7 @@ public class NBCLI implements Function { .setConsolePattern(globalOptions.getConsoleLoggingPattern()) .setLogfileLevel(globalOptions.getScenarioLogLevel()) .setLogfilePattern(globalOptions.getLogfileLoggingPattern()) - .getLoggerLevelOverrides(globalOptions.getLogLevelOverrides()) + .setLoggerLevelOverrides(globalOptions.getLogLevelOverrides()) .setMaxLogs(globalOptions.getLogsMax()) .setLogsDirectory(globalOptions.getLogsDirectory()) .setAnsiEnabled(globalOptions.isEnableAnsi()) @@ -175,10 +179,10 @@ public class NBCLI implements Function { // Invoke any bundled app which matches the name of the first non-option argument, if it exists. // If it does not, continue with no fanfare. Let it drop through to other command resolution methods. - if (args.length>0 && args[0].matches("\\w[\\w\\d-_.]+")) { + if (args.length > 0 && args[0].matches("\\w[\\w\\d-_.]+")) { ServiceSelector apploader = ServiceSelector.of(args[0], ServiceLoader.load(BundledApp.class)); BundledApp app = apploader.get().orElse(null); - if (app!=null) { + if (app != null) { String[] appargs = Arrays.copyOfRange(args, 1, args.length); logger.info("invoking bundled app '" + args[0] + "' (" + app.getClass().getSimpleName() + ")."); globalOptions.setWantsStackTraces(true); @@ -211,10 +215,10 @@ public class NBCLI implements Function { DockerMetricsManager.GRAFANA_TAG, globalOptions.getDockerGrafanaTag(), DockerMetricsManager.PROM_TAG, globalOptions.getDockerPromTag(), DockerMetricsManager.TSDB_RETENTION, String.valueOf(globalOptions.getDockerPromRetentionDays()), - DockerMetricsManager.GRAPHITE_SAMPLE_EXPIRY,"10m", - DockerMetricsManager.GRAPHITE_CACHE_SIZE,"5000", - DockerMetricsManager.GRAPHITE_LOG_LEVEL,globalOptions.getGraphiteLogLevel(), - DockerMetricsManager.GRAPHITE_LOG_FORMAT,"logfmt" + DockerMetricsManager.GRAPHITE_SAMPLE_EXPIRY, "10m", + DockerMetricsManager.GRAPHITE_CACHE_SIZE, "5000", + DockerMetricsManager.GRAPHITE_LOG_LEVEL, globalOptions.getGraphiteLogLevel(), + DockerMetricsManager.GRAPHITE_LOG_FORMAT, "logfmt" ); dmh.startMetrics(dashboardOptions); @@ -262,7 +266,7 @@ public class NBCLI implements Function { for (ServiceLoader.Provider provider : loader.stream().toList()) { Class appType = provider.type(); String name = appType.getAnnotation(Service.class).selector(); - System.out.println(String.format("%-40s %s",name,appType.getCanonicalName())); + System.out.printf("%-40s %s%n", name, appType.getCanonicalName()); } return EXIT_OK; } @@ -316,25 +320,25 @@ public class NBCLI implements Function { Path writeTo = Path.of(data.asPath().getFileName().toString()); if (Files.exists(writeTo)) { - throw new BasicError("A file named " + writeTo.toString() + " exists. Remove it first."); + throw new BasicError("A file named " + writeTo + " exists. Remove it first."); } try { Files.writeString(writeTo, data.getCharBuffer(), StandardCharsets.UTF_8); } catch (IOException e) { - throw new BasicError("Unable to write to " + writeTo.toString() + ": " + e.getMessage()); + throw new BasicError("Unable to write to " + writeTo + ": " + e.getMessage()); } - logger.info("Copied internal resource '" + data.asPath() + "' to '" + writeTo.toString() + "'"); + logger.info("Copied internal resource '" + data.asPath() + "' to '" + writeTo + "'"); return EXIT_OK; } if (options.wantsInputTypes()) { - InputType.FINDER.getAllSelectors().forEach((k,v) -> System.out.println(k + " (" + v.name() + ")")); + InputType.FINDER.getAllSelectors().forEach((k, v) -> System.out.println(k + " (" + v.name() + ")")); return EXIT_OK; } if (options.wantsMarkerTypes()) { - OutputType.FINDER.getAllSelectors().forEach((k,v) -> System.out.println(k + " (" + v.name() + ")")); + OutputType.FINDER.getAllSelectors().forEach((k, v) -> System.out.println(k + " (" + v.name() + ")")); return EXIT_OK; } @@ -464,27 +468,27 @@ public class NBCLI implements Function { executor.execute(scenario); - while (true) { - Optional pendingResult = executor.getPendingResult(scenario.getScenarioName()); - if (pendingResult.isEmpty()) { - LockSupport.parkNanos(100000000L); - } else { - break; - } - } +// while (true) { +// Optional pendingResult = executor.getPendingResult(scenario.getScenarioName()); +// if (pendingResult.isPresent()) { +// break; +// } +// LockSupport.parkNanos(100000000L); +// } ScenariosResults scenariosResults = executor.awaitAllResults(); + logger.debug("Total of " + scenariosResults.getSize() + " result object returned from ScenariosExecutor"); ActivityMetrics.closeMetrics(options.wantsEnableChart()); - //scenariosResults.reportToLog(); + scenariosResults.reportToLog(); ShutdownManager.shutdown(); -// logger.info(scenariosResults.getExecutionSummary()); + logger.info(scenariosResults.getExecutionSummary()); if (scenariosResults.hasError()) { Exception exception = scenariosResults.getOne().getException().get(); -// logger.warn(scenariosResults.getExecutionSummary()); - ScenarioErrorHandler.handle(exception, options.wantsStackTraces()); + logger.warn(scenariosResults.getExecutionSummary()); + NBCLIErrorHandler.handle(exception, options.wantsStackTraces()); System.err.println(exception.getMessage()); // TODO: make this consistent with ConsoleLogging sequencing return EXIT_ERROR; } else { diff --git a/engine-core/src/main/java/io/nosqlbench/engine/core/lifecycle/ActivityExceptionHandler.java b/engine-core/src/main/java/io/nosqlbench/engine/core/lifecycle/ActivityExceptionHandler.java index 5337299ef..5beb7643a 100644 --- a/engine-core/src/main/java/io/nosqlbench/engine/core/lifecycle/ActivityExceptionHandler.java +++ b/engine-core/src/main/java/io/nosqlbench/engine/core/lifecycle/ActivityExceptionHandler.java @@ -16,16 +16,24 @@ package io.nosqlbench.engine.core.lifecycle; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + public class ActivityExceptionHandler implements Thread.UncaughtExceptionHandler { + private static final Logger logger = LogManager.getLogger(ActivityExceptionHandler.class); + private final ActivityExecutor executor; public ActivityExceptionHandler(ActivityExecutor executor) { this.executor = executor; + logger.debug(() -> "Activity exception handler starting up for executor '" + executor + "'"); } + @Override public void uncaughtException(Thread t, Throwable e) { + logger.error("Uncaught exception in thread '" + t.getName() + ", state[" + t.getState() + "], notifying executor '" + executor + "'"); executor.notifyException(t, e); } } diff --git a/engine-core/src/main/java/io/nosqlbench/engine/core/lifecycle/ActivityExecutor.java b/engine-core/src/main/java/io/nosqlbench/engine/core/lifecycle/ActivityExecutor.java index d3f056eef..2ca5bfd71 100644 --- a/engine-core/src/main/java/io/nosqlbench/engine/core/lifecycle/ActivityExecutor.java +++ b/engine-core/src/main/java/io/nosqlbench/engine/core/lifecycle/ActivityExecutor.java @@ -15,14 +15,14 @@ */ package io.nosqlbench.engine.core.lifecycle; +import io.nosqlbench.api.annotations.Annotation; +import io.nosqlbench.api.annotations.Layer; +import io.nosqlbench.api.engine.activityimpl.ActivityDef; +import io.nosqlbench.api.engine.activityimpl.ParameterMap; import io.nosqlbench.engine.api.activityapi.core.*; import io.nosqlbench.engine.api.activityapi.core.progress.ProgressCapable; import io.nosqlbench.engine.api.activityapi.core.progress.ProgressMeterDisplay; -import io.nosqlbench.api.engine.activityimpl.ActivityDef; -import io.nosqlbench.api.engine.activityimpl.ParameterMap; import io.nosqlbench.engine.core.annotation.Annotators; -import io.nosqlbench.api.annotations.Annotation; -import io.nosqlbench.api.annotations.Layer; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -155,8 +155,8 @@ public class ActivityExecutor implements ActivityController, ParameterMap.Listen } public synchronized RuntimeException forceStopScenario(int initialMillisToWait) { - activitylogger.debug("FORCE STOP/before alias=(" + activity.getAlias() + ")"); + activitylogger.debug("FORCE STOP/before alias=(" + activity.getAlias() + ")"); activity.setRunState(RunState.Stopped); executorService.shutdown(); @@ -214,23 +214,29 @@ public class ActivityExecutor implements ActivityController, ParameterMap.Listen } public boolean finishAndShutdownExecutor(int secondsToWait) { - activitylogger.debug("REQUEST STOP/before alias=(" + activity.getAlias() + ")"); + activitylogger.debug("REQUEST STOP/before alias=(" + activity.getAlias() + ")"); logger.debug("Stopping executor for " + activity.getAlias() + " when work completes."); - executorService.shutdown(); boolean wasStopped = false; try { + executorService.shutdown(); logger.trace(() -> "awaiting termination with timeout of " + secondsToWait + " seconds"); wasStopped = executorService.awaitTermination(secondsToWait, TimeUnit.SECONDS); } catch (InterruptedException ie) { logger.trace("interrupted while awaiting termination"); wasStopped = false; - logger.warn("while waiting termination of activity " + activity.getAlias() + ", " + ie.getMessage()); + logger.warn("while waiting termination of shutdown " + activity.getAlias() + ", " + ie.getMessage()); activitylogger.debug("REQUEST STOP/exception alias=(" + activity.getAlias() + ") wasstopped=" + wasStopped); + } catch (RuntimeException e) { + logger.trace("Received exception while awaiting termination: " + e.getMessage()); + wasStopped = true; + stoppingException = e; } finally { + logger.trace(() -> "finally shutting down activity " + this.getActivity().getAlias()); activity.shutdownActivity(); + logger.trace("closing auto-closeables"); activity.closeAutoCloseables(); activity.setRunState(RunState.Stopped); @@ -241,6 +247,7 @@ public class ActivityExecutor implements ActivityController, ParameterMap.Listen logger.trace(() -> "an exception caused the activity to stop:" + stoppingException.getMessage()); throw stoppingException; } + activitylogger.debug("REQUEST STOP/after alias=(" + activity.getAlias() + ") wasstopped=" + wasStopped); return wasStopped; @@ -278,11 +285,13 @@ public class ActivityExecutor implements ActivityController, ParameterMap.Listen * This is the canonical way to wait for an activity to finish. It ties together * any way that an activity can finish under one blocking call. * This should be awaited asynchronously from the control layer in separate threads. - * - * TODO: move activity finisher threaad to this class and remove separate implementation + *

+ * TODO: move activity finisher thread to this class and remove separate implementation */ public boolean awaitCompletion(int waitTime) { + logger.debug(()-> "awaiting completion of '" + this.getActivity().getAlias() + "'"); boolean finished = finishAndShutdownExecutor(waitTime); + Annotators.recordAnnotation(Annotation.newBuilder() .session(sessionId) .interval(startedAt, this.stoppedAt) @@ -412,7 +421,7 @@ public class ActivityExecutor implements ActivityController, ParameterMap.Listen * Await a thread (aka motor/slot) entering a specific SlotState * * @param m motor instance - * @param waitTime milliseconds to wait, total + * @param waitTime milliseco`nds to wait, total * @param pollTime polling interval between state checks * @param desiredRunStates any desired SlotState * @return true, if the desired SlotState was detected @@ -521,7 +530,7 @@ public class ActivityExecutor implements ActivityController, ParameterMap.Listen } public synchronized void notifyException(Thread t, Throwable e) { - //logger.error("Uncaught exception in activity thread forwarded to activity executor:", e); + logger.debug(() -> "Uncaught exception in activity thread forwarded to activity executor: " + e.getMessage()); this.stoppingException = new RuntimeException("Error in activity thread " + t.getName(), e); forceStopScenario(10000); } diff --git a/engine-core/src/main/java/io/nosqlbench/engine/core/lifecycle/ActivityFinisher.java b/engine-core/src/main/java/io/nosqlbench/engine/core/lifecycle/ActivityFinisher.java index 54db6b996..845f7c272 100644 --- a/engine-core/src/main/java/io/nosqlbench/engine/core/lifecycle/ActivityFinisher.java +++ b/engine-core/src/main/java/io/nosqlbench/engine/core/lifecycle/ActivityFinisher.java @@ -16,7 +16,11 @@ package io.nosqlbench.engine.core.lifecycle; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + public class ActivityFinisher extends Thread { + private final static Logger logger = LogManager.getLogger(ActivityFinisher.class); private final ActivityExecutor executor; private final int timeout; @@ -30,10 +34,17 @@ public class ActivityFinisher extends Thread { @Override public void run() { + logger.debug(this + " awaiting async completion of " + executor.getActivity().getAlias() + " on " + executor + " for timeout " + timeout); result = executor.awaitCompletion(timeout); + logger.debug(this + " awaited async completion of " + executor.getActivity().getAlias()); } public boolean getResult() { return result; } + + @Override + public String toString() { + return this.getClass().getSimpleName()+"/" + executor.getActivity().getAlias(); + } } diff --git a/engine-core/src/main/java/io/nosqlbench/engine/core/lifecycle/ScenarioErrorHandler.java b/engine-core/src/main/java/io/nosqlbench/engine/core/lifecycle/NBCLIErrorHandler.java similarity index 87% rename from engine-core/src/main/java/io/nosqlbench/engine/core/lifecycle/ScenarioErrorHandler.java rename to engine-core/src/main/java/io/nosqlbench/engine/core/lifecycle/NBCLIErrorHandler.java index 0b9e45f9b..1a786377f 100644 --- a/engine-core/src/main/java/io/nosqlbench/engine/core/lifecycle/ScenarioErrorHandler.java +++ b/engine-core/src/main/java/io/nosqlbench/engine/core/lifecycle/NBCLIErrorHandler.java @@ -17,9 +17,9 @@ package io.nosqlbench.engine.core.lifecycle; import io.nosqlbench.api.errors.BasicError; -import org.graalvm.polyglot.PolyglotException; -import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.graalvm.polyglot.PolyglotException; import javax.script.ScriptException; @@ -32,28 +32,28 @@ import javax.script.ScriptException; *

    *
  1. Report an error in the most intelligible way to the user.
  2. *
- * + *

* That is all. When this error handler is invoked, it is a foregone conclusion that the scenario * is not able to continue, else the error would have been trapped and handled internal to a lower-level * class. It is the calling exception handler's responsibility to finally shut down the scenario * cleanly and return appropriately. Thus, You should not throw errors from this class. You should only * unwrap and explain errors, sending contents to the logfile as appropriate. - * */ -public class ScenarioErrorHandler { +public class NBCLIErrorHandler { private final static Logger logger = LogManager.getLogger("ERRORHANDLER"); public static String handle(Throwable t, boolean wantsStackTraces) { + if (wantsStackTraces) { StackTraceElement[] st = Thread.currentThread().getStackTrace(); for (int i = 0; i < 10; i++) { - if (st.length>i) { + if (st.length > i) { String className = st[i].getClassName(); String fileName = st[i].getFileName(); int lineNumber = st[i].getLineNumber(); - logger.trace("st["+i+"]:" + className +","+fileName+":"+lineNumber); + logger.trace("st[" + i + "]:" + className + "," + fileName + ":" + lineNumber); } } } @@ -63,18 +63,18 @@ public class ScenarioErrorHandler { } else if (t instanceof BasicError) { logger.trace("Handling basic error: " + t); return handleBasicError((BasicError) t, wantsStackTraces); - } else if (t instanceof Exception){ + } else if (t instanceof Exception) { logger.trace("Handling general exception: " + t); return handleInternalError((Exception) t, wantsStackTraces); } else { - logger.error("Unknown type for error handler: " + t); - throw new RuntimeException("Error in exception handler", t); + logger.error("Unknown type for error handler: " + t); + throw new RuntimeException("Error in exception handler", t); } } private static String handleInternalError(Exception e, boolean wantsStackTraces) { String prefix = "internal error: "; - if (e.getCause()!=null && !e.getCause().getClass().getCanonicalName().contains("io.nosqlbench")) { + if (e.getCause() != null && !e.getCause().getClass().getCanonicalName().contains("io.nosqlbench")) { prefix = "Error from driver or included library: "; } @@ -95,13 +95,13 @@ public class ScenarioErrorHandler { if (cause instanceof PolyglotException) { Throwable hostException = ((PolyglotException) cause).asHostException(); if (hostException instanceof BasicError) { - handleBasicError((BasicError)hostException, wantsStackTraces); + handleBasicError((BasicError) hostException, wantsStackTraces); } else { handle(hostException, wantsStackTraces); } } else { if (wantsStackTraces) { - logger.error("Unknown script exception:",e); + logger.error("Unknown script exception:", e); } else { logger.error(e.getMessage()); logger.error("for the full stack trace, run with --show-stacktraces"); @@ -112,7 +112,7 @@ public class ScenarioErrorHandler { private static String handleBasicError(BasicError e, boolean wantsStackTraces) { if (wantsStackTraces) { - logger.error(e.getMessage(),e); + logger.error(e.getMessage(), e); } else { logger.error(e.getMessage()); logger.error("for the full stack trace, run with --show-stacktraces"); diff --git a/engine-core/src/main/java/io/nosqlbench/engine/core/lifecycle/ScenarioController.java b/engine-core/src/main/java/io/nosqlbench/engine/core/lifecycle/ScenarioController.java index 339d289a5..0bda67c11 100644 --- a/engine-core/src/main/java/io/nosqlbench/engine/core/lifecycle/ScenarioController.java +++ b/engine-core/src/main/java/io/nosqlbench/engine/core/lifecycle/ScenarioController.java @@ -431,6 +431,7 @@ public class ScenarioController { * @return true, if all activities completed before the timer expired, false otherwise */ public boolean awaitCompletion(long waitTimeMillis) { + logger.debug(() -> "awaiting completion"); boolean completed = true; long remaining = waitTimeMillis; @@ -443,7 +444,9 @@ public class ScenarioController { for (ActivityFinisher finisher : finishers) { try { + logger.debug("joining finisher " + finisher.getName()); finisher.join(waitTimeMillis); + logger.debug("joined finisher " + finisher.getName()); } catch (InterruptedException ignored) { } } diff --git a/engine-core/src/main/java/io/nosqlbench/engine/core/lifecycle/ScenarioResult.java b/engine-core/src/main/java/io/nosqlbench/engine/core/lifecycle/ScenarioResult.java index 47ab67f2f..54bf4d007 100644 --- a/engine-core/src/main/java/io/nosqlbench/engine/core/lifecycle/ScenarioResult.java +++ b/engine-core/src/main/java/io/nosqlbench/engine/core/lifecycle/ScenarioResult.java @@ -54,17 +54,19 @@ public class ScenarioResult { private final long startedAt; private final long endedAt; - private Exception exception; + private final Exception exception; private final String iolog; - public ScenarioResult(String iolog, long startedAt, long endedAt) { - this.iolog = iolog; - this.startedAt = startedAt; - this.endedAt = endedAt; - } - - public ScenarioResult(Exception e, long startedAt, long endedAt) { - this.iolog = e.getMessage(); + public ScenarioResult(Exception e, String iolog, long startedAt, long endedAt) { + logger.debug("populating "+(e==null? "NORMAL" : "ERROR")+" scenario result"); + if (logger.isDebugEnabled()) { + StackTraceElement[] st = Thread.currentThread().getStackTrace(); + for (int i = 0; i < st.length; i++) { + logger.debug(":AT " + st[i].getFileName()+":"+st[i].getLineNumber()+":"+st[i].getMethodName()); + if (i>10) break; + } + } + this.iolog = ((iolog!=null) ? iolog + "\n\n" : "") + (e!=null? e.getMessage() : ""); this.startedAt = startedAt; this.endedAt = endedAt; this.exception = e; @@ -147,15 +149,14 @@ public class ScenarioResult { StringBuilder sb = new StringBuilder(); ActivityMetrics.getMetricRegistry().getMetrics().forEach((k, v) -> { - if (v instanceof Counting) { - long count = ((Counting) v).getCount(); + if (v instanceof Counting counting) { + long count = counting.getCount(); if (count > 0) { NBMetricsSummary.summarize(sb, k, v); } - } else if (v instanceof Gauge) { - Object value = ((Gauge) v).getValue(); - if (value != null && value instanceof Number) { - Number n = (Number) value; + } else if (v instanceof Gauge gauge) { + Object value = gauge.getValue(); + if (value instanceof Number n) { if (n.doubleValue() != 0) { NBMetricsSummary.summarize(sb, k, v); } diff --git a/engine-core/src/main/java/io/nosqlbench/engine/core/lifecycle/ScenariosResults.java b/engine-core/src/main/java/io/nosqlbench/engine/core/lifecycle/ScenariosResults.java index 7f74f68ca..6eb5774ea 100644 --- a/engine-core/src/main/java/io/nosqlbench/engine/core/lifecycle/ScenariosResults.java +++ b/engine-core/src/main/java/io/nosqlbench/engine/core/lifecycle/ScenariosResults.java @@ -27,7 +27,6 @@ import java.util.Map; public class ScenariosResults { private static final Logger logger = LogManager.getLogger(ScenariosResults.class); - private final String scenariosExecutorName; private final Map scenarioResultMap = new LinkedHashMap<>(); @@ -77,4 +76,8 @@ public class ScenariosResults { return this.scenarioResultMap.values().stream() .anyMatch(r -> r.getException().isPresent()); } + + public int getSize() { + return this.scenarioResultMap.size(); + } } diff --git a/engine-core/src/main/java/io/nosqlbench/engine/core/logging/LoggerConfig.java b/engine-core/src/main/java/io/nosqlbench/engine/core/logging/LoggerConfig.java index 195dcdda1..b8e7d1af3 100644 --- a/engine-core/src/main/java/io/nosqlbench/engine/core/logging/LoggerConfig.java +++ b/engine-core/src/main/java/io/nosqlbench/engine/core/logging/LoggerConfig.java @@ -29,15 +29,14 @@ import org.apache.logging.log4j.core.config.ConfigurationSource; import org.apache.logging.log4j.core.config.builder.api.*; import org.apache.logging.log4j.core.config.builder.impl.BuiltConfiguration; -import java.nio.file.attribute.*; - - import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.PosixFilePermissions; import java.util.*; import java.util.stream.Collectors; @@ -55,10 +54,10 @@ import java.util.stream.Collectors; public class LoggerConfig extends ConfigurationFactory { public static Map STANDARD_FORMATS = Map.of( - "TERSE", "%8r %-5level [%t] %-12logger{0} %msg%n%throwable", - "VERBOSE", "%d{DEFAULT}{GMT} [%t] %logger %-5level: %msg%n%throwable", - "TERSE-ANSI", "%8r %highlight{%-5level} %style{%C{1.} [%t] %-12logger{0}} %msg%n%throwable", - "VERBOSE-ANSI", "%d{DEFAULT}{GMT} [%t] %highlight{%logger %-5level}: %msg%n%throwable" + "TERSE", "%8r %-5level [%t] %-12logger{0} %msg%n%throwable", + "VERBOSE", "%d{DEFAULT}{GMT} [%t] %logger %-5level: %msg%n%throwable", + "TERSE-ANSI", "%8r %highlight{%-5level} %style{%C{1.} [%t] %-12logger{0}} %msg%n%throwable", + "VERBOSE-ANSI", "%d{DEFAULT}{GMT} [%t] %highlight{%logger %-5level}: %msg%n%throwable" ); /** @@ -66,7 +65,7 @@ public class LoggerConfig extends ConfigurationFactory { * we squelch them to some reasonable level so they aren't a nuisance. */ public static Map BUILTIN_OVERRIDES = Map.of( - "oshi.util", Level.INFO + "oshi.util", Level.INFO ); /** @@ -151,20 +150,20 @@ public class LoggerConfig extends ConfigurationFactory { builder.setStatusLevel(internalLoggingStatusThreshold); builder.add( - builder.newFilter( - "ThresholdFilter", - Filter.Result.ACCEPT, - Filter.Result.NEUTRAL - ).addAttribute("level", builderThresholdLevel) + builder.newFilter( + "ThresholdFilter", + Filter.Result.ACCEPT, + Filter.Result.NEUTRAL + ).addAttribute("level", builderThresholdLevel) ); // CONSOLE appender AppenderComponentBuilder appenderBuilder = - builder.newAppender("console", "CONSOLE") - .addAttribute("target", ConsoleAppender.Target.SYSTEM_OUT); + builder.newAppender("console", "CONSOLE") + .addAttribute("target", ConsoleAppender.Target.SYSTEM_OUT); appenderBuilder.add(builder.newLayout("PatternLayout") - .addAttribute("pattern", consolePattern)); + .addAttribute("pattern", consolePattern)); // appenderBuilder.add( // builder.newFilter("MarkerFilter", Filter.Result.DENY, Filter.Result.NEUTRAL) @@ -174,8 +173,8 @@ public class LoggerConfig extends ConfigurationFactory { // Log4J internal logging builder.add(builder.newLogger("org.apache.logging.log4j", Level.DEBUG) - .add(builder.newAppenderRef("console")) - .addAttribute("additivity", false)); + .add(builder.newAppenderRef("console")) + .addAttribute("additivity", false)); if (sessionName != null) { @@ -189,55 +188,55 @@ public class LoggerConfig extends ConfigurationFactory { // LOGFILE appender LayoutComponentBuilder logfileLayout = builder.newLayout("PatternLayout") - .addAttribute("pattern", logfilePattern); + .addAttribute("pattern", logfilePattern); String filebase = getSessionName().replaceAll("\\s", "_"); String logfilePath = loggerDir.resolve(filebase + ".log").toString(); this.logfileLocation = logfilePath; String archivePath = loggerDir.resolve(filebase + "-TIMESTAMP.log.gz").toString() - .replaceAll("TIMESTAMP", "%d{MM-dd-yy}"); + .replaceAll("TIMESTAMP", "%d{MM-dd-yy}"); ComponentBuilder triggeringPolicy = builder.newComponent("Policies") - .addComponent(builder.newComponent("CronTriggeringPolicy").addAttribute("schedule", "0 0 0 * * ?")) - .addComponent(builder.newComponent("SizeBasedTriggeringPolicy").addAttribute("size", "100M")); + .addComponent(builder.newComponent("CronTriggeringPolicy").addAttribute("schedule", "0 0 0 * * ?")) + .addComponent(builder.newComponent("SizeBasedTriggeringPolicy").addAttribute("size", "100M")); AppenderComponentBuilder logsAppenderBuilder = - builder.newAppender("SCENARIO_APPENDER", RollingFileAppender.PLUGIN_NAME) - .addAttribute("fileName", logfilePath) - .addAttribute("filePattern", archivePath) - .addAttribute("append", false) - .add(logfileLayout) - .addComponent(triggeringPolicy); + builder.newAppender("SCENARIO_APPENDER", RollingFileAppender.PLUGIN_NAME) + .addAttribute("fileName", logfilePath) + .addAttribute("filePattern", archivePath) + .addAttribute("append", false) + .add(logfileLayout) + .addComponent(triggeringPolicy); builder.add(logsAppenderBuilder); rootBuilder.add( - builder.newAppenderRef("SCENARIO_APPENDER") - .addAttribute("level", fileLevel) + builder.newAppenderRef("SCENARIO_APPENDER") + .addAttribute("level", fileLevel) ); } rootBuilder.add( - builder.newAppenderRef("console") - .addAttribute("level", - consoleLevel - ) + builder.newAppenderRef("console") + .addAttribute("level", + consoleLevel + ) ); builder.add(rootBuilder); BUILTIN_OVERRIDES.forEach((k, v) -> { builder.add(builder.newLogger(k, v) - .add(builder.newAppenderRef("console")) - .add(builder.newAppenderRef("SCENARIO_APPENDER")) - .addAttribute("additivity", true)); + .add(builder.newAppenderRef("console")) + .add(builder.newAppenderRef("SCENARIO_APPENDER")) + .addAttribute("additivity", true)); }); logLevelOverrides.forEach((k, v) -> { Level olevel = Level.valueOf(v); builder.add(builder.newLogger(k, olevel) - .add(builder.newAppenderRef("console")) - .add(builder.newAppenderRef("SCENARIO_APPENDER")) - .addAttribute("additivity", true)); + .add(builder.newAppenderRef("console")) + .add(builder.newAppenderRef("SCENARIO_APPENDER")) + .addAttribute("additivity", true)); }); BuiltConfiguration builtConfig = builder.build(); @@ -268,7 +267,7 @@ public class LoggerConfig extends ConfigurationFactory { if (!Files.exists(loggerDir)) { try { FileAttribute> attrs = PosixFilePermissions.asFileAttribute( - PosixFilePermissions.fromString("rwxrwx---") + PosixFilePermissions.fromString("rwxrwx---") ); Path directory = Files.createDirectory(loggerDir, attrs); } catch (Exception e) { @@ -280,22 +279,22 @@ public class LoggerConfig extends ConfigurationFactory { public LoggerConfig setConsolePattern(String consoleLoggingPattern) { - consoleLoggingPattern= (ansiEnabled && STANDARD_FORMATS.containsKey(consoleLoggingPattern+"-ANSI")) - ? consoleLoggingPattern+"-ANSI" : consoleLoggingPattern; + consoleLoggingPattern = (ansiEnabled && STANDARD_FORMATS.containsKey(consoleLoggingPattern + "-ANSI")) + ? consoleLoggingPattern + "-ANSI" : consoleLoggingPattern; this.consolePattern = STANDARD_FORMATS.getOrDefault(consoleLoggingPattern, consoleLoggingPattern); return this; } public LoggerConfig setLogfilePattern(String logfileLoggingPattern) { - logfileLoggingPattern= (logfileLoggingPattern.endsWith("-ANSI") && STANDARD_FORMATS.containsKey(logfileLoggingPattern)) - ? logfileLoggingPattern.substring(logfileLoggingPattern.length()-5) : logfileLoggingPattern; + logfileLoggingPattern = (logfileLoggingPattern.endsWith("-ANSI") && STANDARD_FORMATS.containsKey(logfileLoggingPattern)) + ? logfileLoggingPattern.substring(logfileLoggingPattern.length() - 5) : logfileLoggingPattern; this.logfileLocation = STANDARD_FORMATS.getOrDefault(logfileLoggingPattern, logfileLoggingPattern); return this; } - public LoggerConfig getLoggerLevelOverrides(Map logLevelOverrides) { + public LoggerConfig setLoggerLevelOverrides(Map logLevelOverrides) { this.logLevelOverrides = logLevelOverrides; return this; } @@ -334,9 +333,9 @@ public class LoggerConfig extends ConfigurationFactory { } List toDelete = filesList.stream() - .sorted(fileTimeComparator) - .limit(remove) - .collect(Collectors.toList()); + .sorted(fileTimeComparator) + .limit(remove) + .collect(Collectors.toList()); for (File file : toDelete) { logger.info("removing extra logfile: " + file.getPath()); diff --git a/engine-core/src/main/java/io/nosqlbench/engine/core/script/Scenario.java b/engine-core/src/main/java/io/nosqlbench/engine/core/script/Scenario.java index 6f6108428..93fb5183c 100644 --- a/engine-core/src/main/java/io/nosqlbench/engine/core/script/Scenario.java +++ b/engine-core/src/main/java/io/nosqlbench/engine/core/script/Scenario.java @@ -17,8 +17,13 @@ package io.nosqlbench.engine.core.script; import com.codahale.metrics.MetricRegistry; import com.oracle.truffle.js.scriptengine.GraalJSScriptEngine; -import io.nosqlbench.engine.api.extensions.ScriptingPluginInfo; +import io.nosqlbench.api.annotations.Annotation; +import io.nosqlbench.api.annotations.Layer; import io.nosqlbench.api.engine.metrics.ActivityMetrics; +import io.nosqlbench.api.metadata.ScenarioMetadata; +import io.nosqlbench.api.metadata.ScenarioMetadataAware; +import io.nosqlbench.api.metadata.SystemId; +import io.nosqlbench.engine.api.extensions.ScriptingPluginInfo; import io.nosqlbench.engine.api.scripting.ScriptEnvBuffer; import io.nosqlbench.engine.core.annotation.Annotators; import io.nosqlbench.engine.core.lifecycle.ActivityProgressIndicator; @@ -27,14 +32,12 @@ import io.nosqlbench.engine.core.lifecycle.ScenarioController; import io.nosqlbench.engine.core.lifecycle.ScenarioResult; import io.nosqlbench.engine.core.metrics.PolyglotMetricRegistryBindings; import io.nosqlbench.nb.annotations.Maturity; -import io.nosqlbench.api.annotations.Annotation; -import io.nosqlbench.api.annotations.Layer; -import io.nosqlbench.api.metadata.ScenarioMetadata; -import io.nosqlbench.api.metadata.ScenarioMetadataAware; -import io.nosqlbench.api.metadata.SystemId; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.graalvm.polyglot.*; +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.EnvironmentAccess; +import org.graalvm.polyglot.HostAccess; +import org.graalvm.polyglot.PolyglotAccess; import javax.script.Compilable; import javax.script.CompiledScript; @@ -68,6 +71,12 @@ public class Scenario implements Callable { private Exception error; private ScenarioMetadata scenarioMetadata; + private ScenarioResult result; + + public Optional getResultIfComplete() { + return Optional.ofNullable(this.result); + } + public enum State { Scheduled, @@ -162,10 +171,9 @@ public class Scenario implements Callable { return this; } - private void init() { + private void initializeScriptingEngine() { logger.debug("Using engine " + engine.toString()); - MetricRegistry metricRegistry = ActivityMetrics.getMetricRegistry(); Context.Builder contextSettings = Context.newBuilder("js") @@ -183,7 +191,7 @@ public class Scenario implements Callable { .option("js.nashorn-compat", "true"); org.graalvm.polyglot.Engine.Builder engineBuilder = org.graalvm.polyglot.Engine.newBuilder(); - engineBuilder.option("engine.WarnInterpreterOnly","false"); + engineBuilder.option("engine.WarnInterpreterOnly", "false"); org.graalvm.polyglot.Engine polyglotEngine = engineBuilder.build(); // TODO: add in, out, err for this scenario @@ -205,9 +213,9 @@ public class Scenario implements Callable { // scriptEngine.put("metrics", new PolyglotMetricRegistryBindings(metricRegistry)); // scriptEngine.put("activities", new NashornActivityBindings(scenarioController)); - scriptEngine.put("scenario", new PolyglotScenarioController(scenarioController)); - scriptEngine.put("metrics", new PolyglotMetricRegistryBindings(metricRegistry)); - scriptEngine.put("activities", new NashornActivityBindings(scenarioController)); + scriptEngine.put("scenario", new PolyglotScenarioController(scenarioController)); + scriptEngine.put("metrics", new PolyglotMetricRegistryBindings(metricRegistry)); + scriptEngine.put("activities", new NashornActivityBindings(scenarioController)); for (ScriptingPluginInfo extensionDescriptor : SandboxExtensionFinder.findAll()) { if (!extensionDescriptor.isAutoLoading()) { @@ -241,12 +249,11 @@ public class Scenario implements Callable { return scenarioMetadata; } - public void runScenario() { + private synchronized void runScenario() { scenarioShutdownHook = new ScenarioShutdownHook(this); Runtime.getRuntime().addShutdownHook(scenarioShutdownHook); state = State.Running; - startedAtMillis = System.currentTimeMillis(); Annotators.recordAnnotation( Annotation.newBuilder() @@ -256,21 +263,21 @@ public class Scenario implements Callable { .detail("engine", this.engine.toString()) .build() ); - init(); + + initializeScriptingEngine(); logger.debug("Running control script for " + getScenarioName() + "."); + for (String script : scripts) { try { Object result = null; - if (scriptEngine instanceof Compilable && wantsCompiledScript) { + if (scriptEngine instanceof Compilable compilableEngine && wantsCompiledScript) { logger.debug("Using direct script compilation"); - Compilable compilableEngine = (Compilable) scriptEngine; CompiledScript compiled = compilableEngine.compile(script); logger.debug("-> invoking main scenario script (compiled)"); result = compiled.eval(); logger.debug("<- scenario script completed (compiled)"); } else { if (scriptfile != null && !scriptfile.isEmpty()) { - String filename = scriptfile.replace("_SESSION_", scenarioName); logger.debug("-> invoking main scenario script (" + "interpreted from " + filename + ")"); @@ -292,16 +299,21 @@ public class Scenario implements Callable { } if (result != null) { - logger.debug("scenario result: type(" + result.getClass().getCanonicalName() + "): value:" + result.toString()); + logger.debug("scenario result: type(" + result.getClass().getCanonicalName() + "): value:" + result); } System.err.flush(); System.out.flush(); } catch (Exception e) { this.state = State.Errored; - logger.error("Error in scenario, shutting down. (" + e.toString() + ")"); - this.scenarioController.forceStopScenario(5000, false); - this.error = e; - throw new RuntimeException(e); + logger.error("Error in scenario, shutting down. (" + e + ")"); + try { + this.scenarioController.forceStopScenario(5000, false); + } catch (Exception eInner) { + logger.debug("Found inner exception while forcing stop with rethrow=false: " + eInner); + } finally { + this.error = e; + throw new RuntimeException(e); + } } finally { System.out.flush(); System.err.flush(); @@ -355,14 +367,29 @@ public class Scenario implements Callable { return endedAtMillis; } - public ScenarioResult call() { - runScenario(); - String iolog = scriptEnv.getTimedLog(); - ScenarioResult result = new ScenarioResult(iolog, this.startedAtMillis, this.endedAtMillis); + /** + * This should be the only way to get a ScenarioResult for a Scenario. + * + * @return + */ + public synchronized ScenarioResult call() { + if (result == null) { + try { + runScenario(); + } catch (Exception e) { + if (this.error!=null) { + logger.debug("OVERLAPPING ERRORS: prior" + this.error.getMessage() + ", current:" + e.getMessage()); + } + this.error = e; + } finally { + logger.debug((this.error == null ? "NORMAL" : "ERRORED") + " scenario run"); + } - result.reportToLog(); - - doReportSummaries(reportSummaryTo, result); + String iolog = scriptEnv.getTimedLog(); + this.result = new ScenarioResult(this.error, iolog, this.startedAtMillis, this.endedAtMillis); + result.reportToLog(); + doReportSummaries(reportSummaryTo, result); + } return result; } diff --git a/engine-core/src/main/java/io/nosqlbench/engine/core/script/ScenariosExecutor.java b/engine-core/src/main/java/io/nosqlbench/engine/core/script/ScenariosExecutor.java index 023736b43..0d6581266 100644 --- a/engine-core/src/main/java/io/nosqlbench/engine/core/script/ScenariosExecutor.java +++ b/engine-core/src/main/java/io/nosqlbench/engine/core/script/ScenariosExecutor.java @@ -16,10 +16,7 @@ package io.nosqlbench.engine.core.script; -import io.nosqlbench.engine.core.lifecycle.IndexedThreadFactory; -import io.nosqlbench.engine.core.lifecycle.ScenarioController; -import io.nosqlbench.engine.core.lifecycle.ScenarioResult; -import io.nosqlbench.engine.core.lifecycle.ScenariosResults; +import io.nosqlbench.engine.core.lifecycle.*; import io.nosqlbench.api.errors.BasicError; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -43,9 +40,9 @@ public class ScenariosExecutor { public ScenariosExecutor(String name, int threads) { executor = new ThreadPoolExecutor(1, threads, - 0L, TimeUnit.MILLISECONDS, - new LinkedBlockingQueue<>(), - new IndexedThreadFactory("scenarios", new ScenarioExceptionHandler(this))); + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>(), + new IndexedThreadFactory("scenarios", new ScenarioExceptionHandler(this))); this.name = name; } @@ -92,7 +89,6 @@ public class ScenariosExecutor { long waitedAt = System.currentTimeMillis(); long updateAt = Math.min(timeoutAt, waitedAt + updateInterval); while (!isShutdown && System.currentTimeMillis() < timeoutAt) { - while (!isShutdown && System.currentTimeMillis() < updateAt) { try { long timeRemaining = updateAt - System.currentTimeMillis(); @@ -108,11 +104,17 @@ public class ScenariosExecutor { if (!isShutdown) { throw new RuntimeException("executor still runningScenarios after awaiting all results for " + timeout - + "ms. isTerminated:" + executor.isTerminated() + " isShutdown:" + executor.isShutdown()); + + "ms. isTerminated:" + executor.isTerminated() + " isShutdown:" + executor.isShutdown()); } Map scenarioResultMap = new LinkedHashMap<>(); getAsyncResultStatus() - .entrySet().forEach(es -> scenarioResultMap.put(es.getKey(), es.getValue().orElse(null))); + .entrySet() + .forEach( + es -> scenarioResultMap.put( + es.getKey(), + es.getValue().orElse(null) + ) + ); return new ScenariosResults(this, scenarioResultMap); } @@ -121,9 +123,9 @@ public class ScenariosExecutor { */ public List getPendingScenarios() { return new ArrayList<>( - submitted.values().stream() - .map(SubmittedScenario::getName) - .collect(Collectors.toCollection(ArrayList::new))); + submitted.values().stream() + .map(SubmittedScenario::getName) + .collect(Collectors.toCollection(ArrayList::new))); } /** @@ -149,7 +151,8 @@ public class ScenariosExecutor { oResult = Optional.of(resultFuture.get()); } catch (Exception e) { long now = System.currentTimeMillis(); - oResult = Optional.of(new ScenarioResult(e, now, now)); + logger.debug("creating exceptional scenario result from getAsyncResultStatus"); + oResult = Optional.of(new ScenarioResult(e, "errored output", now, now)); } } @@ -176,23 +179,8 @@ public class ScenariosExecutor { * @param scenarioName the scenario name of interest * @return an optional result */ - public Optional getPendingResult(String scenarioName) { - - Future resultFuture1 = submitted.get(scenarioName).resultFuture; - if (resultFuture1 == null) { - throw new BasicError("Unknown scenario name:" + scenarioName); - } - long now = System.currentTimeMillis(); - if (resultFuture1.isDone()) { - try { - return Optional.ofNullable(resultFuture1.get()); - } catch (Exception e) { - return Optional.of(new ScenarioResult(e, now, now)); - } - } else if (resultFuture1.isCancelled()) { - return Optional.of(new ScenarioResult(new Exception("result was cancelled."), now, now)); - } - return Optional.empty(); + public Optional> getPendingResult(String scenarioName) { + return Optional.ofNullable(submitted.get(scenarioName)).map(s -> s.resultFuture); } public synchronized void stopScenario(String scenarioName) { @@ -200,6 +188,7 @@ public class ScenariosExecutor { } public synchronized void stopScenario(String scenarioName, boolean rethrow) { + logger.debug("#stopScenario(name=" + scenarioName + ", rethrow="+ rethrow+")"); Optional pendingScenario = getPendingScenario(scenarioName); if (pendingScenario.isPresent()) { ScenarioController controller = pendingScenario.get().getScenarioController(); @@ -256,6 +245,7 @@ public class ScenariosExecutor { } public synchronized void notifyException(Thread t, Throwable e) { + logger.debug(() -> "Scenario executor uncaught exception: " + e.getMessage()); this.stoppingException = new RuntimeException("Error in scenario thread " + t.getName(), e); } diff --git a/engine-core/src/test/java/io/nosqlbench/engine/core/ScenarioTest.java b/engine-core/src/test/java/io/nosqlbench/engine/core/ScenarioTest.java index 62b2ba70a..2678f0719 100644 --- a/engine-core/src/test/java/io/nosqlbench/engine/core/ScenarioTest.java +++ b/engine-core/src/test/java/io/nosqlbench/engine/core/ScenarioTest.java @@ -19,19 +19,27 @@ package io.nosqlbench.engine.core; import io.nosqlbench.engine.api.scripting.ScriptEnvBuffer; import io.nosqlbench.engine.core.script.Scenario; import io.nosqlbench.nb.annotations.Maturity; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; public class ScenarioTest { + private final Logger logger = LogManager.getLogger(ScenarioTest.class); @Test public void shouldLoadScriptText() { ScriptEnvBuffer buffer = new ScriptEnvBuffer(); - Scenario env = new Scenario("testing", Scenario.Engine.Graalvm, "stdout:300", Maturity.Any); - env.addScriptText("print('loaded script environment...');\n"); - env.runScenario(); - assertThat(env.getIOLog().get().get(0)).contains("loaded script environment..."); + Scenario scenario = new Scenario("testing", Scenario.Engine.Graalvm, "stdout:300", Maturity.Any); + scenario.addScriptText("print('loaded script environment...');\n"); + try { + var result=scenario.call(); + } catch (Exception e) { + logger.debug("Scenario run encountered an exception: " + e.getMessage()); + + } + assertThat(scenario.getIOLog().get().get(0)).contains("loaded script environment..."); } } diff --git a/engine-rest/pom.xml b/engine-rest/pom.xml index c1b72cd64..25583745a 100644 --- a/engine-rest/pom.xml +++ b/engine-rest/pom.xml @@ -29,7 +29,7 @@ io.swagger.core.v3 swagger-models - 2.2.2 + 2.2.3 diff --git a/engine-rest/src/main/java/io/nosqlbench/engine/rest/resources/ScenarioExecutorEndpoint.java b/engine-rest/src/main/java/io/nosqlbench/engine/rest/resources/ScenarioExecutorEndpoint.java index 61cb7a79a..06a3f0d5e 100644 --- a/engine-rest/src/main/java/io/nosqlbench/engine/rest/resources/ScenarioExecutorEndpoint.java +++ b/engine-rest/src/main/java/io/nosqlbench/engine/rest/resources/ScenarioExecutorEndpoint.java @@ -41,6 +41,7 @@ import org.joda.time.format.DateTimeFormat; import java.io.CharArrayWriter; import java.io.PrintWriter; import java.util.*; +import java.util.concurrent.Future; @Service(value = WebServiceObject.class, selector = "scenario-executor") @Singleton @@ -233,8 +234,9 @@ public class ScenarioExecutorEndpoint implements WebServiceObject { Optional pendingScenario = executor.getPendingScenario(scenarioName); if (pendingScenario.isPresent()) { - Optional pendingResult = executor.getPendingResult(scenarioName); - return new LiveScenarioView(pendingScenario.get(), pendingResult.orElse(null)); + Optional> pendingResult = executor.getPendingResult(scenarioName); + Future scenarioResultFuture = pendingResult.get(); + return new LiveScenarioView(pendingScenario.get()); } else { throw new RuntimeException("Scenario name '" + scenarioName + "' not found."); } diff --git a/engine-rest/src/main/java/io/nosqlbench/engine/rest/transfertypes/LiveScenarioView.java b/engine-rest/src/main/java/io/nosqlbench/engine/rest/transfertypes/LiveScenarioView.java index 800e34717..bcd0e9a1f 100644 --- a/engine-rest/src/main/java/io/nosqlbench/engine/rest/transfertypes/LiveScenarioView.java +++ b/engine-rest/src/main/java/io/nosqlbench/engine/rest/transfertypes/LiveScenarioView.java @@ -19,7 +19,6 @@ package io.nosqlbench.engine.rest.transfertypes; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; import io.nosqlbench.engine.api.activityapi.core.progress.ProgressMeterDisplay; -import io.nosqlbench.engine.core.lifecycle.ScenarioResult; import io.nosqlbench.engine.core.script.Scenario; import java.util.ArrayList; @@ -29,21 +28,16 @@ import java.util.List; public class LiveScenarioView { private final Scenario scenario; - private final ScenarioResult result; - public LiveScenarioView(Scenario scenario, ScenarioResult result) { + public LiveScenarioView(Scenario scenario) { this.scenario = scenario; - this.result = result; } @JsonProperty @JsonPropertyDescription("Optionally populated result, "+ " present only if there was an error or the scenario is complete") public ResultView getResult() { - if (result==null) { - return null; - } - return new ResultView(result); + return new ResultView(scenario.getResultIfComplete().orElse(null)); } @JsonProperty("scenario_name") diff --git a/engine-rest/src/main/java/io/nosqlbench/engine/rest/transfertypes/ResultView.java b/engine-rest/src/main/java/io/nosqlbench/engine/rest/transfertypes/ResultView.java index 7bc8fe2de..1394ce7e6 100644 --- a/engine-rest/src/main/java/io/nosqlbench/engine/rest/transfertypes/ResultView.java +++ b/engine-rest/src/main/java/io/nosqlbench/engine/rest/transfertypes/ResultView.java @@ -27,14 +27,17 @@ public class ResultView { } public String getIOLog() { - return result.getIOLog(); - } - - public String getError() { - if (result.getException().isPresent()) { - return result.getException().get().getMessage(); + if (result!=null) { + return result.getIOLog(); } else { return ""; } } + + public String getError() { + if (result!=null && result.getException().isPresent()) { + return result.getException().get().getMessage(); + } + return ""; + } } diff --git a/mvn-defaults/pom.xml b/mvn-defaults/pom.xml index d48f66855..e0dcc4f7e 100644 --- a/mvn-defaults/pom.xml +++ b/mvn-defaults/pom.xml @@ -141,7 +141,7 @@ io.dropwizard.metrics metrics-core - 4.2.10 + 4.2.12 @@ -189,7 +189,7 @@ io.netty netty-handler - 4.1.81.Final + 4.1.82.Final @@ -271,7 +271,7 @@ com.google.code.gson gson - 2.9.0 + 2.9.1 diff --git a/nb-api/pom.xml b/nb-api/pom.xml index d2aea36ec..2e0631a8a 100644 --- a/nb-api/pom.xml +++ b/nb-api/pom.xml @@ -97,7 +97,7 @@ com.amazonaws aws-java-sdk-s3 - 1.12.325 + 1.12.340 diff --git a/engine-api/src/main/java/io/nosqlbench/engine/api/activityapi/core/Shutdownable.java b/nb-api/src/main/java/io/nosqlbench/engine/api/activityapi/core/Shutdownable.java similarity index 100% rename from engine-api/src/main/java/io/nosqlbench/engine/api/activityapi/core/Shutdownable.java rename to nb-api/src/main/java/io/nosqlbench/engine/api/activityapi/core/Shutdownable.java diff --git a/nb/pom.xml b/nb/pom.xml index 3cbffe028..43e93653e 100644 --- a/nb/pom.xml +++ b/nb/pom.xml @@ -67,6 +67,12 @@ 4.17.22-SNAPSHOT + + io.nosqlbench + adapter-pulsar + 4.17.22-SNAPSHOT + + diff --git a/nb5/pom.xml b/nb5/pom.xml index f2fda67a0..f801d40cd 100644 --- a/nb5/pom.xml +++ b/nb5/pom.xml @@ -88,6 +88,12 @@ 4.17.31-SNAPSHOT + + io.nosqlbench + adapter-pulsar + 4.17.31-SNAPSHOT + + diff --git a/nbr-examples/src/test/resources/scripts/examples/extension_histostatslogger.js b/nbr-examples/src/test/resources/scripts/examples/extension_histostatslogger.js index 377c1b357..50c99758b 100644 --- a/nbr-examples/src/test/resources/scripts/examples/extension_histostatslogger.js +++ b/nbr-examples/src/test/resources/scripts/examples/extension_histostatslogger.js @@ -18,16 +18,15 @@ activitydef = { "alias" : "testhistostatslogger", "driver" : "diag", "cycles" : "50000", - "threads" : "20", - "interval" : "2000", - "targetrate" : "10000.0", + "threads" : "5", + "rate" : "100.0", "op" : "noop" }; histostatslogger.logHistoStats("testing extention histostatslogger", ".*", "logs/histostats.csv", "0.5s"); print("started logging to logs/histostats.csv for all metrics at 1/2" + " second intervals."); - scenario.start(activitydef); -scenario.waitMillis(2000); +scenario.waitMillis(4000); scenario.stop(activitydef); + diff --git a/nbr/src/test/java/io/nosqlbench/cli/testing/ExitStatusIntegrationTests.java b/nbr/src/test/java/io/nosqlbench/cli/testing/ExitStatusIntegrationTests.java index f50704c62..70a163eaf 100644 --- a/nbr/src/test/java/io/nosqlbench/cli/testing/ExitStatusIntegrationTests.java +++ b/nbr/src/test/java/io/nosqlbench/cli/testing/ExitStatusIntegrationTests.java @@ -19,73 +19,85 @@ package io.nosqlbench.cli.testing; import org.junit.jupiter.api.Test; import java.util.Optional; -import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; -public class ExitStatusIntegrationTests { +class ExitStatusIntegrationTests { private final String java = Optional.ofNullable(System.getenv( - "JAVA_HOME")).map(v -> v+"/bin/java").orElse("java"); + "JAVA_HOME")).map(v -> v + "/bin/java").orElse("java"); private final static String JARNAME = "target/nbr.jar"; + @Test - public void testExitStatusOnBadParam() { + void testExitStatusOnBadParam() { ProcessInvoker invoker = new ProcessInvoker(); invoker.setLogDir("logs/test"); ProcessResult result = invoker.run("exitstatus_badparam", 15, java, "-jar", JARNAME, "--logs-dir", "logs/test/badparam/", - "badparam" + "badparam" ); assertThat(result.exception).isNull(); - String stderr = result.getStderrData().stream().collect(Collectors.joining("\n")); + String stderr = String.join("\n", result.getStderrData()); assertThat(stderr).contains("Scenario stopped due to error"); assertThat(result.exitStatus).isEqualTo(2); } @Test - public void testExitStatusOnActivityInitException() { + void testExitStatusOnActivityInitException() { ProcessInvoker invoker = new ProcessInvoker(); invoker.setLogDir("logs/test"); ProcessResult result = invoker.run("exitstatus_initexception", 15, java, "-jar", JARNAME, "--logs-dir", "logs/test/initerror", "run", - "driver=diag", "op=initdelay:initdelay=notanumber" + "driver=diag", "op=initdelay:initdelay=notanumber" ); assertThat(result.exception).isNull(); - String stderr = result.getStdoutData().stream().collect(Collectors.joining("\n")); + String stderr = String.join("\n", result.getStdoutData()); assertThat(stderr).contains("For input string: \"notanumber\""); assertThat(result.exitStatus).isEqualTo(2); } -// Temporarily disabled for triage -// TODO: figure out if github actions is an issue for this test. -// It passes locally, but fails spuriously in github actions runner -// @Test -// public void testExitStatusOnActivityThreadException() { -// ProcessInvoker invoker = new ProcessInvoker(); -// invoker.setLogDir("logs/test"); -// ProcessResult result = invoker.run("exitstatus_threadexception", 30, -// "java", "-jar", JARNAME, "--logs-dir", "logs/test", "run", "driver=diag", "throwoncycle=10", "cycles=1000", "cyclerate=10", "-vvv" -// ); -// String stdout = result.getStdoutData().stream().collect(Collectors.joining("\n")); -// assertThat(stdout).contains("Diag was asked to throw an error on cycle 10"); -// assertThat(result.exitStatus).isEqualTo(2); -// } + @Test + void testExitStatusOnActivityBasicCommandException() { + ProcessInvoker invoker = new ProcessInvoker(); + invoker.setLogDir("logs/test"); + + // Forcing a thread exception via basic command issue. + ProcessResult result = invoker.run("exitstatus_threadexception", 30, + "java", "-jar", JARNAME, "--logs-dir", "logs/test/threadexcep", "--logs-level", "debug", "run", + "driver=diag", "cyclerate=10", "not_a_thing", "cycles=100", "-vvv" + ); + String stdout = String.join("\n", result.getStdoutData()); + assertThat(stdout).contains("Could not recognize command"); + assertThat(result.exitStatus).isEqualTo(2); + } @Test - public void testExitStatusOnActivityOpException() { + void testExitStatusOnActivityOpException() { ProcessInvoker invoker = new ProcessInvoker(); invoker.setLogDir("logs/test"); ProcessResult result = invoker.run("exitstatus_asyncstoprequest", 30, - java, "-jar", JARNAME, "--logs-dir", "logs/test/asyncstop", "run", - "driver=diag", "cyclerate=5", "op=erroroncycle:erroroncycle=10", "cycles=2000", "-vvv" + "java", "-jar", JARNAME, "--logs-dir", "logs/test/asyncstop", "--logs-level", "debug", "run", + "driver=diag", "threads=2", "cyclerate=10", "op=erroroncycle:erroroncycle=10", "cycles=500", "-vvv" ); assertThat(result.exception).isNull(); - String stdout = result.getStdoutData().stream().collect(Collectors.joining("\n")); + String stdout = String.join("\n", result.getStdoutData()); assertThat(stdout).contains("Diag was requested to stop on cycle 10"); assertThat(result.exitStatus).isEqualTo(2); } - - +// This will not work reliablyl until the activity shutdown bug is fixed. +// @Test +// public void testCloseErrorHandlerOnSpace() { +// ProcessInvoker invoker = new ProcessInvoker(); +// invoker.setLogDir("logs/test"); +// ProcessResult result = invoker.run("exitstatus_erroronclose", 30, +// java, "-jar", JARNAME, "--logs-dir", "logs/test/error_on_close", "run", +// "driver=diag", "threads=2", "rate=5", "op=noop", "cycles=10", "erroronclose=true", "-vvv" +// ); +// String stdout = String.join("\n", result.getStdoutData()); +// String stderr = String.join("\n", result.getStderrData()); +// assertThat(result.exception).isNotNull(); +// assertThat(result.exception.getMessage()).contains("diag space was configured to throw"); +// } } diff --git a/nbr/src/test/java/io/nosqlbench/cli/testing/ProcessInvoker.java b/nbr/src/test/java/io/nosqlbench/cli/testing/ProcessInvoker.java index 1432c0b95..43e205033 100644 --- a/nbr/src/test/java/io/nosqlbench/cli/testing/ProcessInvoker.java +++ b/nbr/src/test/java/io/nosqlbench/cli/testing/ProcessInvoker.java @@ -16,10 +16,16 @@ package io.nosqlbench.cli.testing; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + import java.io.File; import java.util.concurrent.TimeUnit; public class ProcessInvoker { + + private static final Logger logger = LogManager.getLogger(ProcessInvoker.class); + private File runDirectory = new File("."); private File logDirectory = new File("."); @@ -49,13 +55,17 @@ public class ProcessInvoker { try { result.cmdDir = new File(".").getCanonicalPath(); process = pb.start(); - + var handle = process.toHandle(); boolean terminated = process.waitFor(timeoutSeconds, TimeUnit.SECONDS); if (!terminated) { process.destroyForcibly().waitFor(); result.exception = new RuntimeException("timed out waiting for process, so it was shutdown forcibly."); } + } catch (Exception e) { + if (process != null) { + logger.debug("Exception received, with exit value: " + process.exitValue()); + } result.exception = e; } finally { result.startNanosTime = startNanosTime; @@ -66,7 +76,7 @@ public class ProcessInvoker { if (process != null) { result.exitStatus = process.exitValue(); } else { - result.exitStatus=255; + result.exitStatus = 255; } } return result; diff --git a/pom.xml b/pom.xml index 9bb917de6..7eac71c2c 100644 --- a/pom.xml +++ b/pom.xml @@ -61,6 +61,7 @@ adapter-tcp adapter-dynamodb adapter-mongodb + adapter-pulsar @@ -91,7 +92,6 @@ driver-jmx driver-jdbc driver-cockroachdb - driver-pulsar