JingsongLi commented on code in PR #6426:
URL: https://github.com/apache/paimon/pull/6426#discussion_r3523333943


##########
paimon-core/src/main/java/org/apache/paimon/index/PartitionIndex.java:
##########
@@ -150,6 +196,97 @@ public static PartitionIndex loadIndex(
                 throw new UncheckedIOException(e);
             }
         }
-        return new PartitionIndex(mapBuilder.build(), buckets, 
targetBucketRowNumber);
+        return new PartitionIndex(
+                mapBuilder.build(), buckets, targetBucketRowNumber, 
indexFileHandler, partition);
+    }
+
+    private boolean shouldRefreshWhenBucketNearFull(
+            long currentBucketRowCount,
+            long targetBucketRowNumber,
+            int minEmptyBucketsBeforeAsyncCheck) {
+        if (minEmptyBucketsBeforeAsyncCheck == -1) {
+            return false;
+        }
+
+        long threshold = targetBucketRowNumber - 
minEmptyBucketsBeforeAsyncCheck;
+        return currentBucketRowCount >= threshold;
+    }
+
+    private boolean isReachedTheMinRefreshInterval(final Duration duration) {
+        return lastRefreshTime == null || 
Instant.now().isAfter(lastRefreshTime.plus(duration));
+    }
+
+    /** Called when this PartitionIndex is dropped (e.g. cleanup in 
prepareCommit). */
+    public void cancelOngoingRefresh() {
+        if (refreshFuture != null && !refreshFuture.isDone()) {
+            refreshFuture.cancel(false);
+            if (LOG.isDebugEnabled()) {
+                LOG.debug("Cancelled ongoing refresh for partition {}", 
partition);
+            }
+        }
+    }
+
+    // Merges disk row count into nonFullBucketInformation, keeping 
uncommitted increments.
+    private void reconcileBucketWithDisk(int bucket, long diskNow) {
+        if (!totalBucketSet.contains(bucket)) {
+            return;
+        }
+        nonFullBucketInformation.compute(
+                bucket,
+                (b, inMemory) ->
+                        inMemory == null
+                                ? diskNow
+                                : diskNow
+                                        + (inMemory
+                                                - 
diskRowCountAtLastRefresh.getOrDefault(
+                                                        b, inMemory)));
+        diskRowCountAtLastRefresh.put(bucket, diskNow);
+    }
+
+    private void refreshBucketsFromDisk(IntPredicate bucketFilter) {
+        if (refreshFuture == null || refreshFuture.isDone()) {
+            // Reuse the shared FileOperationThreadPool (I/O-bound scan) 
rather than a dedicated
+            // executor, to avoid extra thread-pool resources and fan-out.
+            refreshFuture =
+                    CompletableFuture.runAsync(
+                                    () -> scanAndReconcile(bucketFilter),
+                                    FileOperationThreadPool.getExecutorService(

Review Comment:
   This still does not put a hard bound on queued refresh work. 
`FileOperationThreadPool.getExecutorService(...)` uses the shared cached pool 
backed by `ThreadPoolUtils.createCachedThreadPool`, whose default queue is an 
unbounded `LinkedBlockingQueue`. Since every active `PartitionIndex` can 
enqueue one refresh, a job with many near-full partitions can build an 
unbounded backlog of disk-scan tasks. Please add a global 
bound/backoff/coalescing policy, or otherwise make the refresh queue size 
controlled.



##########
paimon-core/src/main/java/org/apache/paimon/index/PartitionIndex.java:
##########
@@ -150,6 +196,97 @@ public static PartitionIndex loadIndex(
                 throw new UncheckedIOException(e);
             }
         }
-        return new PartitionIndex(mapBuilder.build(), buckets, 
targetBucketRowNumber);
+        return new PartitionIndex(
+                mapBuilder.build(), buckets, targetBucketRowNumber, 
indexFileHandler, partition);
+    }
+
+    private boolean shouldRefreshWhenBucketNearFull(
+            long currentBucketRowCount,
+            long targetBucketRowNumber,
+            int minEmptyBucketsBeforeAsyncCheck) {
+        if (minEmptyBucketsBeforeAsyncCheck == -1) {
+            return false;
+        }
+
+        long threshold = targetBucketRowNumber - 
minEmptyBucketsBeforeAsyncCheck;
+        return currentBucketRowCount >= threshold;
+    }
+
+    private boolean isReachedTheMinRefreshInterval(final Duration duration) {
+        return lastRefreshTime == null || 
Instant.now().isAfter(lastRefreshTime.plus(duration));
+    }
+
+    /** Called when this PartitionIndex is dropped (e.g. cleanup in 
prepareCommit). */
+    public void cancelOngoingRefresh() {
+        if (refreshFuture != null && !refreshFuture.isDone()) {
+            refreshFuture.cancel(false);
+            if (LOG.isDebugEnabled()) {
+                LOG.debug("Cancelled ongoing refresh for partition {}", 
partition);
+            }
+        }
+    }
+
+    // Merges disk row count into nonFullBucketInformation, keeping 
uncommitted increments.
+    private void reconcileBucketWithDisk(int bucket, long diskNow) {
+        if (!totalBucketSet.contains(bucket)) {

Review Comment:
   This is still a data race on a non-thread-safe collection. `assign` mutates 
`totalBucketSet` when it creates a new bucket, while the background refresh can 
read `totalBucketSet.contains(bucket)` here at the same time. The map update is 
now atomic, but this `LinkedHashSet` access is not. Please either capture an 
immutable owned-bucket snapshot when scheduling the refresh, or make the 
total-bucket structures explicitly thread-safe/synchronized.



##########
paimon-core/src/main/java/org/apache/paimon/index/PartitionIndex.java:
##########
@@ -54,20 +66,41 @@ public class PartitionIndex {
 
     public long lastAccessedCommitIdentifier;
 
+    private final IndexFileHandler indexFileHandler;
+
+    private final BinaryRow partition;
+
+    private volatile CompletableFuture<Void> refreshFuture;
+
+    // null = never refreshed; the first refresh is not throttled by 
min-refresh-interval.
+    private Instant lastRefreshTime;

Review Comment:
   This timestamp is still written by the async refresh thread and read by the 
assign thread without any visibility guarantee. The latest `computeIfPresent` 
change fixes the bucket-count update race, but `lastRefreshTime` can still be 
stale on the assign side, so `dynamic-bucket.min-refresh-interval` may be 
ignored or applied late. Please make this field `volatile` or guard the refresh 
state and timestamp with the same synchronization/atomic path.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to