[improve][broker]Reduce the lock range of SimpleCache to enhance performance - #25293
Conversation
There was a problem hiding this comment.
@poorbarcode Is it better just fix SimpleCache so get() does not run valueSupplier under a global synchronized lock? It will avoids per-thread TableView growth/leak risk.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #25293 +/- ##
============================================
- Coverage 72.73% 72.72% -0.02%
+ Complexity 34264 33886 -378
============================================
Files 1954 1954
Lines 154792 154875 +83
Branches 17731 17741 +10
============================================
+ Hits 112586 112627 +41
- Misses 33170 33206 +36
- Partials 9036 9042 +6
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
Denovo1998
left a comment
There was a problem hiding this comment.
Can we add targeted tests for the new future lifecycle here?
The risks in this change lie not in the expected path, but in the ownership and expiration transitions:
- the cache entry is removed or expired before the future completes
- the future completes successfully after expiration and should close the reader exactly once
- the future completes exceptionally and should not try to close anything
lhotari
left a comment
There was a problem hiding this comment.
Please check the comment about using ConcurrentHasMap and moving locking to ExpirableValue class to avoid race conditions with expiration.
There was a problem hiding this comment.
Pull request overview
This PR reduces the lock contention in SimpleCache by moving the blocking reader creation out of the synchronized block. Previously, the get() method held the global lock while synchronously creating and waiting for a reader, which blocked all other threads. Now, the cache stores CompletableFuture<Reader<T>> instead of Reader<T>, and the lock is only held to insert/retrieve the future — the actual wait() call happens outside the lock.
Changes:
SimpleCachegains a newgetWithCacheInfo()method that exposes theExpirableValuewrapper, allowing callers to update the deadline after async operations complete outside the lock.TableViewnow cachesCompletableFuture<Reader<T>>and waits on the future outside the synchronized block, with updated expiration logic to handle incomplete futures.- A new test
TableViewTest.testFailedCreateReadervalidates behavior when reader creation fails.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
SimpleCache.java |
Adds getWithCacheInfo() method and makes ExpirableValue and its members public. |
TableView.java |
Changes cache type to CompletableFuture<Reader<T>>, moves blocking wait outside the lock, adds expiration handling for futures, and extracts closeReader() helper. |
TableViewTest.java |
New test for failed reader creation scenario. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
7ef90da to
9b800b2
Compare
2768824 to
037796c
Compare
I have already responded to your concern here. Please take a look first #25293 (comment) |
Answered his question and pinged him in Slack, but he did not reply for a long time
@poorbarcode I'll fix that issue separately. |
|
@lhotari Could you point to the exact test method? |
I took a closer look and in this case, it happens to be fine since TableView and SimpleCache don't keep state by starting their own threads. The allocated resources such as the Reader will get closed when the client closes. This is fine. |
…ormance (apache#25293) (cherry picked from commit 9bbea3e) (cherry picked from commit 5f347ee)
…ormance (apache#25293) (cherry picked from commit 9bbea3e) (cherry picked from commit 5f347ee)
…ormance (apache#25293) (cherry picked from commit 9bbea3e) (cherry picked from commit 5f347ee)
…ormance (apache#25293) (cherry picked from commit 9bbea3e) (cherry picked from commit 5f347ee)
Motivation
#23062 improved the behaviour of initialising the Transaction Buffer: Synchronise the reader creation, read loop and the following process on its result. Maintain only one reader for each namespace. The reader is now not closed unless there is no snapshot read request in 1 minute.
However, SimpleCache is a global lock, and the lock force is too strong, causing all
pulsar-transaction-snapshot-recoverthreads to get stuck in the creation of the first reader. When system resources are insufficient, the problem will be magnified infinitelyQ1: The creation of the reader that reads
__transaction_buffer_snapshotand the reading of existing messages are both executed synchronously. Is it necessary to change it to be completed asynchronously?A1: It is not necessary, since the initialisation of all TransactionBuffers under the same namespace requires waiting for the reader to complete the processing of all messages, whether it is asynchronous or not is not important
Q2. Will these synchronisation operations cause the thread
pulsar-transaction-snapshot-recoverto get stuck and affect other functionsA2: No. The function of this thread is quite simple: 1. Initialise Transaction Buffer. 2. After the Transaction Buffer recovery is completed, handle the accumulated transaction write operations (subsequent writes will no longer use this thread).
So the stuck functions actually all need to wait for the reader's messages to be processed, and thus, no other impacts have been caused
Modifications
No longer wait for the initialisation of the reader to complete within the lock code block
Documentation
docdoc-requireddoc-not-neededdoc-completeMatching PR in forked repository
PR in forked repository: x