Skip to content

[Bug] Fix concurrent LocalTableQuery lookups returning null across files - #9500

Open
zhang-arvin wants to merge 1 commit into
apache:masterfrom
zhang-arvin:fix/local-table-query-concurrent-null-9483
Open

[Bug] Fix concurrent LocalTableQuery lookups returning null across files#9500
zhang-arvin wants to merge 1 commit into
apache:masterfrom
zhang-arvin:fix/local-table-query-concurrent-null-9483

Conversation

@zhang-arvin

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

Fix #9483: Concurrent LocalTableQuery lookups across files can return null because the SliceComparator in RowCompactedSerializer uses shared mutable reader1/reader2 fields across all threads. When two threads concurrently call compare(), the pointTo() calls interleave, producing incorrect comparison results and causing binary search to return false negatives (null).

Root Cause

LocalTableQuery creates a single LookupStoreFactory with a RowCompactedSerializer.createSliceComparator() that is shared across all LookupLevels instances (all partitions and buckets). The SliceComparator has mutable reader1 and reader2 fields that are repositioned via pointTo() on each compare() call, creating a race condition.

Fix

Convert reader1 and reader2 in SliceComparator to ThreadLocal<RowReader> instances, giving each thread its own independent reader pair.

How was this patch tested?

  • paimon-common module compiles successfully
  • LookupLevelsTest 19 tests pass
  • PrimaryKeySimpleTableTest 133 tests pass

Copilot AI left a comment

Copy link
Copy Markdown

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 fixes a concurrency race in RowCompactedSerializer.SliceComparator that could cause incorrect key comparisons during concurrent LocalTableQuery lookups, leading to intermittent false misses (null results) across different files/partitions.

Changes:

  • Replaced shared mutable RowReader instances in SliceComparator with per-thread ThreadLocal<RowReader> instances.
  • Updated compare() to use thread-local readers to avoid cross-thread pointTo() interleaving.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +738 to +746
private final int headerSizeInBytes;
private final ThreadLocal<RowReader> reader1;
private final ThreadLocal<RowReader> reader2;
private final FieldReader[] fieldReaders;

public SliceComparator(RowType rowType) {
int bitSetInBytes = calculateBitSetInBytes(rowType.getFieldCount());
this.reader1 = new RowReader(bitSetInBytes);
this.reader2 = new RowReader(bitSetInBytes);
this.headerSizeInBytes = calculateBitSetInBytes(rowType.getFieldCount());
this.reader1 = ThreadLocal.withInitial(() -> new RowReader(headerSizeInBytes));
this.reader2 = ThreadLocal.withInitial(() -> new RowReader(headerSizeInBytes));
Comment on lines 754 to +758
public int compare(MemorySlice slice1, MemorySlice slice2) {
reader1.pointTo(slice1.segment(), slice1.offset());
reader2.pointTo(slice2.segment(), slice2.offset());
RowReader r1 = reader1.get();
RowReader r2 = reader2.get();
r1.pointTo(slice1.segment(), slice1.offset());
r2.pointTo(slice2.segment(), slice2.offset());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants