From cdbb96e8eb5ca6915181fb0ee5abff42972e52b1 Mon Sep 17 00:00:00 2001 From: Michael Musgrove Date: Sat, 18 Jul 2026 18:54:30 +0100 Subject: [PATCH 1/7] JBTM-4038 JGroupsSlots and JGroupsRaftSlots benchmarks --- .github/scripts/benchmark.sh | 2 +- narayana/ArjunaJTA/jta/etc/jgroups-raft.xml | 88 +++++++++++++ narayana/ArjunaJTA/jta/etc/jgroups.xml | 16 +++ narayana/ArjunaJTA/jta/pom.xml | 7 + .../JGroupsRaftSlotsStoreBenchmark.java | 118 +++++++++++++++++ .../JGroupsSlotsStoreBenchmark.java | 122 ++++++++++++++++++ 6 files changed, 352 insertions(+), 1 deletion(-) create mode 100644 narayana/ArjunaJTA/jta/etc/jgroups-raft.xml create mode 100644 narayana/ArjunaJTA/jta/etc/jgroups.xml create mode 100644 narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JGroupsRaftSlotsStoreBenchmark.java create mode 100644 narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JGroupsSlotsStoreBenchmark.java diff --git a/.github/scripts/benchmark.sh b/.github/scripts/benchmark.sh index 917af516..3eb4e76b 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.* 7" #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..aeab4740 --- /dev/null +++ b/narayana/ArjunaJTA/jta/etc/jgroups-raft.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/JGroupsRaftSlotsStoreBenchmark.java b/narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JGroupsRaftSlotsStoreBenchmark.java new file mode 100644 index 00000000..571a5605 --- /dev/null +++ b/narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JGroupsRaftSlotsStoreBenchmark.java @@ -0,0 +1,118 @@ +/* + * 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.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 + */ +@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); + BeanPopulator.getDefaultInstance(SlotStoreEnvironmentBean.class).setBackingSlotsClassName(JGroupsRaftSlots.class.getName()); + int threadCount = getThreadCountFromProperties(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); + + // Set the slot size, making sure it's a sensible multiple + configBean.setNumberOfSlots(roundUp(256, threadCount)); + 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()); + } + + @Benchmark + public void testJGroupsRaftSlotsStore(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/JGroupsSlotsStoreBenchmark.java b/narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JGroupsSlotsStoreBenchmark.java new file mode 100644 index 00000000..7a9ca0db --- /dev/null +++ b/narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JGroupsSlotsStoreBenchmark.java @@ -0,0 +1,122 @@ +/* + * 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); + BeanPopulator.getDefaultInstance(SlotStoreEnvironmentBean.class).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.setWalAsyncIO(false); // requires libaio to be on the LD_LIBRARY_PATH + + // L2 cache configuration (disabled for consistency) + configBean.setCachingTime(0L); + + // Set the slot size, making sure it's a sensible multiple + configBean.setNumberOfSlots(roundUp(256, threadCount)); + 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()); + } +} From 7d64adfc4ae2a81bb11707f60c85e11cb38fa387 Mon Sep 17 00:00:00 2001 From: Michael Musgrove Date: Mon, 20 Jul 2026 23:02:43 +0100 Subject: [PATCH 2/7] JBTM-4038 Benchmark the effects of allowDirtyReads and a real raft cluster of JGroupsRaftSlots (using SHARED_LOOPBACK) --- .github/scripts/benchmark.sh | 2 +- narayana/ArjunaJTA/jta/etc/jgroups-raft.xml | 32 ++- .../JGroupsRaftClusterStoreBenchmark.java | 216 ++++++++++++++++++ .../JGroupsRaftSlotsStoreBenchmark.java | 12 +- 4 files changed, 254 insertions(+), 8 deletions(-) create mode 100644 narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JGroupsRaftClusterStoreBenchmark.java diff --git a/.github/scripts/benchmark.sh b/.github/scripts/benchmark.sh index 3eb4e76b..e6940f02 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.* 7" +BM5="ArjunaJTA/jta com.arjuna.ats.jta.xa.performance.*StoreBenchmark.* 8" #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 index aeab4740..dca1789f 100644 --- a/narayana/ArjunaJTA/jta/etc/jgroups-raft.xml +++ b/narayana/ArjunaJTA/jta/etc/jgroups-raft.xml @@ -1,3 +1,24 @@ + @@ -27,8 +48,8 @@ - + @@ -48,6 +69,8 @@ max_bytes="8M"/> + + @@ -58,13 +81,10 @@ min_threshold="0.4"/> - + - - - 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 index 571a5605..0206d07e 100644 --- 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 @@ -40,7 +40,17 @@ * 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 + *

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 { From d85962c10fc79fac302ffce7fdb1d7a2c0416501 Mon Sep 17 00:00:00 2001 From: Michael Musgrove Date: Wed, 22 Jul 2026 10:48:59 +0100 Subject: [PATCH 3/7] JBTM-4038 cleanup failed transactions --- .../JGroupsRaftSlotsStoreBenchmark.java | 2 ++ .../JGroupsSlotsStoreBenchmark.java | 2 ++ .../ats/jta/xa/performance/JTAStoreBase.java | 19 ++++++++++++++++--- 3 files changed, 20 insertions(+), 3 deletions(-) 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 index 0206d07e..d411438a 100644 --- 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 @@ -107,6 +107,8 @@ public static void setup() throws CoreEnvironmentBeanException { configBean.setRaftLogFsync(true); // durable writes for fault tolerance configBean.setAllowDirtyReads(false); + configBean.setRecycleFailedSlots(true); // otherwise failed slot writes will lead to slot exhaustion + // Set the slot size, making sure it's a sensible multiple configBean.setNumberOfSlots(roundUp(256, threadCount)); configBean.setBackingSlotsClassName(JGroupsRaftSlots.class.getName()); 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 index 7a9ca0db..febdd3f9 100644 --- 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 @@ -96,6 +96,8 @@ public static void setup() throws CoreEnvironmentBeanException { 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) 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..7b79f179 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 @@ -15,6 +15,7 @@ import jakarta.transaction.NotSupportedException; import jakarta.transaction.RollbackException; import jakarta.transaction.SystemException; +import jakarta.transaction.Transaction; import jakarta.transaction.TransactionManager; import org.jboss.logging.Logger; @@ -109,10 +110,22 @@ public boolean jtaTest() throws HeuristicRollbackException, SystemException, Heu tm.commit(); } catch(Exception e) { - log.fatal("JTAStoreTests#jtaTest%n", e); - throw new Error(e); + log.warn("JTAStoreTests#jtaTest%n", e); + try { + tm.getTransaction().rollback(); + } catch (Exception suppressed) { + e.addSuppressed(suppressed); + } + throw e; + } finally { + Transaction txn = tm.getTransaction(); + if (txn != null) { + try { + txn.rollback(); + } catch (Exception ignored) { + } + } } - return true; } } From fb7b133c02e1b78054aa0835ac04b810a3432229 Mon Sep 17 00:00:00 2001 From: Michael Musgrove Date: Thu, 23 Jul 2026 08:21:06 +0100 Subject: [PATCH 4/7] JBTM-4038 Ensure that the slot index is large enough for the thread counts and has headroom and ensure that the slot config for numberOfSlots is applied SlotStoreEnvironmentBean (required because the SlotStoreAdaptor no-arg constructor creates the SlotStore with SlotStoreEnvironmentBean). --- .../xa/performance/JGroupsRaftSlotsStoreBenchmark.java | 10 +++++++--- .../jta/xa/performance/JGroupsSlotsStoreBenchmark.java | 10 +++++++--- .../arjuna/ats/jta/xa/performance/JTAStoreBase.java | 6 +++--- 3 files changed, 17 insertions(+), 9 deletions(-) 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 index d411438a..93e2aef7 100644 --- 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 @@ -92,7 +92,8 @@ public static void main(String[] args) throws RunnerException { public static void setup() throws CoreEnvironmentBeanException { JTAStoreBase.setup(SlotStoreAdaptor.class.getName()); JGroupsRaftStoreEnvironmentBean configBean = BeanPopulator.getDefaultInstance(JGroupsRaftStoreEnvironmentBean.class); - BeanPopulator.getDefaultInstance(SlotStoreEnvironmentBean.class).setBackingSlotsClassName(JGroupsRaftSlots.class.getName()); + SlotStoreEnvironmentBean slotStoreBean = BeanPopulator.getDefaultInstance(SlotStoreEnvironmentBean.class); + slotStoreBean.setBackingSlotsClassName(JGroupsRaftSlots.class.getName()); int threadCount = getThreadCountFromProperties(THREADS); // JGroups configuration @@ -109,8 +110,11 @@ public static void setup() throws CoreEnvironmentBeanException { configBean.setRecycleFailedSlots(true); // otherwise failed slot writes will lead to slot exhaustion - // Set the slot size, making sure it's a sensible multiple - configBean.setNumberOfSlots(roundUp(256, threadCount)); + // 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(JGroupsRaftSlots.class.getName()); cleanStore(Paths.get(configBean.getStoreDir()).toFile()); 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 index febdd3f9..b6f7a359 100644 --- 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 @@ -82,7 +82,8 @@ public static void main(String[] args) throws RunnerException { public static void setup() throws CoreEnvironmentBeanException { JTAStoreBase.setup(SlotStoreAdaptor.class.getName()); JGroupsStoreEnvironmentBean configBean = BeanPopulator.getDefaultInstance(JGroupsStoreEnvironmentBean.class); - BeanPopulator.getDefaultInstance(SlotStoreEnvironmentBean.class).setBackingSlotsClassName(JGroupsSlots.class.getName()); + SlotStoreEnvironmentBean slotStoreBean = BeanPopulator.getDefaultInstance(SlotStoreEnvironmentBean.class); + slotStoreBean.setBackingSlotsClassName(JGroupsSlots.class.getName()); int threadCount = getThreadCountFromProperties(THREADS); // JGroups configuration @@ -103,8 +104,11 @@ public static void setup() throws CoreEnvironmentBeanException { // L2 cache configuration (disabled for consistency) configBean.setCachingTime(0L); - // Set the slot size, making sure it's a sensible multiple - configBean.setNumberOfSlots(roundUp(256, threadCount)); + // 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()); 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 7b79f179..be9fe68f 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 @@ -84,11 +84,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; } } From 111110ceafaa80603fbee296d36fee901082a985 Mon Sep 17 00:00:00 2001 From: Michael Musgrove Date: Thu, 23 Jul 2026 14:25:05 +0100 Subject: [PATCH 5/7] JBTM-4038 Raft serializes all writes through a single consensus thread, so at high thread counts (e.g. -t 1600) the slot pool fills faster than transactions complete. Cap slot allocation to the THREADS constant and swallow RollbackException in the benchmark method so -foe true JMH flag does not kill the entire JMH run. --- .../JGroupsRaftSlotsStoreBenchmark.java | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) 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 index 93e2aef7..a2401bf9 100644 --- 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 @@ -95,6 +95,10 @@ public static void setup() throws CoreEnvironmentBeanException { 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"); @@ -112,7 +116,7 @@ public static void setup() throws CoreEnvironmentBeanException { // 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); + int numberOfSlots = roundUp(256, effectiveThreadCount); slotStoreBean.setNumberOfSlots(numberOfSlots); configBean.setNumberOfSlots(numberOfSlots); configBean.setBackingSlotsClassName(JGroupsRaftSlots.class.getName()); @@ -128,7 +132,15 @@ public static void tearDown() { } @Benchmark - public void testJGroupsRaftSlotsStore(Blackhole bh) throws HeuristicRollbackException, SystemException, HeuristicMixedException, NotSupportedException, RollbackException { - bh.consume(super.jtaTest()); + public void testJGroupsRaftSlotsStore(Blackhole bh) throws HeuristicRollbackException, SystemException, HeuristicMixedException, NotSupportedException { + try { + bh.consume(super.jtaTest()); + } catch (RollbackException e) { + // Raft serializes writes through a single consensus thread so under high thread + // counts the slot pool fills faster than transactions complete. Swallow slot-exhaustion + // errors so -foe true doesn't kill the entire benchmark — the throughput score + // naturally reflects the contention. + bh.consume(false); + } } } From f739d18f7c42cbc9987609d7ffca2bd17054278c Mon Sep 17 00:00:00 2001 From: Michael Musgrove Date: Thu, 23 Jul 2026 14:59:05 +0100 Subject: [PATCH 6/7] JBTM-4038 "coderabbit ai review suggestions: JGroupsRaftSlotsStoreBenchmark: replace bh.consume(false) on RollbackException with @AuxCounters so JMH reports committed and rolledBack throughput separately instead of inflating the primary score with failed operations. benchmark.sh: bump store benchmark count to 9 for JGroupsRaftClusterStoreBenchmark." --- .github/scripts/benchmark.sh | 2 +- .../JGroupsRaftSlotsStoreBenchmark.java | 37 ++++++++++++++++--- .../ats/jta/xa/performance/JTAStoreBase.java | 24 +++++++----- 3 files changed, 47 insertions(+), 16 deletions(-) diff --git a/.github/scripts/benchmark.sh b/.github/scripts/benchmark.sh index e6940f02..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.* 8" +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/tests/classes/com/arjuna/ats/jta/xa/performance/JGroupsRaftSlotsStoreBenchmark.java b/narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JGroupsRaftSlotsStoreBenchmark.java index a2401bf9..279c59d7 100644 --- 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 @@ -18,6 +18,7 @@ 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; @@ -131,16 +132,40 @@ public static void tearDown() { 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) throws HeuristicRollbackException, SystemException, HeuristicMixedException, NotSupportedException { + public void testJGroupsRaftSlotsStore(Blackhole bh, TxnCounters counters) throws HeuristicRollbackException, SystemException, HeuristicMixedException, NotSupportedException { try { bh.consume(super.jtaTest()); + counters.committed++; } catch (RollbackException e) { - // Raft serializes writes through a single consensus thread so under high thread - // counts the slot pool fills faster than transactions complete. Swallow slot-exhaustion - // errors so -foe true doesn't kill the entire benchmark — the throughput score - // naturally reflects the contention. - bh.consume(false); + counters.rolledBack++; } } } 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 be9fe68f..e1cd011a 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 @@ -102,8 +102,10 @@ protected static void setup(String storeType) throws CoreEnvironmentBeanExceptio } public boolean jtaTest() throws HeuristicRollbackException, SystemException, HeuristicMixedException, NotSupportedException, RollbackException { + boolean txnStarted = false; try { tm.begin(); + txnStarted = true; tm.getTransaction().enlistResource(resource1); tm.getTransaction().enlistResource(resource2); @@ -111,18 +113,22 @@ public boolean jtaTest() throws HeuristicRollbackException, SystemException, Heu tm.commit(); } catch(Exception e) { log.warn("JTAStoreTests#jtaTest%n", e); - try { - tm.getTransaction().rollback(); - } catch (Exception suppressed) { - e.addSuppressed(suppressed); + if (txnStarted) { + try { + tm.getTransaction().rollback(); + } catch (Exception suppressed) { + e.addSuppressed(suppressed); + } } throw e; } finally { - Transaction txn = tm.getTransaction(); - if (txn != null) { - try { - txn.rollback(); - } catch (Exception ignored) { + if (txnStarted) { + Transaction txn = tm.getTransaction(); + if (txn != null) { + try { + txn.rollback(); + } catch (Exception ignored) { + } } } } From 5dd323880215f93b51a2ed0e85f0d0c64f686941 Mon Sep 17 00:00:00 2001 From: Michael Musgrove Date: Thu, 23 Jul 2026 19:08:09 +0100 Subject: [PATCH 7/7] JBTM-4038 clean up exception handler in JTAStoreBase.jtaTest --- .../ats/jta/xa/performance/JTAStoreBase.java | 30 ++++--------------- 1 file changed, 6 insertions(+), 24 deletions(-) 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 e1cd011a..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 @@ -15,7 +15,6 @@ import jakarta.transaction.NotSupportedException; import jakarta.transaction.RollbackException; import jakarta.transaction.SystemException; -import jakarta.transaction.Transaction; import jakarta.transaction.TransactionManager; import org.jboss.logging.Logger; @@ -102,35 +101,18 @@ protected static void setup(String storeType) throws CoreEnvironmentBeanExceptio } public boolean jtaTest() throws HeuristicRollbackException, SystemException, HeuristicMixedException, NotSupportedException, RollbackException { - boolean txnStarted = false; + tm.begin(); try { - tm.begin(); - txnStarted = true; - tm.getTransaction().enlistResource(resource1); tm.getTransaction().enlistResource(resource2); - tm.commit(); - } catch(Exception e) { - log.warn("JTAStoreTests#jtaTest%n", e); - if (txnStarted) { - try { - tm.getTransaction().rollback(); - } catch (Exception suppressed) { - e.addSuppressed(suppressed); - } + } catch (Exception e) { + try { + tm.rollback(); + } catch (Exception suppressed) { + e.addSuppressed(suppressed); } throw e; - } finally { - if (txnStarted) { - Transaction txn = tm.getTransaction(); - if (txn != null) { - try { - txn.rollback(); - } catch (Exception ignored) { - } - } - } } return true; }