Fix DistributionData percentile sketch data race (AIOOBE) and partial-read deserialization bug#169
Open
Zhangyx39 wants to merge 1 commit into
Open
Fix DistributionData percentile sketch data race (AIOOBE) and partial-read deserialization bug#169Zhangyx39 wants to merge 1 commit into
Zhangyx39 wants to merge 1 commit into
Conversation
…-read deserialization bug DistributionData holds a single mutable, non-thread-safe DataSketches UpdateDoublesSketch and mutated it in place with no synchronization. The per-record writer (DistributionCell.update on the task/mailbox thread) races concurrent readers -- percentiles()/extractResult() during metric extraction and writeObject() during accumulator serialization (TM->JM), plus combine() during JobManager merge. A read (getQuantiles, which sizes a destination array from getN() then copies) concurrent with a write (update, which grows the sketch) overruns the array and throws ArrayIndexOutOfBoundsException in DoublesAuxiliary.populateFromDoublesSketch. Fix with snapshot-under-lock: guard all sketch/primitive access with a final ReentrantLock, but keep the expensive O(n) getQuantiles outside the lock by copying the bounded (k=256) sketch under the lock and computing on the immutable snapshot. A ReentrantLock (rather than synchronized on a field) is used because it is final and Serializable -- it survives the accumulator round-trip and deserializes unlocked, so no transient re-initialization is needed. combine(DistributionData) snapshots the other instance under its own lock first to stay deadlock-free and avoid aliasing a mutable sketch. Also fix a tightly-coupled deserialization bug: readObject used InputStream.read(byte[]) (reads up to len bytes) which returned a partial sketch image when the array spanned the stream buffer, corrupting the deserialized sketch and yielding wrong percentiles after TM->JM transfer. Switched to readFully. Add regression tests: a concurrency stress test (writers + readers + serializers) that reproduces the AIOOBE on the old code and passes with the fix, plus serialization round-trip and combine parity tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
DistributionData(LI Beam fork) holds a single mutable, non-thread-safe DataSketchesUpdateDoublesSketchand mutated it in place with no synchronization. The per-record writer (DistributionCell.updateon the task/mailbox thread) races concurrent readers:percentiles()/extractResult()during metric extraction (the AIOOBE site),writeObject()during accumulator serialization (TM→JM),combine(DistributionData)during JobManager accumulator merge.A read (
getQuantiles, which sizes a destination array fromgetN()then copies) concurrent with a write (update, which grows the sketch) overruns the array and throws:DataSketches
UpdateDoublesSketchis documented single-threaded, so this is a caller-side concurrency bug, independent of the datasketches version.Fix — snapshot-under-lock
private final ReentrantLock lockguarding all sketch/primitive access.ReentrantLock(rather thansynchronizedon a field) is used because it isfinal+Serializable: it survives the TM→JM accumulator round-trip and always deserializes unlocked, so notransientre-init is needed. (Also satisfies ErrorProneSynchronizeOnNonFinalFieldunder-Werror.)update,combine(long),combine(long,long,long,long),combine(DistributionData),reset.percentiles(): copies the bounded (k=256) sketch under the lock, then runs the expensive O(n)getQuantileson the immutable snapshot outside the lock — so per-record writers never block on the quantile computation.combine(DistributionData): snapshotsotherunder its own lock first (copying sketch bytes, not aliasing) → deadlock-free, no shared mutable sketch.writeObjectguarded;sum/count/min/maxgetters guarded for consistent snapshots.Additional fix (tightly coupled)
readObjectusedInputStream.read(byte[])(reads up tolenbytes), which returned a partial sketch image when the array spanned the stream buffer — corrupting the deserialized sketch and yielding wrong percentiles after the TM→JM transfer. Switched toreadFully. This was caught by the new serialization parity test.Testing
ArrayIndexOutOfBoundsException(DoublesAuxiliary.populateFromDoublesSketch→getQuantiles) on the pre-fix code and passes with the fix — the regression guard.combineparity tests.DistributionDataTesttests pass; Spotless/ErrorProne clean. Pre-existing unrelated failures inDistributionCellTest/MetricsContainerImplTestwere confirmed present on the untouched baseline.Performance
Write path adds one uncontended monitor acquire (nanoseconds, lock-elidable). Read/serialize path holds the lock only for a bounded few-KB
toByteArray()copy, not the O(n)getQuantiles. No measurable throughput regression expected — the fix removes a crash, not adds a bottleneck.