Skip to content

JBTM-4038 JGroupsSlots and JGroupsRaftSlots benchmarks - #239

Open
mmusgrov wants to merge 9 commits into
jbosstm:mainfrom
mmusgrov:JBTM-4038
Open

JBTM-4038 JGroupsSlots and JGroupsRaftSlots benchmarks#239
mmusgrov wants to merge 9 commits into
jbosstm:mainfrom
mmusgrov:JBTM-4038

Conversation

@mmusgrov

@mmusgrov mmusgrov commented Jul 20, 2026

Copy link
Copy Markdown
Member

https://redhat.atlassian.net/browse/JBTM-4038

NARAYANA_REPO=mmusgrov
NARAYANA_BRANCH=JBTM-4038
NY_BRANCH=JBTM-4038
THREAD_COUNTS=1 24 240
COMPARE_STORES=y
COMPARE_IMPLEMENTATIONS=n
COMPARE_TRANSPORTS=n
COMPARE_JOURNAL_PARAMETERS=n

JDK25

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds JGroups and JGroups Raft configurations, Maven dependency wiring, three JMH slot-store benchmarks, transaction rollback cleanup, report filtering, slot-count headroom, and benchmark CSV row-count validation.

Changes

JGroups benchmark support

Layer / File(s) Summary
JGroups and Raft configuration
narayana/ArjunaJTA/jta/pom.xml, narayana/ArjunaJTA/jta/etc/jgroups.xml, narayana/ArjunaJTA/jta/etc/jgroups-raft.xml
Adds JGroups protocol configurations and an optional, versioned JGroups Raft dependency.
Benchmark setup and execution
narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JGroupsSlotsStoreBenchmark.java, narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JGroupsRaftSlotsStoreBenchmark.java
Adds JMH entry points, store configuration, WAL or Raft setup, lifecycle cleanup, transaction counters, and benchmark methods for both slot-store implementations.
Raft cluster benchmark
narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JGroupsRaftClusterStoreBenchmark.java
Adds a three-node Raft benchmark for leader election, replicated slot initialization, follower reads, and leader writes.
Transaction and slot-count handling
narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JTAStoreBase.java
Adds transaction rollback cleanup and changes slot-count rounding to add headroom for exact multiples.
Benchmark reporting and validation
narayana/ArjunaJTA/jta/tests/classes/io/narayana/perf/product/ReportGenerator.java, .github/scripts/benchmark.sh
Filters sub-metric records, uses the configured product suffix, and changes the expected store benchmark result count from five to nine.

Sequence Diagram(s)

sequenceDiagram
  participant JMHRunner
  participant RaftChannels
  participant JGroupsRaftSlots
  participant Leader
  participant Follower
  JMHRunner->>RaftChannels: create and connect three channels
  RaftChannels->>RaftChannels: elect leader
  JMHRunner->>JGroupsRaftSlots: initialize node slot stores
  Leader->>Follower: replicate pre-populated slots
  JMHRunner->>Follower: readFromFollower()
  JMHRunner->>Leader: writeToLeader()
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the addition of JGroupsSlots and JGroupsRaftSlots benchmarks and includes the related issue identifier.
Description check ✅ Passed The description provides the issue, repository and branch settings, benchmark parameters, comparison settings, and JDK selection required for this benchmark pull request.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/scripts/benchmark.sh:
- Line 113: Update BM5’s JMH selector pattern so it matches the newly added
JGroupsSlotsBenchmark and JGroupsRaftSlotsBenchmark benchmarks, including their
testJGroupsSlotsStore and testJGroupsRaftSlotsStore methods, while retaining the
existing StoreBenchmark matches and expected count of 7.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6999bbdf-b1be-4c03-ae87-b1357153804c

📥 Commits

Reviewing files that changed from the base of the PR and between 2c4e9ac and 7615a43.

📒 Files selected for processing (6)
  • .github/scripts/benchmark.sh
  • narayana/ArjunaJTA/jta/etc/jgroups-raft.xml
  • narayana/ArjunaJTA/jta/etc/jgroups.xml
  • narayana/ArjunaJTA/jta/pom.xml
  • narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JGroupsRaftSlotsBenchmark.java
  • narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JGroupsSlotsBenchmark.java

Comment thread .github/scripts/benchmark.sh Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/scripts/benchmark.sh:
- Line 113: Update the BM5 benchmark selector configuration to use a lower bound
of 9 instead of 8, ensuring all four added StoreBenchmark methods are required
while preserving the existing selector.

In
`@narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JGroupsRaftSlotsStoreBenchmark.java`:
- Around line 90-115: Remove the stale “configure 3 node cluster” TODO from the
setup method in JGroupsRaftSlotsStoreBenchmark, since
JGroupsRaftClusterStoreBenchmark now covers that configuration. Leave the
existing single-node benchmark setup unchanged.

In
`@narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JTAStoreBase.java`:
- Around line 113-127: Update the transaction setup and cleanup around
tm.begin() in JTAStoreBase so rollback occurs only when this method successfully
started the transaction. Track that state with a local transactionStarted flag,
set it after begin succeeds, and guard both the catch-path rollback and
finally-block rollback with it, preserving suppressed-exception handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 5676c249-55bc-4a74-9f89-97b5e1e0fa1b

