fix(common): publish queue configuration safely - #7141
dengliming wants to merge 3 commits into
Conversation
| private static final long serialVersionUID = 8032578371749960142L; | ||
|
|
||
| private int maxFreeMemory; | ||
| private volatile int maxFreeMemory; |
There was a problem hiding this comment.
volatile is the right fix and it is genuinely needed here - unlike a plain immutable field, both values have public setters (setMaxFreeMemory / setRejector), so they really are mutable shared state read from put/offer on arbitrary threads.
Two notes, neither blocking:
hasRemainedMemory()is still check-then-act:
if (!hasRemainedMemory()) { rejector.reject(e, this); return false; }
return super.offer(e);volatile gives visibility for maxFreeMemory/rejector, it does not make the memory check atomic with the enqueue. That is fine for a best-effort OOM guard - arguably publishing the config safely is exactly what was missing - but worth a comment so nobody reads volatile as "the guard is atomic".
- Same as elsewhere in this batch: asserting
Modifier.isVolatile(...)by reflection checks an implementation detail, and would silently pass even if the setter were removed and the field became effectively final. A behavioural test (e.g.setMaxFreeMemory(0)from one thread while another offers) is more valuable, though harder to make deterministic - so keeping this as a cheap regression guard is acceptable.
Aias00
left a comment
There was a problem hiding this comment.
Summary
Marks maxFreeMemory and rejector in MemorySafeLinkedBlockingQueue as volatile.
Why this is correct
These are not effectively-final fields - the class exposes setMaxFreeMemory(int) and setRejector(Rejector<E>), and both values are read on the hot path in put / offer / hasRemainedMemory from whatever thread is producing into the queue. Without volatile there is no happens-before between a configuration change and those reads, so a producer can keep using a stale threshold or a partially-published Rejector. Making both volatile is the minimal correct fix; int and reference volatile reads/writes are atomic, so no further synchronisation is needed for single-variable visibility.
Non-blocking notes (inline)
- The guard in
offer/putis still check-then-act:
if (!hasRemainedMemory()) { rejector.reject(e, this); return false; }
return super.offer(e);volatile fixes visibility, not atomicity - free memory can drop between the check and the enqueue. That is fine for a best-effort OOM guard, but worth a comment so volatile is not misread as "the guard is atomic".
testMutableConfigurationIsVolatileassertsModifier.isVolatileby reflection. It is a cheap regression guard and I will not block on it, but it tests an implementation detail: it would still pass if the setters were deleted and the fields became de-facto immutable. A behavioural test (callsetMaxFreeMemoryfrom one thread while another offers) is more meaningful if you want stronger coverage.
Approving - small, correct, and the risk is essentially zero.
Summary
Testing
mvn -q -pl shenyu-common -am -DskipTests=false -Dcheckstyle.skip=false -Dtest=MemorySafeLinkedBlockingQueueTest -DfailIfNoTests=false testFixes #6774