module-3

Signed-off-by: Weihao Li <18110526956@163.com>
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 3feefd7..5cb613d 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
@@ -3818,5 +3818,8 @@
   public static final String EXCEPTION_VISIBLEALIASES_IS_NULL_630B27F1 = "visibleAliases is null";
   public static final String EXCEPTION_HAS_NO_PERMISSION_TO_EXECUTE_ARG_BECAUSE_ONLY_THE_SUPERUSER_CAN_ALTER_HIM_HERSELF_C5902893 =
       "Has no permission to execute %s, because only the superuser can alter him/herself.";
+  public static final String
+      LOG_FAILED_TO_CLEAN_DEVICEENTRY_DATA_SET_ASYNCHRONOUSLY_QUERYID_ARG_PLANNODEID_ARG_9106C4C5 =
+          "Failed to clean DeviceEntry data set asynchronously: queryId=%s, planNodeId=%s";
 
 }
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 0d4e7cc..89bafbd 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
@@ -4575,5 +4575,8 @@
   public static final String EXCEPTION_VISIBLEALIASES_IS_NULL_630B27F1 = "visibleAliases 不能为空";
   public static final String EXCEPTION_HAS_NO_PERMISSION_TO_EXECUTE_ARG_BECAUSE_ONLY_THE_SUPERUSER_CAN_ALTER_HIM_HERSELF_C5902893 =
       "无权执行 %s,因为只有超级用户可以修改其自身。";
+  public static final String
+      LOG_FAILED_TO_CLEAN_DEVICEENTRY_DATA_SET_ASYNCHRONOUSLY_QUERYID_ARG_PLANNODEID_ARG_9106C4C5 =
+          "异步清理 DeviceEntry 数据集失败:queryId=%s,planNodeId=%s";
 
 }
diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/exchange/MPPDataExchangeManager.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/exchange/MPPDataExchangeManager.java
index 2d3447d..0e0d6b1 100644
--- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/exchange/MPPDataExchangeManager.java
+++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/exchange/MPPDataExchangeManager.java
@@ -43,11 +43,14 @@
 import org.apache.iotdb.db.queryengine.execution.memory.LocalMemoryManager;
 import org.apache.iotdb.db.queryengine.metric.DataExchangeCostMetricSet;
 import org.apache.iotdb.db.queryengine.metric.DataExchangeCountMetricSet;
+import org.apache.iotdb.db.queryengine.plan.relational.metadata.spill.DeviceEntrySpillManager;
 import org.apache.iotdb.db.utils.SetThreadName;
 import org.apache.iotdb.mpp.rpc.thrift.MPPDataExchangeService;
 import org.apache.iotdb.mpp.rpc.thrift.TAcknowledgeDataBlockEvent;
 import org.apache.iotdb.mpp.rpc.thrift.TCloseSinkChannelEvent;
 import org.apache.iotdb.mpp.rpc.thrift.TEndOfDataBlockEvent;
+import org.apache.iotdb.mpp.rpc.thrift.TFetchDeviceEntrySegmentReq;
+import org.apache.iotdb.mpp.rpc.thrift.TFetchDeviceEntrySegmentResp;
 import org.apache.iotdb.mpp.rpc.thrift.TFragmentInstanceId;
 import org.apache.iotdb.mpp.rpc.thrift.TGetDataBlockRequest;
 import org.apache.iotdb.mpp.rpc.thrift.TGetDataBlockResponse;
@@ -97,6 +100,46 @@
         DataExchangeCountMetricSet.getInstance();
 
     @Override
