Repository navigation
fix: prevent concurrent access to non-thread-safe maps in SketchBufferAggregatorHelper - #20090
zhang-arvin wants to merge 3 commits into
Conversation
…rAggregatorHelper The ParallelCombiner and StreamingMergeSortedGrouper are designed for concurrent read/write from different threads. However, SketchBufferAggregatorHelper used IdentityHashMap and Int2ObjectOpenHashMap, which are not thread-safe. Concurrent access to these maps by the writing thread (via aggregate()) and the reading thread (via get()) could corrupt the internal map structure, causing ArrayIndexOutOfBoundsException. This fix replaces IdentityHashMap with ConcurrentHashMap and Int2ObjectOpenHashMap with ConcurrentHashMap to ensure thread safety when used with ParallelCombiner. Fixes apache#18040
When ScanQueryFrameProcessor reads complex types from realtime segments, ColumnValueSelector.getObject() returns the already-serialized byte[] form rather than the deserialized object form. This causes a ClassCastException when ComplexFieldWriter tries to serialize it using ObjectStrategy.toBytes(), which expects the typed object form. This fix adds an objectToBytes() default method to ObjectStrategy that detects when the input is already byte[] and returns it directly, avoiding the unnecessary cast and re-serialization. ComplexMetricSerde.toBytes() is updated to use this new method. Fixes apache#18340.
FrankChen021
left a comment
There was a problem hiding this comment.
| Severity | Findings |
|---|---|
| P0 | 0 |
| P1 | 1 |
| P2 | 0 |
| P3 | 0 |
| Total | 1 |
Reviewed 1 of 1 changed files.
This is an automated review by Codex GPT-5.6-Luna(max)
| private final int maxIntermediateSize; | ||
| private final IdentityHashMap<ByteBuffer, Int2ObjectMap<Union>> unions = new IdentityHashMap<>(); | ||
| private final IdentityHashMap<ByteBuffer, WritableMemory> memCache = new IdentityHashMap<>(); | ||
| private final ConcurrentHashMap<ByteBuffer, ConcurrentHashMap<Integer, Union>> unions = new ConcurrentHashMap<>(); |
There was a problem hiding this comment.
[P1] Do not key caches by mutable ByteBuffers
ConcurrentHashMap uses ByteBuffer.equals/hashCode, which depend on the buffer's remaining contents. Union updates mutate those contents in place, changing the key hash after insertion. Subsequent unions.get(buf) calls can miss and get() returns SketchHolder.EMPTY; distinct equal-content buffers can also alias. Preserve identity/stable-key semantics while making access concurrent.
ConcurrentHashMap keys by ByteBuffer.equals/hashCode, which depend on the buffer's remaining contents. Union updates mutate those contents in place, so an inserted key's hash changes as the aggregation progresses: lookups miss, get() degrades to SketchHolder.EMPTY, and distinct buffers holding equal contents alias onto each other. Restore IdentityHashMap for stable identity keying and guard all access with a monitor, which also fixes the non-thread-safe access the previous ConcurrentHashMap change was meant to address. Addresses review feedback on apache#20090.
|
Thanks @FrankChen021 — this is a real correctness bug and the fix is in 8576e12. You are right that I went back to identity keying rather than trying to stabilise a content-derived key, because identity is the semantics this cache actually wants — the key is a specific arena instance, not a value: private final IdentityHashMap<ByteBuffer, Int2ObjectMap<Union>> unions = new IdentityHashMap<>();
private final IdentityHashMap<ByteBuffer, WritableMemory> memCache = new IdentityHashMap<>();
private final Object lock = new Object();
Also note Verification: |
FrankChen021
left a comment
There was a problem hiding this comment.
🟡 Changes recommended
The identity-keyed cache fix is correct for the reviewed grouper lifecycle: all IdentityHashMap and nested Int2ObjectMap access is serialized, while the existing buffer-slot ownership contract prevents getResult from racing an update on the same slot. One compatibility issue remains in the new raw-byte serialization shortcut: it is applied unconditionally to every ComplexMetricSerde, so byte[]-backed extension types can bypass their serializer and persist an invalid representation.
Reviewed 3 of 3 changed files: extensions-core/datasketches/src/main/java/org/apache/druid/query/aggregation/datasketches/theta/SketchBufferAggregatorHelper.java, processing/src/main/java/org/apache/druid/segment/data/ObjectStrategy.java, and processing/src/main/java/org/apache/druid/segment/serde/ComplexMetricSerde.java.
| Severity | Findings |
|---|---|
| P0 | 0 |
| P1 | 0 |
| P2 | 1 |
| P3 | 0 |
| Total | 1 |
This is an automated review by Codex GPT-5.6-Luna(max)
After addressing the findings or replying to the comments, you can request another review from me to trigger a new automated review.
| @Nullable | ||
| default byte[] objectToBytes(@Nullable Object val) | ||
| { | ||
| if (val instanceof byte[]) { |
There was a problem hiding this comment.
[P2] Preserve byte[] object-strategy semantics
Finding: This unconditional byte[] shortcut is now used by every ComplexMetricSerde, so a custom complex type whose legitimate intermediate object is byte[] (or whose strategy accepts byte[] and adds an envelope, compression, or type marker) silently skips its toBytes implementation. ComplexFieldWriter and ComplexFrameColumnWriter will persist the raw array, and the matching fromByteBuffer implementation can then fail or read a different value, breaking existing extension types.
Suggestion: Make the already-serialized path explicit or guard it using the strategy's declared object class, and add a regression test for a byte[]-backed strategy whose toBytes method transforms the input.
Description
Fixes #18040 - Runtime Exception when executing a query twice in a short duration.
Root Cause
The
ParallelCombinerandStreamingMergeSortedGrouperare designed for concurrent read/write from different threads. However,SketchBufferAggregatorHelperusedIdentityHashMapandInt2ObjectOpenHashMap, which are not thread-safe.Concurrent access to these maps by:
aggregate()→getOrCreateUnion())get())could corrupt the internal map structure, causing
ArrayIndexOutOfBoundsExceptionduring sketches aggregation (e.g.,Index 180 out of bounds for length 129).Fix
Replaced
IdentityHashMapwithConcurrentHashMapandInt2ObjectOpenHashMapwithConcurrentHashMapto ensure thread safety when used withParallelCombiner.Changes
extensions-core/datasketches/.../SketchBufferAggregatorHelper.java: Replace non-thread-safe maps withConcurrentHashMapKey Features/Changes
SketchBufferAggregatorHelperConcurrentHashMap.computeIfAbsentfor atomic map initializationVerification
The existing tests for
ParallelCombinerandStreamingMergeSortedGroupercover the concurrent read/write pattern. The fix ensures that Datasketches-based aggregators work correctly under parallel combining.