📥 Commits

Reviewing files that changed from the base of the PR and between 3a97f23 and 5af708c.

📒 Files selected for processing (8)
  • .github/scripts/benchmark.sh
  • narayana/ArjunaJTA/jta/etc/jgroups-raft.xml
  • narayana/ArjunaJTA/jta/etc/jgroups.xml
  • narayana/ArjunaJTA/jta/pom.xml
  • narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JGroupsRaftClusterStoreBenchmark.java
  • narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JGroupsRaftSlotsStoreBenchmark.java
  • narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JGroupsSlotsStoreBenchmark.java
  • narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JTAStoreBase.java

Comment thread .github/scripts/benchmark.sh Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JGroupsRaftSlotsStoreBenchmark.java`:
- Around line 135-144: Update testJGroupsRaftSlotsStore so RollbackException is
not counted as a successful JMH operation: add and increment JMH auxiliary
counters for committed and rolled-back executions, and publish committed
throughput separately while recording rollback failures. Do not let the
exception handler return as an unclassified successful invocation; preserve the
existing transaction invocation and Blackhole consumption for committed results.

In
`@narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JTAStoreBase.java`:
- Around line 120-127: Update the finally-block cleanup in JTAStoreBase so it
runs only when the transaction was started, and wrap both tm.getTransaction()
and rollback failures as best-effort cleanup. Preserve the original exception
while attaching any cleanup failure via addSuppressed, including failures
retrieving the transaction.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8ffea7d3-2358-4ec7-87ae-a20ba7385b4f

📥 Commits

Reviewing files that changed from the base of the PR and between 5af708c and 111110c.

📒 Files selected for processing (3)
  • narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JGroupsRaftSlotsStoreBenchmark.java
  • narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JGroupsSlotsStoreBenchmark.java
  • narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JTAStoreBase.java

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/scripts/benchmark.sh:
- Line 113: Update the BM5 benchmark pattern to expect 11 CSV rows, accounting
for the committed and rolledBack auxiliary rows emitted by
JGroupsRaftSlotsStoreBenchmark while preserving the existing benchmark
selection.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: cd24571f-5b77-47d5-b175-7e85520ddcfd

📥 Commits

Reviewing files that changed from the base of the PR and between 111110c and 5dd3238.

📒 Files selected for processing (3)
  • .github/scripts/benchmark.sh
  • narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JGroupsRaftSlotsStoreBenchmark.java
  • narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JTAStoreBase.java

Comment thread .github/scripts/benchmark.sh
mmusgrov added 7 commits July 27, 2026 10:43
…uster of JGroupsRaftSlots (using SHARED_LOOPBACK)
…ounts 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).
…d, 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: 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."
throw new Error(e);
} catch (Exception e) {
try {
tm.rollback();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very interesting. If I am correctly understanding we are also able to count the rolled back transactions with the benchmark now.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can, it's nice isn't it. We can count pretty much any numeric type and have it reported as a separate benchmark run.

@marcosgopen

Copy link
Copy Markdown
Member

Thanks @mmusgrov for your PR!
I had a look at the benchmark-store-output, which is:

JMH Benchmarks Results

Module: ArjunaJTA/jta
Pattern: com.arjuna.ats.jta.xa.performance.*StoreBenchmark.*
Run arguments: -t 1  -r 30 -f 3 -wi 5 -i 5 -foe true 
Run output:
"Benchmark","Mode","Threads","Samples","Score","Score Error (99.9%)","Unit","Param: allowDirtyReads"
"com.arjuna.ats.jta.xa.performance.DiskSlotsStoreBenchmark.testDiskSlotsStore","thrpt",1,15,2085.362302,73.252498,"ops/s",
"com.arjuna.ats.jta.xa.performance.HQStoreBenchmark.testHQStore","thrpt",1,15,292.887681,0.527811,"ops/s",
"com.arjuna.ats.jta.xa.performance.InfinispanSlotsStoreBenchmark.testInfinispanStore","thrpt",1,15,5250.356305,56.773446,"ops/s",
"com.arjuna.ats.jta.xa.performance.JDBCStoreBenchmark.testJDBCStore","thrpt",1,15,175.285207,5.214724,"ops/s",
"com.arjuna.ats.jta.xa.performance.JGroupsRaftClusterStoreBenchmark.readFromFollower","thrpt",1,15,119593838.728571,1086234.870506,"ops/s",true
"com.arjuna.ats.jta.xa.performance.JGroupsRaftClusterStoreBenchmark.readFromFollower","thrpt",1,15,10021.673715,137.849710,"ops/s",false
"com.arjuna.ats.jta.xa.performance.JGroupsRaftClusterStoreBenchmark.writeToLeader","thrpt",1,15,6534.717422,67.998621,"ops/s",true
"com.arjuna.ats.jta.xa.performance.JGroupsRaftClusterStoreBenchmark.writeToLeader","thrpt",1,15,6575.663156,88.686556,"ops/s",false
"com.arjuna.ats.jta.xa.performance.JGroupsRaftSlotsStoreBenchmark.testJGroupsRaftSlotsStore","thrpt",1,15,2626.523792,50.386224,"ops/s",
"com.arjuna.ats.jta.xa.performance.JGroupsRaftSlotsStoreBenchmark.testJGroupsRaftSlotsStore:committed","thrpt",1,15,2626.523792,50.386224,"ops/s",
"com.arjuna.ats.jta.xa.performance.JGroupsRaftSlotsStoreBenchmark.testJGroupsRaftSlotsStore:rolledBack","thrpt",1,15,0.000000,0.000000,"ops/s",
"com.arjuna.ats.jta.xa.performance.JGroupsSlotsStoreBenchmark.testJGroupsSlotsStore","thrpt",1,15,479.881762,0.626848,"ops/s",
"com.arjuna.ats.jta.xa.performance.ShadowNoFileLockStoreBenchmark.testShadowNoFileLockStore","thrpt",1,15,2168.566636,41.324747,"ops/s",
"com.arjuna.ats.jta.xa.performance.VolatileStoreBenchmark.testVolatileStore","thrpt",1,15,388660.144227,2384.374095,"ops/s",

Threads           A           B           C           D           E           F           G           H
      1        2085         292        5250         175           0         479        2168      388660


I believe letter E is showing results of 'JGroupsRaftSlotsStoreBenchmark.testJGroupsRaftSlotsStore:rolledBack' which is 0, but JGroupsRaftSlotsStoreBenchmark.testJGroupsRaftSlotsStore is not listed instead (which means it is not visible in the png file).
I can see JGroupsRaftSlotsStoreBenchmark.testJGroupsRaftSlotsStore throughput is 2626 and JGroupsSlotsStoreBenchmark.testJGroupsSlotsStore is 479, I would have expected the raftSlotStore to be less efficient from the sentence "You can tolerate higher write latency for stronger guarantees"?

I can read '"Param: allowDirtyReads"' in the run output, I noticed allowDirtyReads is not a Param, which is great, but I don't understand how that works. Do the throughputs listed set is as false or true?

@mmusgrov

mmusgrov commented Jul 29, 2026

Copy link
Copy Markdown
Member Author

I believe letter E is showing results of 'JGroupsRaftSlotsStoreBenchmark.testJGroupsRaftSlotsStore:rolledBack' which is 0, but JGroupsRaftSlotsStoreBenchmark.testJGroupsRaftSlotsStore is not listed instead (which means it is not visible in the png file).

I don't understand the output, it looks like a bug in the pre-existing io.narayana.perf.product.ProductComparison.java and io.narayana.perf.product.ReportGenerator.java logic - the store comparison bencharks are getting conflated with the JTA product comparisons when they are completely independent from each other. I will investigate and get back to you.

I can see JGroupsRaftSlotsStoreBenchmark.testJGroupsRaftSlotsStore throughput is 2626 and JGroupsSlotsStoreBenchmark.testJGroupsSlotsStore is 479, I would have expected the raftSlotStore to be less efficient from the sentence "You can tolerate higher write latency for stronger guarantees"?

This was a difficult one to answer since multiple factors could be at play and I'm not a performance expert: hardware differences, fsync performance is critical (eg on tmpfs it's a no-op which obviously is unsuitable for transaction logs), error margins on the JMH measurements etc. but I'll provide my analysis with the caveat that it's by no means definitive:

Even though slot store can process reads and writes concurrently Raft serializes through a single thread. Therefore

  • Raft performance on a high number of threads suffers from contention (they all funnel through the single Raft thread).
  • Raft performance on a low number of threads does not suffer so much from contention so relative throughput decreases as more threads are added, whereas
  • JGroups ReplCache can process reads and writes in parallel so relative throughput increases as more threads are added

The CI benchmarks seem to corroborate this rationale: throughput increases at a faster rate as more threads are added with JGroupsSlotsStore - the crossover happening somewhere between 24 and 240 threads

Run arguments: -t 1 -r 10 -f 1 -wi 2 -i 5 -foe true
"JGroupsRaftSlotsStoreBenchmark.testJGroupsRaftSlotsStore","thrpt",1,5,    2630.043285,294.539817,"ops/s",^M 
"JGroupsSlotsStoreBenchmark.testJGroupsSlotsStore","thrpt",1,5,            480.078176,0.853981,"ops/s",^M
 
Run arguments: -t 24 -r 10 -f 1 -wi 2 -i 5 -foe true
"JGroupsRaftSlotsStoreBenchmark.testJGroupsRaftSlotsStore","thrpt",24,5,   18330.368790,1075.796448,"ops/s",^M
"JGroupsSlotsStoreBenchmark.testJGroupsSlotsStore","thrpt",24,5,           11953.585953,18.221865,"ops/s",^M
 
Run arguments: -t 240 -r 10 -f 1 -wi 2 -i 5 -foe true
"JGroupsRaftSlotsStoreBenchmark.testJGroupsRaftSlotsStore","thrpt",240,5,  64618.393889,1687.855256,"ops/s",^M
"JGroupsSlotsStoreBenchmark.testJGroupsSlotsStore","thrpt",240,5,          76401.592772,4202.502216,"ops/s",^M

On my laptop both stores scale similarly with thread count. At 1 and 240 threads they're roughly equal and at 24 threads ReplCache has the edge. The error margins are large (especially ReplCache's ±36,010 at 240 threads), so the differences aren't statistically significant.

A google search reports that JGroups Raft supports [quote] "batching multiple pending proposals and entries into fewer network/log append operations, relying on underlying transport optimization and the architectural message bundling/batching capabilities provided by the core JGroups framework". So this will account for some of the performance benefits seen by Raft which might account for some of the higher than expected throughput.

Some other things to consider (though I'm not sure how to relate them to the varying benchmark results on CI and my laptop) are:

  • JGroupsSlotStore write (ReplCache.put()) incurs per-message overhead: each put constructs a JGroups message, serialises the slot store key (ByteArrayKey) and value byte[], traverses the full protocol stack, and dispatches via JGroups RpcDispatcher which uses reflection (this happens on a single-node cluster too)
  • GroupsRaftSlotStore avoids this overhead: in a single-node cluster the Raft majority is 1 (itself), so a write just appends to the local FileBasedLog (which will be very fast since it's the bottle neck in the JGroups Raft implementation) and applies an Integer key to the in-memory state machine so there is no serialisation or protocol stack overhead like there is with JGroupsSlotStore.

I can read '"Param: allowDirtyReads"' in the run output, I noticed allowDirtyReads is not a Param, which is great, but I don't understand how that works. Do the throughputs listed set is as false or true?

The JMH @Param annotation runs the benchmark for each element of the annotation, so the following @Param specification will run each benchmark twice, the first run with allowDirtyReads=true followed by allowDirtyReads=false.

    @Param({"true", "false"})                                                                                                                                                                                       
    private boolean allowDirtyReads;

The reason I did this is I wanted to characterise the overhead of routing reads and writes through the Raft leader:

readFromFollower + allowDirtyReads=true is ~800x faster on my laptop with "-t 4 -f 1 -wi 3 -i 5 -r 1" than the other three combinations which makes sense because "reads are served from each node’s local in-memory state machine" so readers avoid the Raft protocol overhead of getting the value from the leader.

readFromLeader + allowDirtyReads=true|false makes little difference because the leader can consult the local in-memory state machine.

The kind of layout of the JMH results should include a column called (allowDirtyReads), on my laptop, for example:

Benchmark                                                            (allowDirtyReads)   Mode  Cnt        Score         Error  Units                                                                                
JGroupsRaftClusterStoreBenchmark.readFromFollower                                 true  thrpt    5  5265221.415 ± 1196603.578  ops/s
JGroupsRaftClusterStoreBenchmark.readFromFollower                                false  thrpt    5   632007.865 ±   30103.850  ops/s
JGroupsRaftClusterStoreBenchmark.writeToLeader                                    true  thrpt    5   635201.083 ±   40417.615  ops/s
JGroupsRaftClusterStoreBenchmark.writeToLeader                                   false  thrpt    5   637094.159 ±  111058.462  ops/s
JGroupsRaftSlotsStoreBenchmark.testJGroupsRaftSlotsStore                           N/A  thrpt    5    74704.529 ±    9339.771  ops/s                                                                                
JGroupsRaftSlotsStoreBenchmark.testJGroupsRaftSlotsStore:committed                 N/A  thrpt    5    75185.903 ±    9345.117  ops/s                                                                                
JGroupsRaftSlotsStoreBenchmark.testJGroupsRaftSlotsStore:rolledBack                N/A  thrpt    5          ≈ 0                ops/s                                                                                
JGroupsSlotsStoreBenchmark.testJGroupsSlotsStore                                   N/A  thrpt    5    67952.787 ±   14228.163  ops/s                                                                                

I will say that benchmarking on CI and laptops has always been fraught with issues.

@marcosgopen

marcosgopen commented Jul 30, 2026

Copy link
Copy Markdown
Member

I believe letter E is showing results of 'JGroupsRaftSlotsStoreBenchmark.testJGroupsRaftSlotsStore:rolledBack' which is 0, but JGroupsRaftSlotsStoreBenchmark.testJGroupsRaftSlotsStore is not listed instead (which means it is not visible in the png file).

I don't understand the output, it looks like a bug in the pre-existing io.narayana.perf.product.ProductComparison.java and io.narayana.perf.product.ReportGenerator.java logic - the store comparison bencharks are getting conflated with the JTA product comparisons when they are completely independent from each other. I will investigate and get back to you.

I can see JGroupsRaftSlotsStoreBenchmark.testJGroupsRaftSlotsStore throughput is 2626 and JGroupsSlotsStoreBenchmark.testJGroupsSlotsStore is 479, I would have expected the raftSlotStore to be less efficient from the sentence "You can tolerate higher write latency for stronger guarantees"?

This was a difficult one to answer since multiple factors could be at play and I'm not a performance expert: hardware differences, fsync performance is critical (eg on tmpfs it's a no-op which obviously is unsuitable for transaction logs), error margins on the JMH measurements etc. but I'll provide my analysis with the caveat that it's by no means definitive:

Even though slot store can process reads and writes concurrently Raft serializes through a single thread. Therefore

* Raft performance on a high number of threads suffers from contention (they all funnel through the single Raft thread).

* Raft performance on a low number of threads does not suffer so much from contention so _relative throughput_ decreases as more threads are added, whereas

* JGroups ReplCache can process reads and writes in parallel so _relative throughput_ increases as more threads are added

The CI benchmarks seem to corroborate this rationale: throughput increases at a faster rate as more threads are added with JGroupsSlotsStore - the crossover happening somewhere between 24 and 240 threads

Run arguments: -t 1 -r 10 -f 1 -wi 2 -i 5 -foe true
"JGroupsRaftSlotsStoreBenchmark.testJGroupsRaftSlotsStore","thrpt",1,5,    2630.043285,294.539817,"ops/s",^M 
"JGroupsSlotsStoreBenchmark.testJGroupsSlotsStore","thrpt",1,5,            480.078176,0.853981,"ops/s",^M
 
Run arguments: -t 24 -r 10 -f 1 -wi 2 -i 5 -foe true
"JGroupsRaftSlotsStoreBenchmark.testJGroupsRaftSlotsStore","thrpt",24,5,   18330.368790,1075.796448,"ops/s",^M
"JGroupsSlotsStoreBenchmark.testJGroupsSlotsStore","thrpt",24,5,           11953.585953,18.221865,"ops/s",^M
 
Run arguments: -t 240 -r 10 -f 1 -wi 2 -i 5 -foe true
"JGroupsRaftSlotsStoreBenchmark.testJGroupsRaftSlotsStore","thrpt",240,5,  64618.393889,1687.855256,"ops/s",^M
"JGroupsSlotsStoreBenchmark.testJGroupsSlotsStore","thrpt",240,5,          76401.592772,4202.502216,"ops/s",^M

On my laptop both stores scale similarly with thread count. At 1 and 240 threads they're roughly equal and at 24 threads ReplCache has the edge. The error margins are large (especially ReplCache's ±36,010 at 240 threads), so the differences aren't statistically significant.

A google search reports that JGroups Raft supports [quote] "batching multiple pending proposals and entries into fewer network/log append operations, relying on underlying transport optimization and the architectural message bundling/batching capabilities provided by the core JGroups framework". So this will account for some of the performance benefits seen by Raft which might account for some of the higher than expected throughput.

Some other things to consider (though I'm not sure how to relate them to the varying benchmark results on CI and my laptop) are:

* JGroupsSlotStore write (ReplCache.put()) incurs per-message overhead: each put constructs a JGroups message, serialises the slot store key (ByteArrayKey) and value byte[], traverses the full protocol stack, and dispatches via JGroups RpcDispatcher which uses reflection (this happens on a single-node cluster too)

* GroupsRaftSlotStore avoids this overhead: in a single-node cluster the Raft majority is 1 (itself), so a write just appends to the local FileBasedLog (which will be very fast since it's the bottle neck in the JGroups Raft implementation) and applies an Integer key to the in-memory state machine so there is no serialisation or protocol stack overhead like there is with JGroupsSlotStore.

I can read '"Param: allowDirtyReads"' in the run output, I noticed allowDirtyReads is not a Param, which is great, but I don't understand how that works. Do the throughputs listed set is as false or true?

The JMH @Param annotation runs the benchmark for each element of the annotation, so the following @Param specification will run each benchmark twice, the first run with allowDirtyReads=true followed by allowDirtyReads=false.

    @Param({"true", "false"})                                                                                                                                                                                       
    private boolean allowDirtyReads;

The reason I did this is I wanted to characterise the overhead of routing reads and writes through the Raft leader:

readFromFollower + allowDirtyReads=true is ~800x faster on my laptop with "-t 4 -f 1 -wi 3 -i 5 -r 1" than the other three combinations which makes sense because "reads are served from each node’s local in-memory state machine" so readers avoid the Raft protocol overhead of getting the value from the leader.

readFromLeader + allowDirtyReads=true|false makes little difference because the leader can consult the local in-memory state machine.

The kind of layout of the JMH results should include a column called (allowDirtyReads), on my laptop, for example:

Benchmark                                                            (allowDirtyReads)   Mode  Cnt        Score         Error  Units                                                                                
JGroupsRaftClusterStoreBenchmark.readFromFollower                                 true  thrpt    5  5265221.415 ± 1196603.578  ops/s
JGroupsRaftClusterStoreBenchmark.readFromFollower                                false  thrpt    5   632007.865 ±   30103.850  ops/s
JGroupsRaftClusterStoreBenchmark.writeToLeader                                    true  thrpt    5   635201.083 ±   40417.615  ops/s
JGroupsRaftClusterStoreBenchmark.writeToLeader                                   false  thrpt    5   637094.159 ±  111058.462  ops/s
JGroupsRaftSlotsStoreBenchmark.testJGroupsRaftSlotsStore                           N/A  thrpt    5    74704.529 ±    9339.771  ops/s                                                                                
JGroupsRaftSlotsStoreBenchmark.testJGroupsRaftSlotsStore:committed                 N/A  thrpt    5    75185.903 ±    9345.117  ops/s                                                                                
JGroupsRaftSlotsStoreBenchmark.testJGroupsRaftSlotsStore:rolledBack                N/A  thrpt    5          ≈ 0                ops/s                                                                                
JGroupsSlotsStoreBenchmark.testJGroupsSlotsStore                                   N/A  thrpt    5    67952.787 ±   14228.163  ops/s                                                                                

I will say that benchmarking on CI and laptops has always been fraught with issues.

Thanks @mmusgrov for clarifying! This was surely useful. The PR looks good and it would be great to improve the performance output in the future (especially the png file which is not so clear IMO). Having the allowDirtyReads would be optimal or alternatively having methods like writeToLeaderWithallowDirtyReadsFalse can be good too.

[UPDATE] For 'allowDirtyReads'=true I would expect to see similar performance for 'write' and improved performance for 'readFromFollower' executions as the reading is done in the local cache.

@marcosgopen marcosgopen left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @mmusgrov . I am approving but I mention I am not an expert on the jgroup configuration file so I suppose those file have the default/recommended configuration for this performance testing. Additionally, as I mentioned above, we have space for improvements in the performance output files (readability) in the future.

…the counters for committed and rolledBack and not summing to the total number of transactions because the metric includes the warmup time whereas the primary benchmark only reports during the measurement window.
…coded Comparison.length() and skip JMH sub-metrics (:committed, :rolledBack) that were overwriting aggregate results
@mmusgrov

mmusgrov commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

I believe letter E is showing results of 'JGroupsRaftSlotsStoreBenchmark.testJGroupsRaftSlotsStore:rolledBack' which is 0, but JGroupsRaftSlotsStoreBenchmark.testJGroupsRaftSlotsStore is not listed instead (which means it is not visible in the png file).

I don't understand the output, it looks like a bug in the pre-existing io.narayana.perf.product.ProductComparison.java and io.narayana.perf.product.ReportGenerator.java logic - the store comparison bencharks are getting conflated with the JTA product comparisons when they are completely independent from each other. I will investigate and get back to you.

@marcosgopen I raised an issue for this problem and pushed a fix to this PR since it needs the fix

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JGroupsRaftClusterStoreBenchmark.java`:
- Around line 118-160: Replace the fixed 500 ms sleep after the leader
pre-population loop with an explicit wait that verifies followerSlots has
applied all PREPOPULATE_SLOTS entries before benchmarking begins. Use the
existing JGroupsRaftSlots state or application/index inspection APIs available
in the class, and fail or time out clearly if follower replication does not
complete; keep readFromFollower() from starting until the follower is caught up.