+    public TFetchDeviceEntrySegmentResp fetchDeviceEntrySegment(
+        TFetchDeviceEntrySegmentReq request) {
+      try {
+        DeviceEntrySpillManager spillManager = DeviceEntrySpillManager.getInstance();
+        byte[] payload =
+            spillManager.readSegment(
+                request.getQueryId(), request.getPlanNodeId(), request.getSegmentId());
+        if (request.getSegmentId() > 0) {
+          spillManager.deleteSegment(
+              request.getQueryId(), request.getPlanNodeId(), request.getSegmentId() - 1);
+        }
+        return new TFetchDeviceEntrySegmentResp(
+                new TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode()))
+            .setPayload(payload);
+      } catch (IOException | RuntimeException e) {
+        return new TFetchDeviceEntrySegmentResp(
+            new TSStatus(TSStatusCode.INTERNAL_SERVER_ERROR.getStatusCode())
+                .setMessage(e.getMessage()));
+      }
+    }
+
+    @Override
+    public TSStatus finishDeviceEntrySegment(String queryId, String planNodeId) {
+      executorService.submit(
+          () -> {
+            try {
+              DeviceEntrySpillManager.getInstance().finishSegmentDataSet(queryId, planNodeId);
+            } catch (IOException | RuntimeException e) {
+              LOGGER.warn(
+                  DataNodeQueryMessages
+                      .LOG_FAILED_TO_CLEAN_DEVICEENTRY_DATA_SET_ASYNCHRONOUSLY_QUERYID_ARG_PLANNODEID_ARG_9106C4C5,
+                  queryId,
+                  planNodeId,
+                  e);
+            }
+          });
+      return new TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode());
+    }
+
+    @Override
     public TGetDataBlockResponse getDataBlock(TGetDataBlockRequest req) throws TException {
       long startTime = System.nanoTime();
       try (SetThreadName fragmentInstanceName =
@@ -623,6 +666,11 @@
     return mppDataExchangeService;
   }
 
+  public IClientManager<TEndPoint, SyncDataNodeMPPDataExchangeServiceClient>
+      getMppDataExchangeServiceClientManager() {
+    return mppDataExchangeServiceClientManager;
+  }
+
   public void deRegisterFragmentInstanceFromMemoryPool(
       String queryId, String fragmentInstanceId, boolean forceDeregister) {
     localMemoryManager
diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/BatchDeviceEntrySource.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/BatchDeviceEntrySource.java
new file mode 100644
index 0000000..72f00e7
--- /dev/null
+++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/BatchDeviceEntrySource.java
@@ -0,0 +1,35 @@
+/*
+ * 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.plan.relational.metadata.spill;
+
+import org.apache.iotdb.db.queryengine.plan.relational.metadata.DeviceEntry;
+
+import java.io.IOException;
+import java.util.List;
+
+public interface BatchDeviceEntrySource extends AutoCloseable {
+
+  boolean hasNextBatch();
+
+  List<DeviceEntry> nextBatch() throws IOException;
+
+  @Override
+  void close() throws IOException;
+}
diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/DeviceEntryDataSetHandle.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/DeviceEntryDataSetHandle.java
index d731aae..f295cd0 100644
--- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/DeviceEntryDataSetHandle.java
+++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/DeviceEntryDataSetHandle.java
@@ -101,7 +101,7 @@
         new PlanNodeId(ReadWriteIOUtils.readString(byteBuffer)),
         ThriftCommonsSerDeUtils.deserializeTEndPoint(byteBuffer),
         ReadWriteIOUtils.readInt(byteBuffer),
-        ReadWriteIOUtils.readLong(byteBuffer),
+        ReadWriteIOUtils.readInt(byteBuffer),
         ReadWriteIOUtils.readBool(byteBuffer));
   }
 }
diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/DeviceEntryReader.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/DeviceEntryReader.java
index 5fa2e04..cf5410d 100644
--- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/DeviceEntryReader.java
+++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/DeviceEntryReader.java
@@ -25,10 +25,10 @@
 
 public interface DeviceEntryReader extends AutoCloseable {
 
-  public boolean hasNext() throws IOException;
+  boolean hasNext() throws IOException;
 
-  public DeviceEntry next() throws IOException;
+  DeviceEntry next() throws IOException;
 
   @Override
-  public void close() throws IOException;
+  void close() throws IOException;
 }
diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/DeviceEntryRpcSegmentFetcher.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/DeviceEntryRpcSegmentFetcher.java
new file mode 100644
index 0000000..90b33b5
--- /dev/null
+++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/DeviceEntryRpcSegmentFetcher.java
@@ -0,0 +1,111 @@
+/*
+ * 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.plan.relational.metadata.spill;
+
+import org.apache.iotdb.common.rpc.thrift.TEndPoint;
+import org.apache.iotdb.commons.client.IClientManager;
+import org.apache.iotdb.commons.client.exception.ClientManagerException;
+import org.apache.iotdb.commons.client.sync.SyncDataNodeMPPDataExchangeServiceClient;
+import org.apache.iotdb.commons.utils.TestOnly;
+import org.apache.iotdb.db.queryengine.execution.exchange.MPPDataExchangeService;
+import org.apache.iotdb.mpp.rpc.thrift.TFetchDeviceEntrySegmentReq;
+import org.apache.iotdb.mpp.rpc.thrift.TFetchDeviceEntrySegmentResp;
+import org.apache.iotdb.rpc.TSStatusCode;
+
+import org.apache.thrift.TException;
+
+import java.io.IOException;
+
+public final class DeviceEntryRpcSegmentFetcher implements DeviceEntrySegmentFetcher {
+
+  private static final int MAX_ATTEMPTS = 3;
+
+  private final IClientManager<TEndPoint, SyncDataNodeMPPDataExchangeServiceClient> clientManager;
+
+  private DeviceEntryRpcSegmentFetcher() {
+    this(
+        MPPDataExchangeService.getInstance()
+            .getMPPDataExchangeManager()
+            .getMppDataExchangeServiceClientManager());
+  }
+
+  @TestOnly
+  public DeviceEntryRpcSegmentFetcher(
+      IClientManager<TEndPoint, SyncDataNodeMPPDataExchangeServiceClient> clientManager) {
+    this.clientManager = clientManager;
+  }
+
+  public static DeviceEntryRpcSegmentFetcher getInstance() {
+    return DeviceEntryRpcSegmentFetcherHolder.INSTANCE;
+  }
+
+  @Override
+  public byte[] fetch(DeviceEntryDataSetHandle handle, int segmentId) throws IOException {
+    IOException failure = null;
+    for (int attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
+      TFetchDeviceEntrySegmentResp response;
+      try {
+        try (SyncDataNodeMPPDataExchangeServiceClient client =
+            clientManager.borrowClient(handle.getCoordinatorEndPoint())) {
+          response = client.fetchDeviceEntrySegment(createFetchRequest(handle, segmentId));
+        }
+      } catch (ClientManagerException | TException e) {
+        failure = new IOException(e);
+        continue;
+      }
+      if (response.getStatus().getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
+        throw new IOException(response.getStatus().getMessage());
+      }
+      return response.getPayload();
+    }
+    throw failure;
+  }
+
+  @Override
+  public void finish(DeviceEntryDataSetHandle handle) {
+    for (int attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
+      try {
+        try (SyncDataNodeMPPDataExchangeServiceClient client =
+            clientManager.borrowClient(handle.getCoordinatorEndPoint())) {
+          org.apache.iotdb.common.rpc.thrift.TSStatus status =
+              client.finishDeviceEntrySegment(handle.getQueryId(), handle.getPlanNodeId().getId());
+          if (status.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
+            return;
+          }
+        }
+        return;
+      } catch (ClientManagerException | TException e) {
+        // Cleanup notification is best effort and must not fail the query.
+      }
+    }
+  }
+
+  private TFetchDeviceEntrySegmentReq createFetchRequest(
+      DeviceEntryDataSetHandle handle, int segmentId) {
+    return new TFetchDeviceEntrySegmentReq(
+        handle.getQueryId(), handle.getPlanNodeId().getId(), segmentId);
+  }
+
+  private static class DeviceEntryRpcSegmentFetcherHolder {
+    private static final DeviceEntryRpcSegmentFetcher INSTANCE = new DeviceEntryRpcSegmentFetcher();
+
+    private DeviceEntryRpcSegmentFetcherHolder() {}
+  }
+}
diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/DeviceEntrySegmentFetcher.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/DeviceEntrySegmentFetcher.java
new file mode 100644
index 0000000..c66e46f
--- /dev/null
+++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/DeviceEntrySegmentFetcher.java
@@ -0,0 +1,29 @@
+/*
+ * 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.plan.relational.metadata.spill;
+
+import java.io.IOException;
+
+public interface DeviceEntrySegmentFetcher {
+
+  byte[] fetch(DeviceEntryDataSetHandle handle, int segmentId) throws IOException;
+
+  void finish(DeviceEntryDataSetHandle handle);
+}
diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/DeviceEntrySpillManager.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/DeviceEntrySpillManager.java
index 5580c08..2b28c95 100644
--- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/DeviceEntrySpillManager.java
+++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/DeviceEntrySpillManager.java
@@ -45,7 +45,7 @@
   }
 
   public Path register(String queryId, PlanNodeId planNodeId) throws IOException {
-    Path ownerDirectory = rootDirectory().resolve(queryId).resolve(planNodeId.getId());
+    Path ownerDirectory = resolveOwnerDirectory(queryId, planNodeId.getId());
     Files.createDirectories(ownerDirectory);
     queryDirectories
         .computeIfAbsent(queryId, ignored -> ConcurrentHashMap.newKeySet())
@@ -66,7 +66,7 @@
 
   public void deregisterQuery(String queryId) throws IOException {
     queryDirectories.remove(queryId);
-    FileUtils.deleteDirectory(rootDirectory().resolve(queryId).toFile());
+    FileUtils.deleteDirectory(resolveQueryDirectory(queryId).toFile());
   }
 
   @TestOnly
@@ -109,17 +109,8 @@
   }
 
   public void finishSegmentDataSet(String queryId, String planNodeId) throws IOException {
-    deregisterOwner(queryId, rootDirectory().resolve(queryId).resolve(planNodeId));
-  }
-
-  public void deregisterFragment(String queryId, String fragmentInstanceId) throws IOException {
-    FileUtils.deleteDirectory(
-        resolveUnderRoot(fragmentRootDirectory(), queryId, fragmentInstanceId).toFile());
-  }
-
-  public void clearStaleFragmentData() throws IOException {
-    FileUtils.deleteDirectory(fragmentRootDirectory().toFile());
-    Files.createDirectories(fragmentRootDirectory());
+    Path ownerDirectory = resolveOwnerDirectory(queryId, planNodeId);
+    deregisterOwner(queryId, ownerDirectory);
   }
 
   private Path resolveRegisteredDataSetDirectory(String queryId, String dataSetId)
@@ -130,7 +121,7 @@
             .anyMatch(path -> path.toString().equals("..") || path.toString().equals("."))) {
       throw new IllegalArgumentException();
     }
-    Path queryDirectory = rootDirectory().resolve(queryId).normalize();
+    Path queryDirectory = resolveQueryDirectory(queryId);
     Path dataSetDirectory = queryDirectory.resolve(relativeDataSetPath).resolve("fi").normalize();
     if (!dataSetDirectory.startsWith(queryDirectory)) {
       throw new IllegalArgumentException();
@@ -166,20 +157,16 @@
     return Path.of(IoTDBDescriptor.getInstance().getConfig().getSortTmpDir(), "device-entry");
   }
 
-  private Path fragmentRootDirectory() {
-    return rootDirectory().resolve("fragment");
+  private Path resolveOwnerDirectory(String queryId, String planNodeId) {
+    Path queryDirectory = resolveQueryDirectory(queryId);
+    Path ownerDirectory = queryDirectory.resolve(planNodeId).normalize();
+    return ownerDirectory;
   }
 
-  private Path resolveUnderRoot(Path root, String... children) {
-    Path result = root;
-    for (String child : children) {
-      result = result.resolve(child);
-    }
-    result = result.normalize();
-    if (!result.startsWith(root.normalize())) {
-      throw new IllegalArgumentException();
-    }
-    return result;
+  private Path resolveQueryDirectory(String queryId) {
+    Path root = rootDirectory().normalize();
+    Path queryDirectory = root.resolve(queryId).normalize();
+    return queryDirectory;
   }
 
   private static class DeviceEntrySpillManagerHolder {
diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/InMemoryDeviceEntrySource.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/InMemoryDeviceEntrySource.java
new file mode 100644
index 0000000..8d67e76
--- /dev/null
+++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/InMemoryDeviceEntrySource.java
@@ -0,0 +1,54 @@
+/*
+ * 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.plan.relational.metadata.spill;
+
+import org.apache.iotdb.db.queryengine.plan.relational.metadata.DeviceEntry;
+
+import java.util.Collections;
+import java.util.List;
+
+public final class InMemoryDeviceEntrySource implements BatchDeviceEntrySource {
+
+  private List<DeviceEntry> entries;
+
+  public InMemoryDeviceEntrySource(List<DeviceEntry> entries) {
+    this.entries = entries;
+  }
+
+  @Override
+  public boolean hasNextBatch() {
+    return entries != null;
+  }
+
+  @Override
+  public List<DeviceEntry> nextBatch() {
+    if (entries == null) {
+      return Collections.emptyList();
+    }
+    List<DeviceEntry> result = entries;
+    entries = null;
+    return result;
+  }
+
+  @Override
+  public void close() {
+    entries = null;
+  }
+}
diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/LocalSegmentDeviceEntrySource.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/LocalSegmentDeviceEntrySource.java
new file mode 100644
index 0000000..684b880
--- /dev/null
+++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/LocalSegmentDeviceEntrySource.java
@@ -0,0 +1,64 @@
+/*
+ * 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.plan.relational.metadata.spill;
+
+import org.apache.iotdb.db.queryengine.plan.relational.metadata.DeviceEntry;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+public final class LocalSegmentDeviceEntrySource extends SegmentDeviceEntrySource {
+
+  private final DeviceEntrySpillManager spillManager;
+
+  public LocalSegmentDeviceEntrySource(DeviceEntryDataSetHandle handle) {
+    this(handle, DeviceEntrySpillManager.getInstance());
+  }
+
+  public LocalSegmentDeviceEntrySource(
+      DeviceEntryDataSetHandle handle, DeviceEntrySpillManager spillManager) {
+    super(handle);
+    this.spillManager = spillManager;
+  }
+
+  @Override
+  public List<DeviceEntry> nextBatch() throws IOException {
+    int segmentId = nextSegmentId;
+    Path segment = acquireSegment(segmentId);
+    List<DeviceEntry> result = deserialize(Files.readAllBytes(segment));
+    releaseSegment(segmentId);
+    nextSegmentId++;
+    return result;
+  }
+
+  private Path acquireSegment(int segmentId) throws IOException {
+    return spillManager.resolveSegment(handle.getQueryId(), handle.getPlanNodeId(), segmentId);
+  }
+
+  private void releaseSegment(int segmentId) throws IOException {
+    if (segmentId + 1 == handle.getSegmentCount()) {
+      spillManager.finishSegmentDataSet(handle.getQueryId(), handle.getPlanNodeId().getId());
+    } else {
+      spillManager.deleteSegment(handle.getQueryId(), handle.getPlanNodeId(), segmentId);
+    }
+  }
+}
diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/RemoteSegmentDeviceEntrySource.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/RemoteSegmentDeviceEntrySource.java
new file mode 100644
index 0000000..d43d7af
--- /dev/null
+++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/RemoteSegmentDeviceEntrySource.java
@@ -0,0 +1,65 @@
+/*
+ * 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.plan.relational.metadata.spill;
+
+import org.apache.iotdb.db.queryengine.plan.relational.metadata.DeviceEntry;
+
+import java.io.IOException;
+import java.util.List;
+
+public final class RemoteSegmentDeviceEntrySource extends SegmentDeviceEntrySource {
+
+  private final DeviceEntrySegmentFetcher fetcher;
+  private boolean finished;
+
+  public RemoteSegmentDeviceEntrySource(DeviceEntryDataSetHandle handle) {
+    this(handle, DeviceEntryRpcSegmentFetcher.getInstance());
+  }
+
+  public RemoteSegmentDeviceEntrySource(
+      DeviceEntryDataSetHandle handle, DeviceEntrySegmentFetcher fetcher) {
+    super(handle);
+    this.fetcher = fetcher;
+  }
+
+  @Override
+  public List<DeviceEntry> nextBatch() throws IOException {
+    int segmentId = nextSegmentId;
+    byte[] payload = fetcher.fetch(handle, segmentId);
+    List<DeviceEntry> result = deserialize(payload);
+    nextSegmentId++;
+    if (!hasNextBatch()) {
+      finish();
+    }
+    return result;
+  }
+
+  private void finish() {
+    fetcher.finish(handle);
+    finished = true;
+  }
+
+  @Override
+  public void close() throws IOException {
+    if (!finished) {
+      finish();
+    }
+  }
+}
diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/SegmentDeviceEntrySource.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/SegmentDeviceEntrySource.java
new file mode 100644
index 0000000..0a00e95
--- /dev/null
+++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/SegmentDeviceEntrySource.java
@@ -0,0 +1,78 @@
+/*
+ * 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.plan.relational.metadata.spill;
+
+import org.apache.iotdb.db.queryengine.common.DataNodeEndPoints;
+import org.apache.iotdb.db.queryengine.plan.relational.metadata.DeviceEntry;
+
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public abstract class SegmentDeviceEntrySource implements BatchDeviceEntrySource {
+
+  protected final DeviceEntryDataSetHandle handle;
+  protected int nextSegmentId;
+
+  protected SegmentDeviceEntrySource(DeviceEntryDataSetHandle handle) {
+    this.handle = handle;
+  }
+
+  public static SegmentDeviceEntrySource create(DeviceEntryDataSetHandle handle) {
+    return handle.getCoordinatorEndPoint().equals(DataNodeEndPoints.LOCAL_HOST_INTERNAL_ENDPOINT)
+        ? new LocalSegmentDeviceEntrySource(handle)
+        : new RemoteSegmentDeviceEntrySource(handle);
+  }
+
+  @Override
+  public final boolean hasNextBatch() {
+    return nextSegmentId < handle.getSegmentCount();
+  }
+
+  protected final List<DeviceEntry> deserialize(byte[] segmentBytes) throws IOException {
+    List<DeviceEntry> result = new ArrayList<>();
+    int segmentLength = segmentBytes.length;
+    try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(segmentBytes))) {
+      int consumedBytes = 0;
+      while (consumedBytes < segmentLength) {
+        if (segmentLength - consumedBytes < Integer.BYTES) {
+          throw new IOException();
+        }
+        int length = input.readInt();
+        consumedBytes += Integer.BYTES;
+        if (length < 0 || length > segmentLength - consumedBytes) {
+          throw new IOException();
+        }
+        byte[] bytes = new byte[length];
+        input.readFully(bytes);
+        consumedBytes += length;
+        result.add(DeviceEntry.deserialize(bytes));
+      }
+    }
+    return result;
+  }
+
+  @Override
+  public void close() throws IOException {
+    // No local cache is created by a segment source.
+  }
+}
diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/QueryCardinalityUtil.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/QueryCardinalityUtil.java
index a59b2e0..6b49aeb 100644
--- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/QueryCardinalityUtil.java
+++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/QueryCardinalityUtil.java
@@ -218,10 +218,10 @@
             && !node.getProjection().getMap().isEmpty()) { // also exist date_bin
           return Range.atLeast(0L);
         } else {
-          return Range.atMost(node.getDeviceEntryCount());
+          return Range.atMost((long) node.getDeviceEntryCount());
         }
       } else {
-        return Range.singleton(node.getDeviceEntryCount());
+        return Range.singleton((long) node.getDeviceEntryCount());
       }
     }
 
diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/DeviceEntryRpcSegmentFetcherTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/DeviceEntryRpcSegmentFetcherTest.java
new file mode 100644
index 0000000..8572f00
--- /dev/null
+++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/DeviceEntryRpcSegmentFetcherTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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.plan.relational.metadata.spill;
+
+import org.apache.iotdb.common.rpc.thrift.TEndPoint;
+import org.apache.iotdb.common.rpc.thrift.TSStatus;
+import org.apache.iotdb.commons.client.IClientManager;
+import org.apache.iotdb.commons.client.sync.SyncDataNodeMPPDataExchangeServiceClient;
+import org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNodeId;
+import org.apache.iotdb.mpp.rpc.thrift.TFetchDeviceEntrySegmentResp;
+import org.apache.iotdb.rpc.TSStatusCode;
+
+import org.apache.thrift.TException;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class DeviceEntryRpcSegmentFetcherTest {
+
+  private IClientManager<TEndPoint, SyncDataNodeMPPDataExchangeServiceClient> clientManager;
+  private SyncDataNodeMPPDataExchangeServiceClient client;
+  private DeviceEntryRpcSegmentFetcher fetcher;
+  private DeviceEntryDataSetHandle handle;
+
+  @Before
+  @SuppressWarnings("unchecked")
+  public void setUp() throws Exception {
+    clientManager = Mockito.mock(IClientManager.class);
+    client = Mockito.mock(SyncDataNodeMPPDataExchangeServiceClient.class);
+    when(clientManager.borrowClient(any())).thenReturn(client);
+    fetcher = new DeviceEntryRpcSegmentFetcher(clientManager);
+    handle =
+        new DeviceEntryDataSetHandle(
+            "query", new PlanNodeId("scan"), new TEndPoint("127.0.0.1", 10740), 1, 1, false);
+  }
+
+  @Test
+  public void testFetchRetriesNetworkFailure() throws Exception {
+    byte[] payload = new byte[] {1, 2, 3};
+    when(client.fetchDeviceEntrySegment(any()))
+        .thenThrow(new TException())
+        .thenThrow(new TException())
+        .thenReturn(
+            new TFetchDeviceEntrySegmentResp(
+                    new TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode()))
+                .setPayload(payload));
+
+    assertArrayEquals(payload, fetcher.fetch(handle, 0));
+    verify(client, times(3)).fetchDeviceEntrySegment(any());
+  }
+
+  @Test
+  public void testFinishRetriesNetworkFailureAtMostThreeTimes() throws Exception {
+    when(client.finishDeviceEntrySegment(any(), any())).thenThrow(new TException());
+
+    fetcher.finish(handle);
+
+    verify(client, times(3)).finishDeviceEntrySegment(any(), any());
+  }
+
+  @Test
+  public void testFinishDoesNotRetryServerFailure() throws Exception {
+    when(client.finishDeviceEntrySegment(any(), any()))
+        .thenReturn(new TSStatus(TSStatusCode.INTERNAL_SERVER_ERROR.getStatusCode()));
+
+    fetcher.finish(handle);
+
+    verify(client, times(1)).finishDeviceEntrySegment(any(), any());
+  }
+}
diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/SegmentDeviceEntrySourceTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/SegmentDeviceEntrySourceTest.java
new file mode 100644
index 0000000..2eba72d
--- /dev/null
+++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/spill/SegmentDeviceEntrySourceTest.java
@@ -0,0 +1,176 @@
+/*
+ * 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.plan.relational.metadata.spill;
+
+import org.apache.iotdb.common.rpc.thrift.TEndPoint;
+import org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNodeId;
+import org.apache.iotdb.db.conf.IoTDBDescriptor;
+import org.apache.iotdb.db.queryengine.plan.relational.metadata.AlignedDeviceEntry;
+import org.apache.iotdb.db.queryengine.plan.relational.metadata.DeviceEntry;
+
+import org.apache.tsfile.file.metadata.IDeviceID;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class SegmentDeviceEntrySourceTest {
+
+  private Path queryDirectory;
+  private String originalSortTmpDir;
+
+  @Before
+  public void setUp() throws Exception {
+    queryDirectory = Files.createTempDirectory("device-entry-source-test");
+    originalSortTmpDir = IoTDBDescriptor.getInstance().getConfig().getSortTmpDir();
+    IoTDBDescriptor.getInstance().getConfig().setSortTmpDir(queryDirectory.toString());
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    DeviceEntrySpillManager.getInstance().clearStaleData();
+    Files.deleteIfExists(queryDirectory.resolve("device-entry"));
+    Files.deleteIfExists(queryDirectory);
+    IoTDBDescriptor.getInstance().getConfig().setSortTmpDir(originalSortTmpDir);
+  }
+
+  @Test
+  public void testLocalSourceConsumesSegmentsAndCleansDataSet() throws Exception {
+    List<DeviceEntry> expected = createEntries(20);
+    PlanNodeId planNodeId = new PlanNodeId("scan-local");
+    SpilledDeviceEntryDataSet dataSet;
+    try (DeviceEntryMaterializer materializer =
+        new DeviceEntryMaterializer("q-local", planNodeId, 128, false)) {
+      for (DeviceEntry entry : expected) {
+        materializer.append(entry);
+      }
+      materializer.forceSpill();
+      dataSet = (SpilledDeviceEntryDataSet) materializer.finish();
+    }
+
+    DeviceEntryDataSetHandle handle =
+        new DeviceEntryDataSetHandle(
+            "q-local",
+            planNodeId,
+            new TEndPoint("127.0.0.1", 1),
+            dataSet.getSegments().size(),
+            expected.size(),
+            false);
+    List<DeviceEntry> actual = new ArrayList<>();
+    try (LocalSegmentDeviceEntrySource source = new LocalSegmentDeviceEntrySource(handle)) {
+      while (source.hasNextBatch()) {
+        actual.addAll(source.nextBatch());
+      }
+    }
+
+    assertEquals(expected, actual);
+    assertFalse(Files.exists(queryDirectory.resolve("device-entry/q-local/scan-local")));
+  }
+
+  @Test
+  public void testRemoteSourceFetchesSegmentsAndFinishes() throws Exception {
+    List<DeviceEntry> expected = createEntries(3);
+    RecordingFetcher fetcher = new RecordingFetcher(expected);
+    DeviceEntryDataSetHandle handle =
+        new DeviceEntryDataSetHandle(
+            "q-remote",
+            new PlanNodeId("scan-remote"),
+            new TEndPoint("127.0.0.2", 2),
+            expected.size(),
+            expected.size(),
+            false);
+    List<DeviceEntry> actual = new ArrayList<>();
+    try (RemoteSegmentDeviceEntrySource source =
+        new RemoteSegmentDeviceEntrySource(handle, fetcher)) {
+      while (source.hasNextBatch()) {
+        actual.addAll(source.nextBatch());
+      }
+    }
+
+    assertEquals(expected, actual);
+    assertEquals(List.of(0, 1, 2), fetcher.segmentIds);
+    assertTrue(fetcher.finished);
+  }
+
+  @Test
+  public void testFinishUnregisteredDataSetIsIdempotent() throws Exception {
+    DeviceEntrySpillManager.getInstance()
+        .finishSegmentDataSet("unregistered-query", "unregistered-scan");
+    DeviceEntrySpillManager.getInstance()
+        .finishSegmentDataSet("unregistered-query", "unregistered-scan");
+  }
+
+  private static List<DeviceEntry> createEntries(int count) {
+    List<DeviceEntry> entries = new ArrayList<>();
+    for (int i = 0; i < count; i++) {
+      entries.add(
+          new AlignedDeviceEntry(
+              IDeviceID.Factory.DEFAULT_FACTORY.create(new String[] {"table", "device" + i}),
+              new org.apache.tsfile.utils.Binary[0]));
+    }
+    return entries;
+  }
+
+  private static byte[] serializeSegment(DeviceEntry entry) throws Exception {
+    byte[] payload = entry.serializeToBytes();
+    try (ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+        DataOutputStream output = new DataOutputStream(bytes)) {
+      output.writeInt(payload.length);
+      output.write(payload);
+      return bytes.toByteArray();
+    }
+  }
+
+  private static final class RecordingFetcher implements DeviceEntrySegmentFetcher {
+
+    private final List<DeviceEntry> entries;
+    private final List<Integer> segmentIds = new ArrayList<>();
+    private boolean finished;
+
+    private RecordingFetcher(List<DeviceEntry> entries) {
+      this.entries = entries;
+    }
+
+    @Override
+    public byte[] fetch(DeviceEntryDataSetHandle handle, int segmentId) throws java.io.IOException {
+      segmentIds.add(segmentId);
+      try {
+        return serializeSegment(entries.get(segmentId));
+      } catch (Exception e) {
+        throw new java.io.IOException(e);
+      }
+    }
+
+    @Override
+    public void finish(DeviceEntryDataSetHandle handle) {
+      finished = true;
+    }
+  }
+}
diff --git a/iotdb-protocol/thrift-datanode/src/main/thrift/datanode.thrift b/iotdb-protocol/thrift-datanode/src/main/thrift/datanode.thrift
index d66ed10..f55f87a 100644
--- a/iotdb-protocol/thrift-datanode/src/main/thrift/datanode.thrift
+++ b/iotdb-protocol/thrift-datanode/src/main/thrift/datanode.thrift
@@ -879,6 +879,17 @@
   2: optional string allowedUsername
 }
 
+struct TFetchDeviceEntrySegmentReq {
+  1: required string queryId
+  2: required string planNodeId
+  3: required i32 segmentId
+}
+
+struct TFetchDeviceEntrySegmentResp {
+  1: required common.TSStatus status
+  2: optional binary payload
+}
+
 /**
 * END: Used for EXPLAIN ANALYZE
 **/
@@ -1440,6 +1451,10 @@
 
   void onEndOfDataBlockEvent(TEndOfDataBlockEvent e);
 
+  TFetchDeviceEntrySegmentResp fetchDeviceEntrySegment(TFetchDeviceEntrySegmentReq req);
+
+  common.TSStatus finishDeviceEntrySegment(1: string queryId, 2: string planNodeId);
+
   /** Empty rpc, only for connection test */
   common.TSStatus testConnectionEmptyRPC()
 }