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

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


The following commit(s) were added to refs/heads/master by this push:
     new d2fcb5383ab Fix delayed query cleanup during file metadata scans 
(#18679)
d2fcb5383ab is described below

commit d2fcb5383abffc2de168bd9bcefb4004a4c12693
Author: Jackie Tien <[email protected]>
AuthorDate: Sun Sep 20 17:17:53 2026 +0800

    Fix delayed query cleanup during file metadata scans (#18679)
---
 .../iotdb/db/i18n/DataNodeQueryMessages.java       |   3 +
 .../iotdb/db/i18n/DataNodeQueryMessages.java       |   3 +
 .../execution/operator/source/SeriesScanUtil.java  |  24 ++
 .../source/SeriesScanUtilCancellationTest.java     | 399 +++++++++++++++++++++
 4 files changed, 429 insertions(+)

diff --git 
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
 
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
index ef13dccd8ac..ec4f7760f58 100644
--- 
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
+++ 
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
@@ -21,6 +21,9 @@ package org.apache.iotdb.db.i18n;
 
 public final class DataNodeQueryMessages {
 
+  public static final String 
EXCEPTION_FRAGMENT_INSTANCE_ARG_IS_ALREADY_ARG_B44984B4 =
+      "Fragment instance %s is already %s";
+
   public static final String 
EXCEPTION_INVALID_THRIFT_MAXIMUM_FRAME_SIZE_ARG_FROM_ARG_A639588B =
       "Invalid Thrift maximum frame size %d from %s";
   public static final String 
MESSAGE_FAILED_TO_DISPATCH_LOAD_COMMAND_ARG_TO_NODE_ARG_BECAUSE_OF_EXCEPTION_ARG_2D8A483D
 =
diff --git 
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
 
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
index f9dde6564ce..c546cfae8c3 100644
--- 
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
+++ 
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
@@ -21,6 +21,9 @@ package org.apache.iotdb.db.i18n;
 
 public final class DataNodeQueryMessages {
 
+  public static final String 
EXCEPTION_FRAGMENT_INSTANCE_ARG_IS_ALREADY_ARG_B44984B4 =
+      "Fragment instance %s 已处于 %s 状态";
+
   public static final String 
EXCEPTION_INVALID_THRIFT_MAXIMUM_FRAME_SIZE_ARG_FROM_ARG_A639588B =
       "Thrift 最大帧大小 %d 无效,来源:%s";
   public static final String 
MESSAGE_FAILED_TO_DISPATCH_LOAD_COMMAND_ARG_TO_NODE_ARG_BECAUSE_OF_EXCEPTION_ARG_2D8A483D
 =
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/source/SeriesScanUtil.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/source/SeriesScanUtil.java
index 1b8ca2b1020..9f54bd63839 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/source/SeriesScanUtil.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/source/SeriesScanUtil.java
@@ -25,6 +25,7 @@ import org.apache.iotdb.commons.path.NonAlignedFullPath;
 import org.apache.iotdb.db.exception.CorruptedTsFileException;
 import org.apache.iotdb.db.i18n.DataNodeQueryMessages;
 import 
org.apache.iotdb.db.queryengine.execution.fragment.FragmentInstanceContext;
+import 
org.apache.iotdb.db.queryengine.execution.fragment.FragmentInstanceState;
 import org.apache.iotdb.db.queryengine.execution.fragment.QueryContext;
 import org.apache.iotdb.db.queryengine.metric.SeriesScanCostMetricSet;
 import 
org.apache.iotdb.db.queryengine.plan.analyze.cache.schema.DataNodeTTLCache;
@@ -285,6 +286,7 @@ public class SeriesScanUtil implements Accountable {
   // Optional.empty(), it needs to return directly to the checkpoint method 
that checks the operator
   // execution time slice.
   public Optional<Boolean> hasNextFile() throws IOException {
+    checkFragmentInstanceState();
     if (runtimeFilterExhausted || !paginationController.hasCurLimit()) {
       return Optional.of(false);
     }
@@ -2016,8 +2018,10 @@ public class SeriesScanUtil implements Accountable {
   }
 
   private Optional<ITimeSeriesMetadata> unpackSeqTsFileResource() throws 
IOException {
+    checkFragmentInstanceState();
     ITimeSeriesMetadata timeseriesMetadata =
         loadTimeSeriesMetadata(orderUtils.getNextSeqFileResource(true), true);
+    checkFragmentInstanceState();
     // skip if data type is mismatched which may be caused by delete
     if (timeseriesMetadata != null && 
timeseriesMetadata.typeMatch(getTsDataTypeList())) {
       timeseriesMetadata.setSeq(true);
@@ -2029,8 +2033,10 @@ public class SeriesScanUtil implements Accountable {
   }
 
   private Optional<ITimeSeriesMetadata> unpackUnseqTsFileResource() throws 
IOException {
+    checkFragmentInstanceState();
     ITimeSeriesMetadata timeseriesMetadata =
         loadTimeSeriesMetadata(orderUtils.getNextUnseqFileResource(true), 
false);
+    checkFragmentInstanceState();
     // skip if data type is mismatched which may be caused by delete
     if (timeseriesMetadata != null && 
timeseriesMetadata.typeMatch(getTsDataTypeList())) {
       timeseriesMetadata.setSeq(false);
@@ -2041,6 +2047,24 @@ public class SeriesScanUtil implements Accountable {
     }
   }
 
+  private void checkFragmentInstanceState() throws IOException {
+    // Compaction also uses this scanner, but its context has no fragment 
state machine.
+    if (context.getStateMachine() == null) {
+      return;
+    }
+    FragmentInstanceState state = context.getStateMachine().getState();
+    if (state.isDone()) {
+      // A scan over many overlapping files may stay in one operator call long 
after cancellation.
+      // Exit on the driver thread so it can release its lock and finish 
resource cleanup.
+      throw new IOException(
+          String.format(
+              
DataNodeQueryMessages.EXCEPTION_FRAGMENT_INSTANCE_ARG_IS_ALREADY_ARG_B44984B4,
+              context.getId(),
+              state),
+          context.getFailureCause().orElse(null));
+    }
+  }
+
   protected ITimeSeriesMetadata loadTimeSeriesMetadata(TsFileResource 
resource, boolean isSeq)
       throws IOException {
     return FileLoaderUtils.loadTimeSeriesMetadata(
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/operator/source/SeriesScanUtilCancellationTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/operator/source/SeriesScanUtilCancellationTest.java
new file mode 100644
index 00000000000..a32a83e1e02
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/operator/source/SeriesScanUtilCancellationTest.java
@@ -0,0 +1,399 @@
+/*
+ * 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.iotdb.db.queryengine.execution.operator.source;
+
+import org.apache.iotdb.calc.execution.operator.Operator;
+import org.apache.iotdb.calc.plan.planner.memory.MemoryReservationManager;
+import org.apache.iotdb.commons.exception.QueryTimeoutException;
+import org.apache.iotdb.commons.path.AlignedFullPath;
+import org.apache.iotdb.commons.path.NonAlignedFullPath;
+import org.apache.iotdb.db.conf.IoTDBDescriptor;
+import org.apache.iotdb.db.queryengine.common.FragmentInstanceId;
+import org.apache.iotdb.db.queryengine.common.PlanFragmentId;
+import org.apache.iotdb.db.queryengine.common.QueryId;
+import org.apache.iotdb.db.queryengine.execution.driver.DataDriver;
+import org.apache.iotdb.db.queryengine.execution.driver.DataDriverContext;
+import 
org.apache.iotdb.db.queryengine.execution.exchange.MPPDataExchangeManager;
+import org.apache.iotdb.db.queryengine.execution.exchange.sink.ISink;
+import 
org.apache.iotdb.db.queryengine.execution.fragment.FragmentInstanceContext;
+import 
org.apache.iotdb.db.queryengine.execution.fragment.FragmentInstanceExecution;
+import 
org.apache.iotdb.db.queryengine.execution.fragment.FragmentInstanceState;
+import 
org.apache.iotdb.db.queryengine.execution.fragment.FragmentInstanceStateMachine;
+import org.apache.iotdb.db.queryengine.execution.schedule.IDriverScheduler;
+import 
org.apache.iotdb.db.queryengine.plan.planner.plan.parameter.SeriesScanOptions;
+import org.apache.iotdb.db.queryengine.plan.statement.component.Ordering;
+import org.apache.iotdb.db.storageengine.dataregion.read.QueryDataSource;
+import org.apache.iotdb.db.storageengine.dataregion.tsfile.TsFileResource;
+
+import io.airlift.units.Duration;
+import org.apache.tsfile.enums.TSDataType;
+import org.apache.tsfile.file.metadata.AbstractAlignedTimeSeriesMetadata;
+import org.apache.tsfile.file.metadata.IDeviceID;
+import org.apache.tsfile.file.metadata.ITimeSeriesMetadata;
+import org.apache.tsfile.file.metadata.statistics.IntegerStatistics;
+import org.apache.tsfile.write.schema.MeasurementSchema;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.function.IntConsumer;
+
+import static org.apache.iotdb.calc.execution.operator.Operator.NOT_BLOCKED;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@RunWith(Parameterized.class)
+public class SeriesScanUtilCancellationTest {
+
+  private final boolean aligned;
+  private final boolean ignoreAllNullRows;
+  private final IDeviceID device;
+  private int loadedFiles;
+  private int previousDataNodeId;
+
+  @Parameterized.Parameters(name = "aligned={0}, ignoreAllNullRows={1}")
+  public static Object[][] parameters() {
+    return new Object[][] {{false, true}, {true, true}, {true, false}};
+  }
+
+  public SeriesScanUtilCancellationTest(boolean aligned, boolean 
ignoreAllNullRows) {
+    this.aligned = aligned;
+    this.ignoreAllNullRows = ignoreAllNullRows;
+    this.device =
+        IDeviceID.Factory.DEFAULT_FACTORY.create(
+            ignoreAllNullRows ? new String[] {"root.sg", "d"} : new String[] 
{"table", "d"});
+  }
+
+  @Before
+  public void setUp() {
+    previousDataNodeId = 
IoTDBDescriptor.getInstance().getConfig().getDataNodeId();
+    // Resource cleanup initializes the query metrics and their coordinator.
+    IoTDBDescriptor.getInstance().getConfig().setDataNodeId(1);
+  }
+
+  @After
+  public void tearDown() {
+    
IoTDBDescriptor.getInstance().getConfig().setDataNodeId(previousDataNodeId);
+  }
+
+  @Test
+  public void testTerminalStateBeforeScanDoesNotLoadMetadata() {
+    for (FragmentInstanceState state :
+        new FragmentInstanceState[] {
+          FragmentInstanceState.CANCELLED,
+          FragmentInstanceState.ABORTED,
+          FragmentInstanceState.FINISHED,
+          FragmentInstanceState.FAILED
+        }) {
+      FragmentInstanceContext context = newContext(Runnable::run);
+      SeriesScanUtil scanner = newScanner(context, Ordering.ASC, false, 3, 
count -> {});
+      switch (state) {
+        case CANCELLED:
+          context.cancel();
+          break;
+        case ABORTED:
+          context.abort();
+          break;
+        case FINISHED:
+          context.finished();
+          break;
+        case FAILED:
+          context.failed(new QueryTimeoutException());
+          break;
+        default:
+          throw new AssertionError(state);
+      }
+
+      IOException exception = assertThrows(IOException.class, 
scanner::hasNextFile);
+      assertSame(context.getFailureCause().orElse(null), exception.getCause());
+      assertEquals(0, loadedFiles);
+      assertEquals(state, context.getStateMachine().getState());
+    }
+  }
+
+  @Test
+  public void testCancellationDuringOverlappingFileScan() {
+    for (Ordering ordering : new Ordering[] {Ordering.ASC, Ordering.DESC}) {
+      for (boolean sequence : new boolean[] {true, false}) {
+        FragmentInstanceContext context = newContext(Runnable::run);
+        SeriesScanUtil scanner =
+            newScanner(
+                context,
+                ordering,
+                sequence,
+                4,
+                count -> {
+                  if (count == 2) {
+                    context.cancel();
+                  }
+                });
+
+        assertThrows(IOException.class, scanner::hasNextFile);
+        assertEquals(2, loadedFiles);
+        assertEquals(FragmentInstanceState.CANCELLED, 
context.getStateMachine().getState());
+      }
+    }
+  }
+
+  @Test
+  public void testTimeoutDuringLastFileReadPreservesCause() {
+    for (boolean sequence : new boolean[] {true, false}) {
+      FragmentInstanceContext context = newContext(Runnable::run);
+      QueryTimeoutException timeout = new QueryTimeoutException();
+      SeriesScanUtil scanner =
+          newScanner(context, Ordering.ASC, sequence, 1, count -> 
context.failed(timeout));
+
+      IOException exception = assertThrows(IOException.class, 
scanner::hasNextFile);
+      assertSame(timeout, exception.getCause());
+      assertEquals(1, loadedFiles);
+      assertEquals(FragmentInstanceState.FAILED, 
context.getStateMachine().getState());
+    }
+  }
+
+  @Test
+  public void testActiveQueryStillLoadsOverlappingFiles() throws IOException {
+    for (boolean flushing : new boolean[] {false, true}) {
+      FragmentInstanceContext context = newContext(Runnable::run);
+      if (flushing) {
+        context.transitionToFlushing();
+      }
+      SeriesScanUtil scanner = newScanner(context, Ordering.ASC, false, 4, 
count -> {});
+
+      assertTrue(scanner.hasNextFile().get());
+      assertEquals(4, loadedFiles);
+      assertEquals(
+          flushing ? FragmentInstanceState.FLUSHING : 
FragmentInstanceState.RUNNING,
+          context.getStateMachine().getState());
+    }
+  }
+
+  @Test
+  public void testCancellationWithCachedMetadata() throws IOException {
+    FragmentInstanceContext context = newContext(Runnable::run);
+    SeriesScanUtil scanner = newScanner(context, Ordering.ASC, false, 4, count 
-> {});
+    assertTrue(scanner.hasNextFile().get());
+    context.cancel();
+
+    assertThrows(IOException.class, scanner::hasNextFile);
+    assertEquals(4, loadedFiles);
+  }
+
+  @Test
+  public void testCancellationDuringMissingMetadataRead() {
+    for (boolean sequence : new boolean[] {true, false}) {
+      FragmentInstanceContext context = newContext(Runnable::run);
+      SeriesScanUtil scanner =
+          newScanner(context, Ordering.ASC, sequence, 1, count -> 
context.cancel(), true);
+
+      assertThrows(IOException.class, scanner::hasNextFile);
+      assertEquals(1, loadedFiles);
+    }
+  }
+
+  @Test
+  public void testCompactionContextWithoutStateMachine() throws IOException {
+    FragmentInstanceContext context =
+        FragmentInstanceContext.createFragmentInstanceContextForCompaction(0);
+    SeriesScanUtil scanner = newScanner(context, Ordering.ASC, false, 2, count 
-> {});
+
+    assertTrue(scanner.hasNextFile().get());
+    assertEquals(2, loadedFiles);
+  }
+
+  @Test(timeout = 15000)
+  public void testTimeoutUnblocksDriverResourceCleanup() throws Exception {
+    ExecutorService notifications = Executors.newSingleThreadExecutor();
+    FragmentInstanceContext context = newContext(notifications);
+    context.initializeNumOfDrivers(1);
+    try {
+      CountDownLatch closeRequested = new CountDownLatch(1);
+      QueryTimeoutException timeout = new QueryTimeoutException();
+      SeriesScanUtil scanner =
+          newScanner(
+              context,
+              Ordering.ASC,
+              false,
+              4,
+              count -> {
+                if (count == 2) {
+                  context.failed(timeout);
+                  try {
+                    // The notification thread has requested close while this 
thread owns the
+                    // driver lock, just as in a query that is still loading 
metadata.
+                    assertTrue(closeRequested.await(5, TimeUnit.SECONDS));
+                  } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                    throw new AssertionError(e);
+                  }
+                }
+              });
+      Operator operator = mock(Operator.class);
+      doReturn(NOT_BLOCKED).when(operator).isBlocked();
+      when(operator.hasNextWithTimer()).thenReturn(true);
+      when(operator.nextWithTimer())
+          .thenAnswer(
+              invocation -> {
+                scanner.hasNextFile();
+                return null;
+              });
+      ISink sink = mock(ISink.class);
+      doReturn(NOT_BLOCKED).when(sink).isFull();
+      DataDriverContext driverContext = new DataDriverContext(context, 0);
+      driverContext.setSink(sink);
+      DataDriver driver =
+          new DataDriver(operator, driverContext, 0) {
+            @Override
+            public void close() {
+              super.close();
+              closeRequested.countDown();
+            }
+          };
+      MPPDataExchangeManager exchangeManager = 
mock(MPPDataExchangeManager.class);
+      FragmentInstanceExecution.createFragmentInstanceExecution(
+          mock(IDriverScheduler.class),
+          context.getId(),
+          context,
+          Collections.singletonList(driver),
+          sink,
+          context.getStateMachine(),
+          1000,
+          false,
+          exchangeManager);
+
+      assertSame(
+          timeout,
+          assertThrows(
+              QueryTimeoutException.class,
+              () -> driver.processFor(new Duration(1, TimeUnit.SECONDS))));
+      // This can complete only after the preceding cleanup callback gets past 
allDriversClosed.
+      notifications.submit(() -> {}).get(5, TimeUnit.SECONDS);
+      assertEquals(2, loadedFiles);
+      verify(operator).close();
+      verify(context.getMemoryReservationContext()).releaseAllReservedMemory();
+      verify(exchangeManager)
+          .deRegisterFragmentInstanceFromMemoryPool(
+              context.getId().getQueryId().getId(), 
context.getId().getFragmentInstanceId(), true);
+    } finally {
+      // Ensure a failed assertion cannot leave the notification thread 
waiting on the latch.
+      context.decrementNumOfUnClosedDriver();
+      notifications.shutdownNow();
+      assertTrue(notifications.awaitTermination(5, TimeUnit.SECONDS));
+    }
+  }
+
+  private FragmentInstanceContext newContext(Executor executor) {
+    FragmentInstanceId id =
+        new FragmentInstanceId(new PlanFragmentId(new 
QueryId("scan_cancellation"), 0), "0");
+    return FragmentInstanceContext.createFragmentInstanceContext(
+        id, new FragmentInstanceStateMachine(id, executor), 
mock(MemoryReservationManager.class));
+  }
+
+  private SeriesScanUtil newScanner(
+      FragmentInstanceContext context,
+      Ordering ordering,
+      boolean sequence,
+      int fileCount,
+      IntConsumer onRead) {
+    return newScanner(context, ordering, sequence, fileCount, onRead, false);
+  }
+
+  private SeriesScanUtil newScanner(
+      FragmentInstanceContext context,
+      Ordering ordering,
+      boolean sequence,
+      int fileCount,
+      IntConsumer onRead,
+      boolean missingMetadata) {
+    context.setIgnoreAllNullRows(ignoreAllNullRows);
+    loadedFiles = 0;
+    List<TsFileResource> resources = new ArrayList<>();
+    for (int i = 0; i < fileCount; i++) {
+      TsFileResource resource = mock(TsFileResource.class);
+      when(resource.getOrderTimeForSeq(device, true)).thenReturn(0L);
+      when(resource.getOrderTimeForSeq(device, false)).thenReturn(100L);
+      when(resource.getOrderTimeForUnseq(device, true)).thenReturn(0L);
+      when(resource.getOrderTimeForUnseq(device, false)).thenReturn(100L);
+      resources.add(resource);
+    }
+    QueryDataSource source =
+        new QueryDataSource(
+            sequence ? resources : Collections.emptyList(),
+            sequence ? Collections.emptyList() : resources);
+    source.setSingleDevice(true);
+    IntegerStatistics statistics = new IntegerStatistics();
+    statistics.update(0L, 0);
+    statistics.update(100L, 100);
+    ITimeSeriesMetadata metadata =
+        aligned ? mock(AbstractAlignedTimeSeriesMetadata.class) : 
mock(ITimeSeriesMetadata.class);
+    doReturn(statistics).when(metadata).getStatistics();
+    when(metadata.typeMatch(anyList())).thenReturn(true);
+    MeasurementSchema schema = new MeasurementSchema("s", TSDataType.INT32);
+    SeriesScanOptions.Builder builder = new SeriesScanOptions.Builder();
+    builder.withAllSensors(Collections.singleton("s"));
+    SeriesScanUtil scanner;
+    if (aligned) {
+      scanner =
+          new AlignedSeriesScanUtil(
+              new AlignedFullPath(
+                  device, Collections.singletonList("s"), 
Collections.singletonList(schema)),
+              ordering,
+              builder.build(),
+              context) {
+            @Override
+            protected AbstractAlignedTimeSeriesMetadata loadTimeSeriesMetadata(
+                TsFileResource resource, boolean isSeq) {
+              onRead.accept(++loadedFiles);
+              return missingMetadata ? null : 
(AbstractAlignedTimeSeriesMetadata) metadata;
+            }
+          };
+    } else {
+      scanner =
+          new SeriesScanUtil(
+              new NonAlignedFullPath(device, schema), ordering, 
builder.build(), context) {
+            @Override
+            protected ITimeSeriesMetadata loadTimeSeriesMetadata(
+                TsFileResource resource, boolean isSeq) {
+              onRead.accept(++loadedFiles);
+              return missingMetadata ? null : metadata;
+            }
+          };
+    }
+    scanner.initQueryDataSource(source);
+    return scanner;
+  }
+}

Reply via email to