In
`@narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JTAStoreBase.java`:
- Around line 109-115: In the catch block where tm.rollback() is called
unconditionally, add a guard condition to check that tm.getTransaction() is
non-null before attempting the rollback. When tm.commit() throws
RollbackException, the transaction manager removes the thread association in a
finally block before propagating the exception, so calling tm.rollback()
unconditionally triggers an IllegalStateException. Only call tm.rollback() if
the transaction is still associated with the current thread, which preserves
cleanup for cases where the transaction remains active due to enlistment
failures.

In
`@narayana/ArjunaJTA/jta/tests/classes/io/narayana/perf/product/ReportGenerator.java`:
- Around line 137-138: The key used in row.addColumn is constructed from only
the class name extracted from prod, which causes different benchmark methods and
parameter values to collide under the same key, allowing later rows to overwrite
earlier ones. Augment the key passed to row.addColumn to include the benchmark
method name and any relevant parameter values (such as allowDirtyReads) along
with the class name, ensuring each unique combination of class, method, and
parameters produces a distinct key that preserves all benchmark series in the
report.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9ff8c15c-c43a-4d44-b788-423e22945212

📥 Commits

Reviewing files that changed from the base of the PR and between 5dd3238 and b086da7.

📒 Files selected for processing (9)
  • .github/scripts/benchmark.sh
  • narayana/ArjunaJTA/jta/etc/jgroups-raft.xml
  • narayana/ArjunaJTA/jta/etc/jgroups.xml
  • narayana/ArjunaJTA/jta/pom.xml
  • narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JGroupsRaftClusterStoreBenchmark.java
  • narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JGroupsRaftSlotsStoreBenchmark.java
  • narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JGroupsSlotsStoreBenchmark.java
  • narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JTAStoreBase.java
  • narayana/ArjunaJTA/jta/tests/classes/io/narayana/perf/product/ReportGenerator.java

