Skip to content

Make FaweStreamChangeSet.blockSize atomic and document read reentrancy#3598

Open
MattBDev wants to merge 3 commits into
mainfrom
phase1/fawestreamchangeset-blocksize
Open

Make FaweStreamChangeSet.blockSize atomic and document read reentrancy#3598
MattBDev wants to merge 3 commits into
mainfrom
phase1/fawestreamchangeset-blocksize

Conversation

@MattBDev

Copy link
Copy Markdown
Contributor

Part of Phase 1 (concurrency hardening) of the com.fastasyncworldedit.core.history improvement plan. One of five independent PRs.

The bug

blockSize was a plain long, incremented from six add* methods that run on pipeline worker threads and read unsynchronized from isEmpty() / longSize() / size(). Neither the increments nor the 64-bit reads were atomic, so counts were lost and readers could observe a torn value. Since longSize() is what history reports as its size, this silently under-reports how much an edit actually recorded.

The fix

  • blockSize is now a LongAdder — chosen over AtomicLong because this path has many concurrent writers and infrequent reads, which is exactly what LongAdder optimises for.
  • RollbackOptimizedHistory's constructor is updated for the new field type. Its (int) truncation of the incoming long size is deliberately preserved — that is a separate known bug (a long silently truncated to int), and fixing it here would be out of scope. It is called out in a comment.
  • Documents that a single FaweStreamChangeSet is not safe for concurrent or overlapping read traversals: posDel, idDel, originX, originZ and version hold decoder state on the instance rather than per traversal, so a second concurrent read — or a read racing readHeader() — corrupts the running deltas. Documentation only; a runtime guard needs the per-traversal codec refactor.

Verification

  • :worldedit-core:compileJava / compileTestJava pass.
  • FaweStreamChangeSetBlockSizeTest is a genuine regression test, and the strongest evidence in this series: pre-fix it fails with expected: <16000> but was: <13070>2,930 lost increments — and passes with the fix.

Reviewer note — possible API break

blockSize is protected, so changing protected longprotected final LongAdder is a source/binary break for any third-party plugin subclassing FaweStreamChangeSet. Only RollbackOptimizedHistory uses it in-tree. If that matters for the public API, the alternative is a private LongAdder behind protected accessors — happy to switch if preferred.

Notes

🤖 Generated with Claude Code

blockSize was a plain long incremented from six add* methods that run on
pipeline worker threads and read unsynchronized from isEmpty()/longSize()/
size(). Neither the increments nor the 64-bit reads were atomic, so counts
could be lost and readers could observe a torn value. Use a LongAdder, which
stays cheap under the concurrent writers this path actually sees.

RollbackOptimizedHistory's constructor is updated for the new field type. Its
(int) truncation of the incoming long size is deliberately preserved here: it
is a separate known bug and fixing it is out of scope for this change.

Also documents that a single FaweStreamChangeSet is not safe for concurrent or
overlapping read traversals: posDel, idDel, originX, originZ and version hold
decoder state on the instance rather than per traversal, so a second
concurrent read, or a read racing readHeader(), corrupts the running deltas.
This is documentation only; a runtime guard needs the per-traversal codec
refactor and is not attempted here.

Adds FaweStreamChangeSetBlockSizeTest covering concurrent add() counting.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 15, 2026 06:06
@MattBDev
MattBDev requested a review from a team as a code owner July 15, 2026 06:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR is part of Phase 1 concurrency hardening for com.fastasyncworldedit.core.history, fixing incorrect history size reporting by making FaweStreamChangeSet.blockSize safe under concurrent writes, and documenting non-reentrant read traversal behavior.

Changes:

  • Replace blockSize from a plain long to LongAdder and update call sites/size readers to use increment()/sum().
  • Update RollbackOptimizedHistory initialization for the new blockSize type while preserving the historical (int) truncation behavior (documented as intentional).
  • Add a regression test that stresses concurrent writers to ensure longSize() remains accurate.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
worldedit-core/src/main/java/com/fastasyncworldedit/core/history/changeset/FaweStreamChangeSet.java Makes blockSize atomic via LongAdder and documents non-reentrant read traversal constraints.
worldedit-core/src/main/java/com/fastasyncworldedit/core/history/RollbackOptimizedHistory.java Adjusts constructor logic to initialize the LongAdder-based blockSize.
worldedit-core/src/test/java/com/fastasyncworldedit/core/history/changeset/FaweStreamChangeSetBlockSizeTest.java Adds a concurrent-writer regression test for the blockSize counter.
worldedit-core/build.gradle.kts Adds lz4Java to test dependencies.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread worldedit-core/build.gradle.kts
…avadoc

If a worker future threw or timed out, the test rethrew immediately from the
catch block, skipping the graceful shutdown()/awaitTermination below it and
leaking non-daemon threads into the rest of the suite. Move executor
construction outside the try and unconditionally shutdownNow() in a finally,
so a stuck or failed worker no longer leaks threads.

Also corrects the class javadoc, which claimed COMPRESSION_LEVEL was forced
to 0 because lz4-java is missing from the test classpath - inaccurate, since
this PR adds lz4-java as a test dependency. The real reason: level 0 bypasses
the compression backend entirely so the test only measures counter accuracy,
and @isolated is needed because Settings is process-global mutable state.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 15, 2026 11:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

This test's PR only migrates blockSize to a LongAdder; it doesn't include
the (separately-fixed, sibling-PR) double-checked-locking fix for
getBlockOS(). Without a single-threaded warm-up call first, the first wave
of concurrent add() calls below would race on that unrelated, still-present
lazy-init bug too, rather than exercising only the counter accuracy this
test targets. getBlockOS() itself doesn't touch blockSize, so the expected
total asserted at the end is unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 15, 2026 12:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

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