This is an automated email from the ASF dual-hosted git repository.

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 09fc333a20 [flink] Add metrics for local partial lookup remote access 
(#8717)
09fc333a20 is described below

commit 09fc333a20f2303b9210dd2f287c021601717a2a
Author: sanshi <[email protected]>
AuthorDate: Mon Jul 20 12:03:35 2026 +0800

    [flink] Add metrics for local partial lookup remote access (#8717)
---
 .../org/apache/paimon/mergetree/LookupLevels.java  | 52 +++++++++++++---
 .../operation/metrics/PartialLookupMetrics.java    | 69 ++++++++++++++++++++++
 .../apache/paimon/table/query/LocalTableQuery.java | 29 ++++++++-
 .../metrics/PartialLookupMetricsTest.java          | 56 ++++++++++++++++++
 .../paimon/table/PrimaryKeySimpleTableTest.java    |  9 ++-
 .../flink/lookup/FileStoreLookupFunction.java      | 24 +++++++-
 .../flink/lookup/PrimaryKeyPartialLookupTable.java | 21 ++++++-
 7 files changed, 246 insertions(+), 14 deletions(-)

diff --git 
a/paimon-core/src/main/java/org/apache/paimon/mergetree/LookupLevels.java 
b/paimon-core/src/main/java/org/apache/paimon/mergetree/LookupLevels.java
index 850892a8c0..0687323e60 100644
--- a/paimon-core/src/main/java/org/apache/paimon/mergetree/LookupLevels.java
+++ b/paimon-core/src/main/java/org/apache/paimon/mergetree/LookupLevels.java
@@ -135,23 +135,40 @@ public class LookupLevels<T> implements 
Levels.DropFileCallback, Closeable {
 
     @Nullable
     public T lookup(InternalRow key, int startLevel) throws IOException {
-        return LookupUtils.lookup(levels, key, startLevel, this::lookup, 
this::lookupLevel0);
+        return lookup(key, startLevel, null);
     }
 
     @Nullable
-    private T lookupLevel0(InternalRow key, TreeSet<DataFileMeta> level0) 
throws IOException {
-        return LookupUtils.lookupLevel0(keyComparator, key, level0, 
this::lookup);
+    public T lookup(InternalRow key, int startLevel, @Nullable LookupContext 
context)
+            throws IOException {
+        return LookupUtils.lookup(
+                levels,
+                key,
+                startLevel,
+                (lookupKey, level) -> lookup(lookupKey, level, context),
+                (lookupKey, level0) -> lookupLevel0(lookupKey, level0, 
context));
+    }
+
+    @Nullable
+    private T lookupLevel0(
+            InternalRow key, TreeSet<DataFileMeta> level0, @Nullable 
LookupContext context)
+            throws IOException {
+        return LookupUtils.lookupLevel0(
+                keyComparator, key, level0, (lookupKey, file) -> 
lookup(lookupKey, file, context));
     }
 
     @Nullable
-    private T lookup(InternalRow key, SortedRun level) throws IOException {
-        return LookupUtils.lookup(keyComparator, key, level, this::lookup);
+    private T lookup(InternalRow key, SortedRun level, @Nullable LookupContext 
context)
+            throws IOException {
+        return LookupUtils.lookup(
+                keyComparator, key, level, (lookupKey, file) -> 
lookup(lookupKey, file, context));
     }
 
     @Nullable
-    private T lookup(InternalRow key, DataFileMeta file) throws IOException {
+    private T lookup(InternalRow key, DataFileMeta file, @Nullable 
LookupContext context)
+            throws IOException {
         byte[] keyBytes = serializeKey(key);
-        LookupResult lookupResult = lookupFile(file, keyBytes);
+        LookupResult lookupResult = lookupFile(file, keyBytes, context);
         byte[] valueBytes = lookupResult.valueBytes;
         if (valueBytes == null) {
             return null;
@@ -165,7 +182,9 @@ public class LookupLevels<T> implements 
Levels.DropFileCallback, Closeable {
                 file.fileName());
     }
 
-    private LookupResult lookupFile(DataFileMeta file, byte[] keyBytes) throws 
IOException {
+    private LookupResult lookupFile(
+            DataFileMeta file, byte[] keyBytes, @Nullable LookupContext 
context)
+            throws IOException {
         String fileName = file.fileName();
         LookupFile lookupFile = lookupFileCache.getIfPresent(fileName);
         LookupResult lookupResult = lookupCachedFile(fileName, lookupFile, 
keyBytes);
@@ -181,6 +200,9 @@ public class LookupLevels<T> implements 
Levels.DropFileCallback, Closeable {
                 return lookupResult;
             }
 
+            if (context != null) {
+                context.markRemoteAccessed();
+            }
             lookupFile = createLookupFile(file);
 
             try {
@@ -191,6 +213,20 @@ public class LookupLevels<T> implements 
Levels.DropFileCallback, Closeable {
         }
     }
 
+    /** Tracks whether one lookup invocation created any lookup file from 
table storage. */
+    public static class LookupContext {
+
+        private boolean remoteAccessed;
+
+        private void markRemoteAccessed() {
+            remoteAccessed = true;
+        }
+
+        public boolean remoteAccessed() {
+            return remoteAccessed;
+        }
+    }
+
     private Object lookupFileLock(String fileName) {
         return lookupFileLocks[Math.floorMod(fileName.hashCode(), 
lookupFileLocks.length)];
     }
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/operation/metrics/PartialLookupMetrics.java
 
b/paimon-core/src/main/java/org/apache/paimon/operation/metrics/PartialLookupMetrics.java
new file mode 100644
index 0000000000..23aab2ec79
--- /dev/null
+++ 
b/paimon-core/src/main/java/org/apache/paimon/operation/metrics/PartialLookupMetrics.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.operation.metrics;
+
+import org.apache.paimon.annotation.VisibleForTesting;
+import org.apache.paimon.metrics.Counter;
+import org.apache.paimon.metrics.MetricGroup;
+import org.apache.paimon.metrics.MetricRegistry;
+
+/**
+ * Request-level metrics for local partial lookup. A remote access means that 
at least one lookup
+ * file had to be created from table storage during the request.
+ */
+public class PartialLookupMetrics {
+
+    public static final String GROUP_NAME = "lookup";
+    public static final String PARTIAL_LOOKUP_COUNT = "partialLookupCount";
+    public static final String PARTIAL_LOOKUP_REMOTE_ACCESS_COUNT =
+            "partialLookupRemoteAccessCount";
+
+    private final MetricGroup metricGroup;
+    private final Counter lookupCount;
+    private final Counter remoteAccessCount;
+
+    public PartialLookupMetrics(MetricRegistry registry, String tableName) {
+        this.metricGroup = registry.createTableMetricGroup(GROUP_NAME, 
tableName);
+        this.lookupCount = metricGroup.counter(PARTIAL_LOOKUP_COUNT);
+        this.remoteAccessCount = 
metricGroup.counter(PARTIAL_LOOKUP_REMOTE_ACCESS_COUNT);
+    }
+
+    /** Reports one lookup invocation and whether it accessed table storage. */
+    public void reportLookup(boolean remoteAccessed) {
+        lookupCount.inc();
+        if (remoteAccessed) {
+            remoteAccessCount.inc();
+        }
+    }
+
+    @VisibleForTesting
+    public MetricGroup metricGroup() {
+        return metricGroup;
+    }
+
+    @VisibleForTesting
+    public long lookupCount() {
+        return lookupCount.getCount();
+    }
+
+    @VisibleForTesting
+    public long remoteAccessCount() {
+        return remoteAccessCount.getCount();
+    }
+}
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/table/query/LocalTableQuery.java 
b/paimon-core/src/main/java/org/apache/paimon/table/query/LocalTableQuery.java
index fdc57861b6..b42e16f5af 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/table/query/LocalTableQuery.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/table/query/LocalTableQuery.java
@@ -40,6 +40,7 @@ import org.apache.paimon.mergetree.LookupLevels;
 import org.apache.paimon.mergetree.lookup.LookupSerializerFactory;
 import org.apache.paimon.mergetree.lookup.PersistValueProcessor;
 import org.apache.paimon.mergetree.lookup.RemoteLookupFileManager;
+import org.apache.paimon.operation.metrics.PartialLookupMetrics;
 import org.apache.paimon.options.Options;
 import org.apache.paimon.reader.RecordReader;
 import org.apache.paimon.table.FileStoreTable;
@@ -89,6 +90,8 @@ public class LocalTableQuery implements TableQuery {
 
     @Nullable private Filter<InternalRow> cacheRowFilter;
 
+    @Nullable private PartialLookupMetrics partialLookupMetrics;
+
     public LocalTableQuery(FileStoreTable table) {
         this.options = table.coreOptions();
         this.tableView = new ConcurrentHashMap<>();
@@ -221,6 +224,25 @@ public class LocalTableQuery implements TableQuery {
     @Nullable
     @Override
     public InternalRow lookup(BinaryRow partition, int bucket, InternalRow 
key) throws IOException {
+        PartialLookupMetrics currentMetrics = partialLookupMetrics;
+        LookupLevels.LookupContext context =
+                currentMetrics == null ? null : new 
LookupLevels.LookupContext();
+        try {
+            return lookup(partition, bucket, key, context);
+        } finally {
+            if (currentMetrics != null) {
+                currentMetrics.reportLookup(context != null && 
context.remoteAccessed());
+            }
+        }
+    }
+
+    @Nullable
+    private InternalRow lookup(
+            BinaryRow partition,
+            int bucket,
+            InternalRow key,
+            @Nullable LookupLevels.LookupContext context)
+            throws IOException {
         Map<Integer, BucketLookupState> buckets = tableView.get(partition);
         if (buckets == null || buckets.isEmpty()) {
             return null;
@@ -237,7 +259,7 @@ public class LocalTableQuery implements TableQuery {
                 return null;
             }
 
-            KeyValue kv = lookupLevels.lookup(key, startLevel);
+            KeyValue kv = lookupLevels.lookup(key, startLevel, context);
             if (kv == null || kv.valueKind().isRetract()) {
                 return null;
             } else {
@@ -264,6 +286,11 @@ public class LocalTableQuery implements TableQuery {
         return this;
     }
 
+    public LocalTableQuery withMetrics(@Nullable PartialLookupMetrics metrics) 
{
+        this.partialLookupMetrics = metrics;
+        return this;
+    }
+
     @Override
     public InternalRowSerializer createValueSerializer() {
         return 
InternalSerializers.create(readerFactoryBuilder.readValueType());
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/operation/metrics/PartialLookupMetricsTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/operation/metrics/PartialLookupMetricsTest.java
new file mode 100644
index 0000000000..1cfa34cfb1
--- /dev/null
+++ 
b/paimon-core/src/test/java/org/apache/paimon/operation/metrics/PartialLookupMetricsTest.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.operation.metrics;
+
+import org.apache.paimon.metrics.Metric;
+import org.apache.paimon.metrics.MetricGroup;
+import org.apache.paimon.metrics.TestMetricRegistry;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Tests for {@link PartialLookupMetrics}. */
+public class PartialLookupMetricsTest {
+
+    @Test
+    public void testRegistrationAndReporting() {
+        PartialLookupMetrics metrics =
+                new PartialLookupMetrics(new TestMetricRegistry(), "myTable");
+        MetricGroup metricGroup = metrics.metricGroup();
+
+        
assertThat(metricGroup.getGroupName()).isEqualTo(PartialLookupMetrics.GROUP_NAME);
+        assertThat(metricGroup.getAllVariables()).containsEntry("table", 
"myTable");
+        Map<String, Metric> registeredMetrics = metricGroup.getMetrics();
+        assertThat(registeredMetrics.keySet())
+                .containsExactlyInAnyOrder(
+                        PartialLookupMetrics.PARTIAL_LOOKUP_COUNT,
+                        
PartialLookupMetrics.PARTIAL_LOOKUP_REMOTE_ACCESS_COUNT);
+
+        metrics.reportLookup(false);
+        assertThat(metrics.lookupCount()).isEqualTo(1);
+        assertThat(metrics.remoteAccessCount()).isZero();
+
+        metrics.reportLookup(true);
+        assertThat(metrics.lookupCount()).isEqualTo(2);
+        assertThat(metrics.remoteAccessCount()).isEqualTo(1);
+    }
+}
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/table/PrimaryKeySimpleTableTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/table/PrimaryKeySimpleTableTest.java
index dd3a21b86c..ea8f096a53 100644
--- 
a/paimon-core/src/test/java/org/apache/paimon/table/PrimaryKeySimpleTableTest.java
+++ 
b/paimon-core/src/test/java/org/apache/paimon/table/PrimaryKeySimpleTableTest.java
@@ -36,8 +36,10 @@ import org.apache.paimon.io.DataFileMeta;
 import org.apache.paimon.manifest.FileKind;
 import org.apache.paimon.manifest.ManifestEntry;
 import org.apache.paimon.manifest.ManifestFileMeta;
+import org.apache.paimon.metrics.TestMetricRegistry;
 import org.apache.paimon.operation.AbstractFileStoreWrite;
 import org.apache.paimon.operation.FileStoreScan;
+import org.apache.paimon.operation.metrics.PartialLookupMetrics;
 import org.apache.paimon.options.MemorySize;
 import org.apache.paimon.options.Options;
 import org.apache.paimon.postpone.PostponeBucketFileStoreWrite;
@@ -2381,7 +2383,10 @@ public class PrimaryKeySimpleTableTest extends 
SimpleTableTestBase {
         }
 
         // full value (no projection) -> downloader is wired -> lookup 
succeeds via download
-        LocalTableQuery query = 
table.newLocalTableQuery().withIOManager(ioManager);
+        PartialLookupMetrics metrics =
+                new PartialLookupMetrics(new TestMetricRegistry(), 
table.name());
+        LocalTableQuery query =
+                
table.newLocalTableQuery().withIOManager(ioManager).withMetrics(metrics);
         for (DataSplit split : dataSplits) {
             query.refreshFiles(
                     split.partition(), split.bucket(), 
Collections.emptyList(), split.dataFiles());
@@ -2394,6 +2399,8 @@ public class PrimaryKeySimpleTableTest extends 
SimpleTableTestBase {
         assertThat(value).isNotNull();
         assertThat(BATCH_ROW_TO_STRING.apply(value))
                 .isEqualTo("1|20|200|binary|varbinary|mapKey:mapVal|multiset");
+        assertThat(metrics.lookupCount()).isEqualTo(2);
+        assertThat(metrics.remoteAccessCount()).isEqualTo(1);
         query.close();
 
         // value projection -> remote sst (full value) is unsafe to reuse, so 
the downloader is
diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/lookup/FileStoreLookupFunction.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/lookup/FileStoreLookupFunction.java
index 77d74526a6..f7c6fb2d67 100644
--- 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/lookup/FileStoreLookupFunction.java
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/lookup/FileStoreLookupFunction.java
@@ -28,8 +28,11 @@ import org.apache.paimon.flink.FlinkRowData;
 import org.apache.paimon.flink.FlinkRowDataWithBlob;
 import org.apache.paimon.flink.FlinkRowWrapper;
 import org.apache.paimon.flink.lookup.partitioner.ShuffleStrategy;
+import org.apache.paimon.flink.metrics.FlinkMetricRegistry;
 import org.apache.paimon.flink.utils.RuntimeContextUtils;
 import org.apache.paimon.flink.utils.TableScanUtils;
+import org.apache.paimon.metrics.MetricRegistry;
+import org.apache.paimon.operation.metrics.PartialLookupMetrics;
 import org.apache.paimon.options.Options;
 import org.apache.paimon.predicate.Predicate;
 import org.apache.paimon.table.ChainGroupReadTable;
@@ -103,6 +106,8 @@ public class FileStoreLookupFunction implements 
Serializable, Closeable {
     private transient File path;
     private transient String tmpDirectory;
     private transient LookupTable lookupTable;
+    @Nullable private transient MetricRegistry metricRegistry;
+    @Nullable private transient PartialLookupMetrics partialLookupMetrics;
 
     // partition refresh
     @Nullable private transient PartitionRefresher partitionRefresher;
@@ -179,6 +184,7 @@ public class FileStoreLookupFunction implements 
Serializable, Closeable {
 
     public void open(FunctionContext context) throws Exception {
         this.functionContext = context;
+        this.metricRegistry = new 
FlinkMetricRegistry(context.getMetricGroup());
         this.tmpDirectory = getTmpDirectory(context);
         open(tmpDirectory);
     }
@@ -225,7 +231,12 @@ public class FileStoreLookupFunction implements 
Serializable, Closeable {
                 try {
                     this.lookupTable =
                             PrimaryKeyPartialLookupTable.createLocalTable(
-                                    table, projection, path, joinKeys, 
getRequireCachedBucketIds());
+                                    table,
+                                    projection,
+                                    path,
+                                    joinKeys,
+                                    getRequireCachedBucketIds(),
+                                    this::partialLookupMetrics);
                     LOG.info(
                             "Remote service isn't available. Created 
PrimaryKeyPartialLookupTable with LocalQueryExecutor.");
                 } catch (UnsupportedOperationException e) {
@@ -277,6 +288,17 @@ public class FileStoreLookupFunction implements 
Serializable, Closeable {
         lookupTable.open();
     }
 
+    @Nullable
+    private PartialLookupMetrics partialLookupMetrics() {
+        if (metricRegistry == null) {
+            return null;
+        }
+        if (partialLookupMetrics == null) {
+            partialLookupMetrics = new PartialLookupMetrics(metricRegistry, 
table.name());
+        }
+        return partialLookupMetrics;
+    }
+
     @Nullable
     private Predicate createProjectedPredicate(int[] projection) {
         Predicate adjustedPredicate = null;
diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/lookup/PrimaryKeyPartialLookupTable.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/lookup/PrimaryKeyPartialLookupTable.java
index 9964a8fa6c..c88112844f 100644
--- 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/lookup/PrimaryKeyPartialLookupTable.java
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/lookup/PrimaryKeyPartialLookupTable.java
@@ -28,6 +28,7 @@ import org.apache.paimon.data.InternalRow;
 import org.apache.paimon.disk.IOManagerImpl;
 import org.apache.paimon.flink.query.RemoteTableQuery;
 import org.apache.paimon.io.DataFileMeta;
+import org.apache.paimon.operation.metrics.PartialLookupMetrics;
 import org.apache.paimon.predicate.Predicate;
 import org.apache.paimon.schema.TableSchema;
 import org.apache.paimon.table.BucketMode;
@@ -53,6 +54,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Supplier;
 
 import static java.util.Collections.emptyList;
 import static org.apache.paimon.table.BucketMode.POSTPONE_BUCKET;
@@ -217,6 +219,16 @@ public class PrimaryKeyPartialLookupTable implements 
LookupTable {
             File tempPath,
             List<String> joinKey,
             Set<Integer> requireCachedBucketIds) {
+        return createLocalTable(table, projection, tempPath, joinKey, 
requireCachedBucketIds, null);
+    }
+
+    public static PrimaryKeyPartialLookupTable createLocalTable(
+            FileStoreTable table,
+            int[] projection,
+            File tempPath,
+            List<String> joinKey,
+            Set<Integer> requireCachedBucketIds,
+            @Nullable Supplier<PartialLookupMetrics> metricsSupplier) {
         return new PrimaryKeyPartialLookupTable(
                 (filter, cacheRowFilter) ->
                         new LocalQueryExecutor(
@@ -225,7 +237,8 @@ public class PrimaryKeyPartialLookupTable implements 
LookupTable {
                                 tempPath,
                                 filter,
                                 requireCachedBucketIds,
-                                cacheRowFilter),
+                                cacheRowFilter,
+                                metricsSupplier == null ? null : 
metricsSupplier.get()),
                 table,
                 joinKey);
     }
@@ -273,11 +286,13 @@ public class PrimaryKeyPartialLookupTable implements 
LookupTable {
                 File tempPath,
                 @Nullable Predicate filter,
                 Set<Integer> requireCachedBucketIds,
-                @Nullable Filter<InternalRow> cacheRowFilter) {
+                @Nullable Filter<InternalRow> cacheRowFilter,
+                @Nullable PartialLookupMetrics metrics) {
             this.tableQuery =
                     table.newLocalTableQuery()
                             .withValueProjection(projection)
-                            .withIOManager(new 
IOManagerImpl(tempPath.toString()));
+                            .withIOManager(new 
IOManagerImpl(tempPath.toString()))
+                            .withMetrics(metrics);
 
             if (cacheRowFilter != null) {
                 this.tableQuery.withCacheRowFilter(cacheRowFilter);

Reply via email to