Comment on lines +118 to +160
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

slots_file="$(fd -a -t f '^JGroupsRaftSlots\.java$' . | head -n 1)"
if [[ -z "${slots_file}" ]]; then
  echo "JGroupsRaftSlots.java was not found." >&2
  exit 1
fi

# Inspect whether write completion requires every follower to apply the entry.
ast-grep outline "${slots_file}" --items all --match JGroupsRaftSlots
rg -n -C 12 '\b(write|read)\s*\(|ReplicatedStateMachine|allowDirtyReads|preConfigured' "${slots_file}"

# Inspect the pinned Raft dependency and related benchmark setup.
rg -n -C 4 'jgroups-raft|JGroupsRaftClusterStoreBenchmark|PREPOPULATE_SLOTS|waitForLeader' \
  narayana/ArjunaJTA/jta/pom.xml \
  narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance

Repository: jbosstm/performance

Length of output: 194


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Repo files matching JGroupsRaftSlots:"
git ls-files | rg '(^|/)JGroupsRaftSlots\.java$|JGroupsRaftClusterStoreBenchmark\.java$|jgroups-raft|JGroupsRaft' | head -200

echo
echo "Benchmark file outline:"
benchmark_file="$(git ls-files | rg 'JGroupsRaftClusterStoreBenchmark\.java$' | head -n 1 || true)"
if [[ -n "${benchmark_file}" ]]; then
  wc -l "${benchmark_file}"
  ast-grep outline "${benchmark_file}" --match JGroupsRaftClusterStoreBenchmark || true
  sed -n '1,240p' "${benchmark_file}"
