diff --git a/.github/scripts/benchmark.sh b/.github/scripts/benchmark.sh
index 917af516..832c63db 100755
--- a/.github/scripts/benchmark.sh
+++ b/.github/scripts/benchmark.sh
@@ -110,7 +110,7 @@ BM4b="ArjunaJTA/jta io.narayana.perf.product.GeronimoComparison.* 1"
BM4c="ArjunaJTA/jta io.narayana.perf.product.NarayanaComparison.* 1"
BM4d="ArjunaJTA/jta io.narayana.perf.product.AtomikosComparison.* 1"
#ArjunaJTA/jta/tests/classes/io/narayana/perf/product/ProductComparison.java
-BM5="ArjunaJTA/jta com.arjuna.ats.jta.xa.performance.*StoreBenchmark.* 5"
+BM5="ArjunaJTA/jta com.arjuna.ats.jta.xa.performance.*StoreBenchmark.* 9"
#ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/HQStoreBenchmark.java
#ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/VolatileStoreBenchmark.java
#ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/ShadowNoFileLockStoreBenchmark.java
diff --git a/narayana/ArjunaJTA/jta/etc/jgroups-raft.xml b/narayana/ArjunaJTA/jta/etc/jgroups-raft.xml
new file mode 100644
index 00000000..dca1789f
--- /dev/null
+++ b/narayana/ArjunaJTA/jta/etc/jgroups-raft.xml
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/narayana/ArjunaJTA/jta/etc/jgroups.xml b/narayana/ArjunaJTA/jta/etc/jgroups.xml
new file mode 100644
index 00000000..d2edf45c
--- /dev/null
+++ b/narayana/ArjunaJTA/jta/etc/jgroups.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/narayana/ArjunaJTA/jta/pom.xml b/narayana/ArjunaJTA/jta/pom.xml
index 219a3461..8ac45ea7 100644
--- a/narayana/ArjunaJTA/jta/pom.xml
+++ b/narayana/ArjunaJTA/jta/pom.xml
@@ -43,6 +43,7 @@
6.0.0
6.0.0
2.1.4
+ 1.1.4.Final
1.0.9
1.0.16
@@ -380,6 +381,12 @@
jcommon
${version.jfreecommon}
+
+ org.jgroups
+ jgroups-raft
+ ${version.org.jgroups.raft}
+ true
+
diff --git a/narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JGroupsRaftClusterStoreBenchmark.java b/narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JGroupsRaftClusterStoreBenchmark.java
new file mode 100644
index 00000000..bf9082ed
--- /dev/null
+++ b/narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JGroupsRaftClusterStoreBenchmark.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright The Narayana Authors
+ * SPDX short identifier: Apache-2.0
+ */
+
+package com.arjuna.ats.jta.xa.performance;
+
+import com.arjuna.ats.internal.arjuna.objectstore.slot.jgroups.JGroupsRaftSlots;
+import com.arjuna.ats.internal.arjuna.objectstore.slot.jgroups.JGroupsRaftStoreEnvironmentBean;
+import org.jgroups.JChannel;
+import org.jgroups.protocols.raft.RAFT;
+import org.jgroups.protocols.raft.Role;
+import org.jgroups.raft.blocks.ReplicatedStateMachine;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.infra.Blackhole;
+import org.openjdk.jmh.runner.Runner;
+import org.openjdk.jmh.runner.RunnerException;
+import org.openjdk.jmh.runner.options.Options;
+import org.openjdk.jmh.runner.options.OptionsBuilder;
+import org.openjdk.jmh.runner.options.TimeValue;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Multi-node Raft cluster benchmark measuring the impact of {@code allowDirtyReads}.
+ *
+ * Creates a 3-node Raft cluster in-process (via SHARED_LOOPBACK), writes pre-populated
+ * data through the leader, then benchmarks reads from a follower node. With dirty reads
+ * enabled, reads hit the follower's local in-memory map; with dirty reads disabled,
+ * reads go through the Raft leader via consensus.
+ *
+ * The benchmark measures raw slot I/O (slots.read() and slots.write()) with no transaction overhead.
+ * The *StoreBenchmark classes measure end-to-end transaction throughput through the full JTA stack.
+ * Consequently one should expect significant performance difference between the benchmarks.
+ *
+ * Run: {@code java -jar benchmarks.jar "JGroupsRaftClusterStoreBenchmark" -t 10 -f 1 -i 5 -wi 2 -r 3}
+ */
+@State(Scope.Benchmark)
+public class JGroupsRaftClusterStoreBenchmark {
+
+ private static final String STORE_BASE_DIR = System.getProperty("user.dir") + "/target/jgroups-raft-cluster-benchmark";
+ private static final String[] NODE_NAMES = {"node1", "node2", "node3"};
+ private static final String RAFT_MEMBERS = String.join(",", NODE_NAMES);
+ private static final int NUM_SLOTS = 256;
+ private static final int PREPOPULATE_SLOTS = 64;
+ private static final byte[] SLOT_DATA = "benchmark-data-payload".getBytes();
+
+ @Param({"true", "false"})
+ private boolean allowDirtyReads;
+
+ private JChannel[] channels;
+ private JGroupsRaftSlots[] allSlots;
+ private JGroupsRaftSlots leaderSlots;
+ private JGroupsRaftSlots followerSlots;
+
+ private final AtomicInteger readCounter = new AtomicInteger(0);
+ private final AtomicInteger writeCounter = new AtomicInteger(0);
+
+ public static void main(String[] args) throws RunnerException {
+ Options opt = new OptionsBuilder()
+ .include(JGroupsRaftClusterStoreBenchmark.class.getSimpleName())
+ .timeUnit(TimeUnit.SECONDS)
+ .threads(10)
+ .forks(1)
+ .mode(Mode.Throughput)
+ .warmupIterations(2)
+ .warmupTime(TimeValue.seconds(2))
+ .measurementIterations(5)
+ .measurementTime(TimeValue.seconds(3))
+ .shouldDoGC(true)
+ .build();
+
+ new Runner(opt).run();
+ }
+
+ @Setup(Level.Trial)
+ @SuppressWarnings("unchecked")
+ public void setup() throws Exception {
+ purgeDirectory(new File(STORE_BASE_DIR));
+
+ String clusterName = "raft-cluster-bm-" + System.currentTimeMillis();
+ channels = new JChannel[3];
+ allSlots = new JGroupsRaftSlots[3];
+ ReplicatedStateMachine[] stateMachines = new ReplicatedStateMachine[3];
+ JGroupsRaftStoreEnvironmentBean[] configs = new JGroupsRaftStoreEnvironmentBean[3];
+
+ // Phase 1: Create and connect all channels before init — Raft needs a quorum
+ // for leader election, so all nodes must be connected before any can elect.
+ for (int i = 0; i < 3; i++) {
+ String storeDir = STORE_BASE_DIR + "/" + NODE_NAMES[i];
+ channels[i] = new JChannel("jgroups-raft.xml").name(NODE_NAMES[i]);
+
+ RAFT raft = channels[i].getProtocolStack().findProtocol(RAFT.class);
+ raft.logDir(storeDir);
+ raft.logUseFsync(false);
+ raft.members(Arrays.asList(RAFT_MEMBERS.split(",")));
+
+ stateMachines[i] = new ReplicatedStateMachine<>(channels[i]);
+ stateMachines[i].raftId(NODE_NAMES[i]);
+ stateMachines[i].allowDirtyReads(allowDirtyReads);
+ stateMachines[i].timeout(5000);
+
+ channels[i].connect(clusterName);
+ }
+
+ // Wait for leader election across the cluster
+ waitForLeader(channels, 10_000);
+
+ // Phase 2: Create JGroupsRaftSlots with pre-configured channels
+ for (int i = 0; i < 3; i++) {
+ configs[i] = new JGroupsRaftStoreEnvironmentBean();
+ configs[i].setJGroupsConfigFileName("jgroups-raft.xml");
+ configs[i].setNodeAddress(NODE_NAMES[i]);
+ configs[i].setClusterName(clusterName);
+ configs[i].setCacheName(clusterName);
+ configs[i].setStoreDir(STORE_BASE_DIR + "/" + NODE_NAMES[i]);
+ configs[i].setNumberOfSlots(NUM_SLOTS);
+ configs[i].setRaftMembers(RAFT_MEMBERS);
+ configs[i].setRaftLogFsync(false);
+ configs[i].setRaftTimeout(5000);
+ configs[i].setAllowDirtyReads(allowDirtyReads);
+ configs[i].setPreConfiguredChannel(channels[i]);
+ configs[i].setPreConfiguredStateMachine(stateMachines[i]);
+
+ allSlots[i] = new JGroupsRaftSlots();
+ allSlots[i].init(configs[i]);
+ }
+
+ // Identify leader and a follower
+ for (JGroupsRaftSlots slots : allSlots) {
+ String role = slots.getRole();
+ if (Role.Leader.name().equals(role) && leaderSlots == null) {
+ leaderSlots = slots;
+ } else if (Role.Follower.name().equals(role) && followerSlots == null) {
+ followerSlots = slots;
+ }
+ }
+
+ if (leaderSlots == null || followerSlots == null) {
+ throw new IllegalStateException("Cluster did not form correctly: leader=" + leaderSlots + " follower=" + followerSlots);
+ }
+
+ // Pre-populate slots via the leader
+ for (int i = 0; i < PREPOPULATE_SLOTS; i++) {
+ leaderSlots.write(i, SLOT_DATA, true);
+ }
+
+ // Brief pause for replication to followers
+ TimeUnit.MILLISECONDS.sleep(500);
+ }
+
+ @TearDown(Level.Trial)
+ public void tearDown() {
+ if (channels != null) {
+ for (JChannel ch : channels) {
+ if (ch != null) {
+ try {
+ ch.close();
+ } catch (Exception ignore) {
+ }
+ }
+ }
+ }
+
+ purgeDirectory(new File(STORE_BASE_DIR));
+ }
+
+ @Benchmark
+ public void readFromFollower(Blackhole bh) throws IOException {
+ int slotId = readCounter.getAndIncrement() % PREPOPULATE_SLOTS;
+ bh.consume(followerSlots.read(slotId));
+ }
+
+ @Benchmark
+ public void writeToLeader(Blackhole bh) throws IOException {
+ int slotId = writeCounter.getAndIncrement() % NUM_SLOTS;
+ leaderSlots.write(slotId, SLOT_DATA, true);
+ }
+
+ private static void waitForLeader(JChannel[] channels, long timeoutMs) throws InterruptedException {
+ long deadline = System.currentTimeMillis() + timeoutMs;
+ while (System.currentTimeMillis() < deadline) {
+ for (JChannel ch : channels) {
+ RAFT raft = ch.getProtocolStack().findProtocol(RAFT.class);
+ if (raft != null && raft.leader() != null) {
+ return;
+ }
+ }
+ TimeUnit.MILLISECONDS.sleep(50);
+ }
+ throw new IllegalStateException("No Raft leader elected within " + timeoutMs + "ms");
+ }
+
+ private static void purgeDirectory(File dir) {
+ if (dir.exists()) {
+ File[] files = dir.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ purgeDirectory(file);
+ }
+ }
+ dir.delete();
+ }
+ }
+}
diff --git a/narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JGroupsRaftSlotsStoreBenchmark.java b/narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JGroupsRaftSlotsStoreBenchmark.java
new file mode 100644
index 00000000..279c59d7
--- /dev/null
+++ b/narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JGroupsRaftSlotsStoreBenchmark.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright The Narayana Authors
+ * SPDX short identifier: Apache-2.0
+ */
+
+package com.arjuna.ats.jta.xa.performance;
+
+import com.arjuna.ats.arjuna.common.CoreEnvironmentBeanException;
+import com.arjuna.ats.arjuna.objectstore.StoreManager;
+import com.arjuna.ats.internal.arjuna.objectstore.slot.SlotStoreAdaptor;
+import com.arjuna.ats.internal.arjuna.objectstore.slot.SlotStoreEnvironmentBean;
+import com.arjuna.ats.internal.arjuna.objectstore.slot.jgroups.JGroupsRaftSlots;
+import com.arjuna.ats.internal.arjuna.objectstore.slot.jgroups.JGroupsRaftStoreEnvironmentBean;
+import com.arjuna.common.internal.util.propertyservice.BeanPopulator;
+import jakarta.transaction.HeuristicMixedException;
+import jakarta.transaction.HeuristicRollbackException;
+import jakarta.transaction.NotSupportedException;
+import jakarta.transaction.RollbackException;
+import jakarta.transaction.SystemException;
+import org.junit.BeforeClass;
+import org.openjdk.jmh.annotations.AuxCounters;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.infra.Blackhole;
+import org.openjdk.jmh.profile.JavaFlightRecorderProfiler;
+import org.openjdk.jmh.runner.Runner;
+import org.openjdk.jmh.runner.RunnerException;
+import org.openjdk.jmh.runner.options.Options;
+import org.openjdk.jmh.runner.options.OptionsBuilder;
+import org.openjdk.jmh.runner.options.TimeValue;
+
+import java.nio.file.Paths;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * JMH Benchmark for the JGroupsRaftSlots store (Raft consensus with a persistent write-ahead log).
+ * The benchmark uses a single-node Raft cluster for performance testing (in production, Raft requires 3 or more nodes)
+ *
Run from IDE: Run main() method
+ *
Run from command line: See performance/README.md e.g.
+ *
+ * java -jar narayana/ArjunaJTA/jta/target/benchmarks.jar \
+ * "JGroupsRaftSlotsStoreBenchmark|JGroupsSlotsStoreBenchmark|HQStoreBenchmark|ShadowNoFileLockStoreBenchmark" \
+ * -t 10 -f 1 -wi 3 -i 5 -r 10
+ *
+ * which produces:
+ * 3 warmup iterations (lets JIT stabilize), 5 measurement iterations (more data points), 10-second windows (I/O stalls average out).
+ *
It takes a bit longer (~4 minutes on a typical development machine) but the error margins drop to
+ * single-digit percentages of the score.
+ *
The quickest run, for these fsync-heavy benchmarks, that still produces tolerable variance is -wi 2 -i 5 -r 5
+ */
+@State(Scope.Benchmark)
+public class JGroupsRaftSlotsStoreBenchmark extends JTAStoreBase {
+ static final int THREADS = 240;
+ static final String BM_CLASS_NAME = JGroupsRaftSlotsStoreBenchmark.class.getSimpleName();
+
+ static final int FORKS = 1;
+ static final int ITERATIONS = 5;
+ static final int TIME_PER_ITER = 2;
+
+ public static void main(String[] args) throws RunnerException {
+ // Sometimes it is useful to run the benchmark directly from an IDE:
+ Options opt = new OptionsBuilder()
+ .include(BM_CLASS_NAME + ".testJGroupsRaftSlotsStore")
+ .timeUnit(TimeUnit.SECONDS)
+ .threads(THREADS)
+ .forks(FORKS)
+ .mode(Mode.Throughput)
+ .warmupIterations(1)
+ .warmupTime(TimeValue.seconds(1))
+ .measurementIterations(ITERATIONS)
+ .measurementTime(TimeValue.seconds(TIME_PER_ITER))
+ .shouldDoGC(true)
+ // use JFR as the profiler, the recording will appear in the User working directory with the
+ // name "-xxx/profile.jfr", which you can change to "wherever" using
+ // addProfiler(JavaFlightRecorderProfiler.class, "dir=wherever"). Java Flight Recorder data files
+ // can be viewed with the jmc graphical tool or with the jfr command line tool which is in the java
+ // bin directory
+ .addProfiler(JavaFlightRecorderProfiler.class)
+ .jvmArgs("-Djmh.executor=FJP") // ForkJoinPool
+ // to debug the forks use "-agentlib:jdwp=transport=dt_socket,address=5005,server=y,suspend=y"
+ .build();
+
+ new Runner(opt).run();
+ }
+
+ @Setup(Level.Trial)
+ @BeforeClass
+ public static void setup() throws CoreEnvironmentBeanException {
+ JTAStoreBase.setup(SlotStoreAdaptor.class.getName());
+ JGroupsRaftStoreEnvironmentBean configBean = BeanPopulator.getDefaultInstance(JGroupsRaftStoreEnvironmentBean.class);
+ SlotStoreEnvironmentBean slotStoreBean = BeanPopulator.getDefaultInstance(SlotStoreEnvironmentBean.class);
+ slotStoreBean.setBackingSlotsClassName(JGroupsRaftSlots.class.getName());
+ int threadCount = getThreadCountFromProperties(THREADS);
+ // Raft serializes all writes through a single consensus thread, so very high thread
+ // counts produce slot exhaustion rather than meaningful throughput data.
+ // Cap the slot allocation to THREADS to keep the benchmark stable at all -t (no of worker threads) values.
+ int effectiveThreadCount = Math.min(threadCount, THREADS);
+
+ // JGroups configuration
+ configBean.setJGroupsConfigFileName("jgroups-raft.xml");
+ configBean.setNodeAddress("raft-benchmark-node"); // TODO configure 3 node cluster
+ configBean.setClusterName("jgroups-raft-benchmark-" + System.currentTimeMillis());
+
+ // Note: Using default key generator (Uid-based) for single-node benchmark
+
+ // Raft configuration (single-node cluster for benchmark)
+ configBean.setRaftMembers("raft-benchmark-node"); // Single node
+ configBean.setRaftLogFsync(true); // durable writes for fault tolerance
+ configBean.setAllowDirtyReads(false);
+
+ configBean.setRecycleFailedSlots(true); // otherwise failed slot writes will lead to slot exhaustion
+
+ // SlotStoreAdaptor creates the SlotStore using the base SlotStoreEnvironmentBean,
+ // so numberOfSlots must be set there (not just on the JGroups config bean)
+ int numberOfSlots = roundUp(256, effectiveThreadCount);
+ slotStoreBean.setNumberOfSlots(numberOfSlots);
+ configBean.setNumberOfSlots(numberOfSlots);
+ configBean.setBackingSlotsClassName(JGroupsRaftSlots.class.getName());
+
+ cleanStore(Paths.get(configBean.getStoreDir()).toFile());
+ }
+
+ @TearDown(Level.Trial)
+ public static void tearDown() {
+ StoreManager.shutdown();
+ JGroupsRaftStoreEnvironmentBean configBean = BeanPopulator.getDefaultInstance(JGroupsRaftStoreEnvironmentBean.class);
+ cleanStore(Paths.get(configBean.getStoreDir()).toFile());
+ }
+
+ @AuxCounters(AuxCounters.Type.OPERATIONS)
+ @State(Scope.Thread)
+ /*
+ TxnCounters is a JMH @AuxCounters class that breaks the benchmark's throughput into two separate metrics in the JMH output:
+ - committed — incremented when jtaTest() succeeds (transaction committed)
+ - rolledBack — incremented when jtaTest() throws RollbackException (slot exhaustion under Raft contention)
+
+ JMH treats both fields as operation counts and reports them as separate ops/sec columns alongside the main throughput metric.
+ This lets you see at a glance how much of the reported throughput is real committed work versus failed transactions
+ (important for Raft at high thread counts where many transactions roll back due to slot exhaustion).
+
+ An example from a run output with "-t 10 -f 2 -wi 3 -i 10 -r 10" shows no rolledBack counts
+ (the Cnt column is the number of data points, forks (-f) x measurement iterations (-i)):
+
+ Benchmark Mode Cnt Score Error Units
+ JGroupsRaftSlotsStoreBenchmark.testJGroupsRaftSlotsStore thrpt 20 1780.754 ± 276.787 ops/s
+ JGroupsRaftSlotsStoreBenchmark.testJGroupsRaftSlotsStore:committed thrpt 20 1782.559 ± 276.783 ops/s
+ JGroupsRaftSlotsStoreBenchmark.testJGroupsRaftSlotsStore:rolledBack thrpt 20 ≈ 0 ops/s
+
+ Without @AuxCounters, catching RollbackException silently would inflate the throughput number (JMH counts every benchmark method invocation as one operation, whether it committed or not). With them, you get
+ three columns: total ops/sec, committed ops/sec, and rolledBack ops/sec.
+ */
+ public static class TxnCounters {
+ public long committed;
+ public long rolledBack;
+ }
+
+ @Benchmark
+ public void testJGroupsRaftSlotsStore(Blackhole bh, TxnCounters counters) throws HeuristicRollbackException, SystemException, HeuristicMixedException, NotSupportedException {
+ try {
+ bh.consume(super.jtaTest());
+ counters.committed++;
+ } catch (RollbackException e) {
+ counters.rolledBack++;
+ }
+ }
+}
diff --git a/narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JGroupsSlotsStoreBenchmark.java b/narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JGroupsSlotsStoreBenchmark.java
new file mode 100644
index 00000000..b6f7a359
--- /dev/null
+++ b/narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JGroupsSlotsStoreBenchmark.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright The Narayana Authors
+ * SPDX short identifier: Apache-2.0
+ */
+
+package com.arjuna.ats.jta.xa.performance;
+
+import com.arjuna.ats.arjuna.common.CoreEnvironmentBeanException;
+import com.arjuna.ats.arjuna.objectstore.StoreManager;
+import com.arjuna.ats.internal.arjuna.objectstore.slot.SlotStoreAdaptor;
+import com.arjuna.ats.internal.arjuna.objectstore.slot.SlotStoreEnvironmentBean;
+import com.arjuna.ats.internal.arjuna.objectstore.slot.jgroups.JGroupsSlots;
+import com.arjuna.ats.internal.arjuna.objectstore.slot.jgroups.JGroupsStoreEnvironmentBean;
+import com.arjuna.common.internal.util.propertyservice.BeanPopulator;
+import jakarta.transaction.HeuristicMixedException;
+import jakarta.transaction.HeuristicRollbackException;
+import jakarta.transaction.NotSupportedException;
+import jakarta.transaction.RollbackException;
+import jakarta.transaction.SystemException;
+import org.junit.BeforeClass;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.infra.Blackhole;
+import org.openjdk.jmh.profile.JavaFlightRecorderProfiler;
+import org.openjdk.jmh.runner.Runner;
+import org.openjdk.jmh.runner.RunnerException;
+import org.openjdk.jmh.runner.options.Options;
+import org.openjdk.jmh.runner.options.OptionsBuilder;
+import org.openjdk.jmh.runner.options.TimeValue;
+
+import java.nio.file.Paths;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * JMH Benchmark for the JGroupsSlots store (with a persistent write-ahead log).
+ * The benchmark uses a single-node cluster for performance testing
+ *
Run from IDE: Run main() method
+ *
Run from command line: See performance/README.md
+ */
+@State(Scope.Benchmark)
+public class JGroupsSlotsStoreBenchmark extends JTAStoreBase {
+ static final int THREADS = 240;
+ static final String BM_CLASS_NAME = JGroupsSlotsStoreBenchmark.class.getSimpleName();
+
+ static final int FORKS = 1;
+ static final int ITERATIONS = 5;
+ static final int TIME_PER_ITER = 2;
+
+ public static void main(String[] args) throws RunnerException {
+ // Sometimes it is useful to run the benchmark directly from an IDE:
+ Options opt = new OptionsBuilder()
+ .include(BM_CLASS_NAME + ".testJGroupsSlotsStore")
+ .timeUnit(TimeUnit.SECONDS)
+ .threads(THREADS)
+ .forks(FORKS)
+ .mode(Mode.Throughput)
+ .warmupIterations(1)
+ .warmupTime(TimeValue.seconds(1))
+ .measurementIterations(ITERATIONS)
+ .measurementTime(TimeValue.seconds(TIME_PER_ITER))
+ .shouldDoGC(true)
+ // use JFR as the profiler, the recording will appear in the User working directory with the
+ // name "-xxx/profile.jfr", which you can change to "wherever" using
+ // addProfiler(JavaFlightRecorderProfiler.class, "dir=wherever"). Java Flight Recorder data files
+ // can be viewed with the jmc graphical tool or with the jfr command line tool which is in the java
+ // bin directory
+ .addProfiler(JavaFlightRecorderProfiler.class)
+ .jvmArgs("-Djmh.executor=FJP") // ForkJoinPool
+ // to debug the forks use "-agentlib:jdwp=transport=dt_socket,address=5005,server=y,suspend=y"
+ .build();
+
+ new Runner(opt).run();
+ }
+
+ @Setup(Level.Trial)
+ @BeforeClass
+ public static void setup() throws CoreEnvironmentBeanException {
+ JTAStoreBase.setup(SlotStoreAdaptor.class.getName());
+ JGroupsStoreEnvironmentBean configBean = BeanPopulator.getDefaultInstance(JGroupsStoreEnvironmentBean.class);
+ SlotStoreEnvironmentBean slotStoreBean = BeanPopulator.getDefaultInstance(SlotStoreEnvironmentBean.class);
+ slotStoreBean.setBackingSlotsClassName(JGroupsSlots.class.getName());
+ int threadCount = getThreadCountFromProperties(THREADS);
+
+ // JGroups configuration
+ configBean.setJGroupsConfigFileName("jgroups.xml");
+ configBean.setNodeAddress("benchmark-node");
+ configBean.setClusterName("jgroups-slots-benchmark-" + System.currentTimeMillis());
+ configBean.setReplicationCount((short) -1); // Full replication
+
+ // write-ahead log configuration
+ configBean.setWalEnabled(true); // set to true for durability of transaction logs
+ configBean.setWalSyncWrites(true); // set to true for maximum durability
+ configBean.setWalSyncDeletes(false);
+
+ configBean.setRecycleFailedSlots(true); // otherwise failed slot writes will lead to slot exhaustion
+
+ configBean.setWalAsyncIO(false); // requires libaio to be on the LD_LIBRARY_PATH
+
+ // L2 cache configuration (disabled for consistency)
+ configBean.setCachingTime(0L);
+
+ // SlotStoreAdaptor creates the SlotStore using the base SlotStoreEnvironmentBean,
+ // so numberOfSlots must be set there (not just on the JGroups config bean)
+ int numberOfSlots = roundUp(256, threadCount);
+ slotStoreBean.setNumberOfSlots(numberOfSlots);
+ configBean.setNumberOfSlots(numberOfSlots);
+ configBean.setBackingSlotsClassName(JGroupsSlots.class.getName());
+
+ cleanStore(Paths.get(configBean.getStoreDir()).toFile());
+ }
+
+ @TearDown(Level.Trial)
+ public static void tearDown() {
+ StoreManager.shutdown();
+ JGroupsStoreEnvironmentBean configBean = BeanPopulator.getDefaultInstance(JGroupsStoreEnvironmentBean.class);
+ cleanStore(Paths.get(configBean.getStoreDir()).toFile());
+ }
+
+ @Benchmark
+ public void testJGroupsSlotsStore(Blackhole bh) throws HeuristicRollbackException, SystemException, HeuristicMixedException, NotSupportedException, RollbackException {
+ bh.consume(super.jtaTest());
+ }
+}
diff --git a/narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JTAStoreBase.java b/narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JTAStoreBase.java
index d8cf6d92..ea5eefd4 100644
--- a/narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JTAStoreBase.java
+++ b/narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JTAStoreBase.java
@@ -83,11 +83,11 @@ protected static boolean purgeFiles(File storeDir) {
protected static int roundUp(int increment, int value) {
int res = value % increment;
- // round up to nearest multiple of increment (eg a h/w page size)
+ // round up to nearest multiple of increment with one page of headroom
if (res == 0) {
- return value;
+ return value + increment;
} else {
- return value + increment - res;
+ return ((value + increment) - res) + increment;
}
}
@@ -101,18 +101,19 @@ protected static void setup(String storeType) throws CoreEnvironmentBeanExceptio
}
public boolean jtaTest() throws HeuristicRollbackException, SystemException, HeuristicMixedException, NotSupportedException, RollbackException {
+ tm.begin();
try {
- tm.begin();
-
tm.getTransaction().enlistResource(resource1);
tm.getTransaction().enlistResource(resource2);
-
tm.commit();
- } catch(Exception e) {
- log.fatal("JTAStoreTests#jtaTest%n", e);
- throw new Error(e);
+ } catch (Exception e) {
+ try {
+ tm.rollback();
+ } catch (Exception suppressed) {
+ e.addSuppressed(suppressed);
+ }
+ throw e;
}
-
return true;
}
}