fi

Repository: jbosstm/performance

Length of output: 9459


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Find JGroupsRaftSlots source:"
git ls-files | rg '(^|/)JGroupsRaftSlots\.java$' | while read -r path; do
  echo "--- $path"
  wc -l "$path"
  ast-gps="$(ast-grep outline "$path" --items all --match JGroupsRaftSlots 2>/dev/null || true)"
  if [[ -n "${ast-gps}" ]]; then
    echo "$ast-gps"
  fi
  echo
  rg -n -C 14 '\b(write|read)\s*\(|applySnapshot|applyCommit|allowDirtyReads|leader|follower|preConfigured' "$path"
done

echo
echo "Find related sources:"
git ls-files | rg 'objectstore/slot/jgroups|ReplicatedStateMachine|JGroupsRaft' | head -100

Repository: jbosstm/performance

Length of output: 187


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Find JGroupsRaftSlots references:"
rg -n 'class JGroupsRaftSlots|new JGroupsRaftSlots\(|interface JGroupsRaftSlots|implements JGroupsRaftSlots|JGroupsRaftSlots' . 

echo
echo "Find ReplicatedStateMachine references:"
rg -n 'class ReplicatedStateMachine|ReplicatedStateMachine<|allowDirtyReads|applyCommit\(|leaderCommit|commitIndex' .

echo
echo "List files under narayana ArjunaJTA path:"
git ls-files narayana/ArjunaJTA | rg 'JGroups|Raft|Slots|ReplicatedStateMachine|objectstore' | head -200

Repository: jbosstm/performance

Length of output: 6313


🌐 Web query:

JGroups Raft ReplicatedStateMachine write returns after follower application commitIndex leader commit

💡 Result:

In JGroups Raft, a write request (such as put or remove in ReplicatedStateMachine) returns to the caller only after the leader has successfully replicated the log entry to a majority of the cluster members and committed it [1][2]. Specifically, the process follows these requirements of the Raft consensus algorithm: 1. Leader Reception: The leader receives the write request from the client [1][2]. 2. Replication: The leader appends the entry to its persistent log and asynchronously replicates it to the other nodes (followers) in the cluster [1][2]. 3. Majority Agreement: The leader waits until the entry has been successfully persisted by a majority of the cluster members [1][2]. 4. Commit and Response: Once the majority acknowledgment is received, the leader updates its commitIndex, applies the entry to its state machine, and finally returns the result of the operation to the client [1][2]. The client does not need to wait for individual followers to explicitly report their local application of the commitIndex; the consensus guarantee is satisfied once the leader confirms that a majority of the cluster has persisted the entry in their logs [1][2]. If the leader cannot replicate the entry to a majority (e.g., due to network partitions or insufficient nodes), the request will time out rather than returning a successful result [2].

Citations:


🌐 Web query:

JGroups Raft ReplicatedStateMachine allowDirtyReads read stale follower implementation

💡 Result:

In JGroups Raft, the ReplicatedStateMachine allows users to trade off linearizability for performance by enabling "dirty reads," where read operations are served locally from a node's state machine rather than undergoing the full Raft consensus process [1][2]. Key aspects of this implementation include: 1. Method Configuration: You can toggle this behavior using the ReplicatedStateMachine.allowDirtyReads(boolean) method [1][2]. By default, early versions of components like CounterService allowed dirty reads to improve response times, but this behavior can be explicitly disabled (e.g., allowDirtyReads(false)) to force quorum reads [1][2]. 2. Consensus Mechanism: When allowDirtyReads is set to false (or when not enabled), read requests are routed through the RAFT protocol and require a majority consensus to complete, ensuring linearizable reads at the cost of higher latency [1]. 3. Learner Nodes: Newer versions of JGroups Raft have introduced the role of "learner" nodes [3][4]. Learner nodes are designed to scale read operations significantly by serving dirty reads from their local state machine without participating in the quorum for entry commitment or leader election [3][4]. 4. Consistency Trade-offs: Enabling dirty reads means the returned value may be stale because the local node may not have processed the most recent committed updates from the Raft leader [1][2]. Note: Some search results returned information regarding ArangoDB's allowDirtyReads feature [5][6][7]; this is distinct from the JGroups Raft implementation. Ensure your configuration specifically targets the JGroups-based API [1].

Citations:


🌐 Web query:

"source" "JGroupsRaftSlots" "JGroupsRaftStoreEnvironmentBean" "allowDirtyReads"

💡 Result:

In JGroups-raft, the allowDirtyReads(boolean) method is used to configure whether read operations on a state machine (such as a CounterService) are allowed to return potentially stale data [1][2]. When allowDirtyReads is set to true (the default in some versions), read requests are executed locally against the node's state machine without going through the Raft consensus process [1][2][3]. This provides lower latency but risks returning stale values because the local state machine may not yet have applied all committed writes [1][2][3]. When set to false, read requests are routed through the Raft protocol, requiring a majority (quorum) to complete [1]. This ensures linearizable reads (always returning the most recent committed value) but incurs higher latency due to the consensus overhead [1]. The allowDirtyReads configuration is typically accessed via state machine implementations like CounterService [1][2]. While you mentioned JGroupsRaftStoreEnvironmentBean, note that the core configuration for read consistency is primarily managed at the state machine or RaftHandle level rather than through an environment bean of that specific name [1][4]. {value: "In JGroups-raft, allowDirtyReads(boolean) controls read consistency. true enables faster, local, potentially stale reads, while false forces linearizable, quorum-based reads.", citations: [1][2][3], confidence: 0.9}

Citations:


Wait for follower replication before the read benchmark starts.

leadserSlots.write(...) returns after Raft replication/commit, not after followerSlots has applied every index. A fixed 500 ms pause does not establish that the selected follower has applied the PREPOPULATE_SLOTS writes. Add an explicit follower-application wait before JMH measurement starts, so readFromFollower() does not measure stale reads or a 0-hit path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JGroupsRaftClusterStoreBenchmark.java`
around lines 118 - 160, Replace the fixed 500 ms sleep after the leader
pre-population loop with an explicit wait that verifies followerSlots has
applied all PREPOPULATE_SLOTS entries before benchmarking begins. Use the
existing JGroupsRaftSlots state or application/index inspection APIs available
in the class, and fail or time out clearly if follower replication does not
complete; keep readFromFollower() from starting until the follower is caught up.

Comment on lines +109 to +115
} catch (Exception e) {
try {
tm.rollback();
} catch (Exception suppressed) {
e.addSuppressed(suppressed);
}
throw e;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Avoid a second rollback after tm.commit() has finished the transaction.

When tm.commit() throws RollbackException, the configured JBTM-4038 transaction manager removes the thread association before it propagates that exception. At Line 111, tm.rollback() then throws IllegalStateException, and Line 113 adds it as suppressed. This adds failure-path work to each commit-time rollback and changes the benchmark total and rolledBack throughput. (raw.githubusercontent.com)

Call tm.rollback() only if tm.getTransaction() is still non-null. This preserves cleanup for enlistment failures that leave the transaction active.

Proposed fix
         } catch (Exception e) {
             try {
-                tm.rollback();
+                if (tm.getTransaction() != null) {
+                    tm.rollback();
+                }
             } catch (Exception suppressed) {
                 e.addSuppressed(suppressed);
             }
#!/bin/bash
set -euo pipefail

impl="$(fd -a '^TransactionImple\.java$' . | sed -n '1p')"
base="$(fd -a '^BaseTransaction\.java$' . | sed -n '1p')"

# Expect: commitAndDisassociate removes the association in a finally block.
if [[ -n "$impl" ]]; then
  ast-grep outline "$impl" --items all
  rg -n -C 14 'commitAndDisassociate|removeTransaction' "$impl"
fi

# Expect: TransactionManager.rollback rejects an absent thread association.
if [[ -n "$base" ]]; then
  ast-grep outline "$base" --items all
  rg -n -C 8 '\brollback\s*\(' "$base"
fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@narayana/ArjunaJTA/jta/tests/classes/com/arjuna/ats/jta/xa/performance/JTAStoreBase.java`
around lines 109 - 115, In the catch block where tm.rollback() is called
unconditionally, add a guard condition to check that tm.getTransaction() is
non-null before attempting the rollback. When tm.commit() throws
RollbackException, the transaction manager removes the thread association in a
finally block before propagating the exception, so calling tm.rollback()
unconditionally triggers an IllegalStateException. Only call tm.rollback() if
the transaction is still associated with the current thread, which preserves
cleanup for cases where the transaction remains active due to enlistment
failures.

Comment on lines +137 to +138
// the class names follow the format: <product><PATTERN2> e.g. NarayanaComparison or DiskSlotsStoreBenchmark
row.addColumn(prod.substring(0, prod.length() - PATTERN2.length()), tput.doubleValue());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Preserve benchmark method and parameter identity.

prod contains only the class name. Row.addColumn uses it as the map key. Therefore, JGroupsRaftClusterStoreBenchmark.readFromFollower and writeToLeader, including both allowDirtyReads values, use the same JGroupsRaftCluster key for one thread count. Later rows overwrite earlier rows. The table and PNG then lose benchmark series.

Include the benchmark method and parameter values in the series key, or create separate report rows before calling row.addColumn.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@narayana/ArjunaJTA/jta/tests/classes/io/narayana/perf/product/ReportGenerator.java`
around lines 137 - 138, The key used in row.addColumn is constructed from only
the class name extracted from prod, which causes different benchmark methods and
parameter values to collide under the same key, allowing later rows to overwrite
earlier ones. Augment the key passed to row.addColumn to include the benchmark
method name and any relevant parameter values (such as allowDirtyReads) along
with the class name, ensuring each unique combination of class, method, and
parameters produces a distinct key that preserves all benchmark series in the
report.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants