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 e7ec99f5d17 Datanode: validate that a query id belongs to the calling 
session (#18643)
e7ec99f5d17 is described below

commit e7ec99f5d1717dbee749474ff149b97b87f4675c
Author: Colin Lee <[email protected]>
AuthorDate: Wed Sep 23 10:59:51 2026 +0800

    Datanode: validate that a query id belongs to the calling session (#18643)
---
 .../apache/iotdb/db/i18n/DataNodeMiscMessages.java |   2 +
 .../apache/iotdb/db/i18n/DataNodeMiscMessages.java |   2 +
 .../iotdb/db/protocol/session/ClientSession.java   |  23 ++
 .../iotdb/db/protocol/session/IClientSession.java  |   3 +
 .../db/protocol/session/InternalClientSession.java |   5 +
 .../db/protocol/session/MqttClientSession.java     |   5 +
 .../db/protocol/session/RestClientSession.java     |   5 +
 .../protocol/thrift/impl/ClientRPCServiceImpl.java |  97 ++++--
 .../db/protocol/session/QueryOwnershipTest.java    | 343 +++++++++++++++++++++
 9 files changed, 459 insertions(+), 26 deletions(-)

diff --git 
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java
 
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java
index c961031b38e..405a5139e34 100644
--- 
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java
+++ 
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java
@@ -21,6 +21,8 @@ package org.apache.iotdb.db.i18n;
 
 /** Compile-time i18n constants for DataNode misc subsystems (English). */
 public final class DataNodeMiscMessages {
+  public static final String 
MESSAGE_QUERY_DOES_NOT_BELONG_TO_CURRENT_SESSION_A1198237 =
+      "The requested query does not belong to the current session.";
 
   public static final String 
MESSAGE_MISSING_LOAD_TSFILE_SLICE_METADATA_ARG_DE4333DA =
       "Missing Load TsFile slice metadata: %s";
diff --git 
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java
 
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java
index 11bc2fc579f..a87a49eaaf5 100644
--- 
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java
+++ 
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java
@@ -21,6 +21,8 @@ package org.apache.iotdb.db.i18n;
 
 /** 编译时国际化常量 - DataNode 杂项子系统(中文)。 */
 public final class DataNodeMiscMessages {
+  public static final String 
MESSAGE_QUERY_DOES_NOT_BELONG_TO_CURRENT_SESSION_A1198237 =
+      "请求的查询不属于当前会话。";
 
   public static final String 
MESSAGE_MISSING_LOAD_TSFILE_SLICE_METADATA_ARG_DE4333DA =
       "缺少 Load TsFile 分片元数据:%s";
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/session/ClientSession.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/session/ClientSession.java
index bad4ddd6c7d..2f08920b8d2 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/session/ClientSession.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/session/ClientSession.java
@@ -90,6 +90,29 @@ public class ClientSession extends IClientSession {
     queryIds.add(queryId);
   }
 
+  @Override
+  public boolean containsQueryId(Long statementId, long queryId) {
+    return containsQueryId(statementIdToQueryId, statementId, queryId);
+  }
+
+  public static boolean containsQueryId(
+      Map<Long, Set<Long>> statementIdToQueryId, Long statementId, long 
queryId) {
+    // Set#contains takes an Object, so box the primitive queryId once: a 
client that does not send
+    // a statement id makes this method visit every statement set of the 
session, and a box per
+    // visited set would allocate once per statement on every fetched page.
+    Long boxedQueryId = queryId;
+    if (statementId == null) {
+      for (Set<Long> queryIds : statementIdToQueryId.values()) {
+        if (queryIds != null && queryIds.contains(boxedQueryId)) {
+          return true;
+        }
+      }
+      return false;
+    }
+    Set<Long> queryIds = statementIdToQueryId.get(statementId);
+    return queryIds != null && queryIds.contains(boxedQueryId);
+  }
+
   @Override
   public void removeQueryId(Long statementId, Long queryId) {
     removeQueryId(statementIdToQueryId, statementId, queryId);
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/session/IClientSession.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/session/IClientSession.java
index bac4b15e342..5114ecb0d78 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/session/IClientSession.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/session/IClientSession.java
@@ -164,6 +164,9 @@ public abstract class IClientSession {
 
   public abstract void addQueryId(Long statementId, long queryId);
 
+  // statementId could be null
+  public abstract boolean containsQueryId(Long statementId, long queryId);
+
   // statementId could be null
   public abstract void removeQueryId(Long statementId, Long queryId);
 
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/session/InternalClientSession.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/session/InternalClientSession.java
index 460ec9319f2..8bbb248e989 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/session/InternalClientSession.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/session/InternalClientSession.java
@@ -90,6 +90,11 @@ public class InternalClientSession extends IClientSession {
     queryIds.add(queryId);
   }
 
+  @Override
+  public boolean containsQueryId(Long statementId, long queryId) {
+    return ClientSession.containsQueryId(statementIdToQueryId, statementId, 
queryId);
+  }
+
   @Override
   public void removeQueryId(Long statementId, Long queryId) {
     ClientSession.removeQueryId(statementIdToQueryId, statementId, queryId);
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/session/MqttClientSession.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/session/MqttClientSession.java
index 65e2c9a5b5d..0fb830bcca3 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/session/MqttClientSession.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/session/MqttClientSession.java
@@ -77,6 +77,11 @@ public class MqttClientSession extends IClientSession {
     throw new UnsupportedOperationException();
   }
 
+  @Override
+  public boolean containsQueryId(Long statementId, long queryId) {
+    return false;
+  }
+
   @Override
   public void removeQueryId(Long statementId, Long queryId) {
     throw new UnsupportedOperationException();
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/session/RestClientSession.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/session/RestClientSession.java
index 58bae05fffe..7fe2ceece08 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/session/RestClientSession.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/session/RestClientSession.java
@@ -79,6 +79,11 @@ public class RestClientSession extends IClientSession {
     throw new UnsupportedOperationException();
   }
 
+  @Override
+  public boolean containsQueryId(Long statementId, long queryId) {
+    return false;
+  }
+
   @Override
   public void removeQueryId(Long statementId, Long queryId) {
     throw new UnsupportedOperationException();
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/thrift/impl/ClientRPCServiceImpl.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/thrift/impl/ClientRPCServiceImpl.java
index ce0c5d67450..692b5bedd2d 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/thrift/impl/ClientRPCServiceImpl.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/thrift/impl/ClientRPCServiceImpl.java
@@ -1533,6 +1533,7 @@ public class ClientRPCServiceImpl implements 
IClientRPCServiceWithHandler {
     String statementType = null;
     Throwable t = null;
     IQueryExecution queryExecution = null;
+    boolean queryOwnedBySession = false;
     IClientSession clientSession = 
SESSION_MANAGER.getCurrSessionAndUpdateIdleTime();
     Long statementId = req.isSetStatementId() ? req.getStatementId() : null;
     try {
@@ -1542,13 +1543,22 @@ public class ClientRPCServiceImpl implements 
IClientRPCServiceWithHandler {
       }
 
       queryExecution = COORDINATOR.getQueryExecution(req.queryId);
-
       if (queryExecution == null) {
         TSStatus noQueryExecutionStatus = new 
TSStatus(QUERY_WAS_KILLED.getStatusCode());
         noQueryExecutionStatus.setMessage(NO_QUERY_EXECUTION_ERR_MSG);
         return RpcUtils.getTSFetchResultsResp(noQueryExecutionStatus);
       }
 
+      if (!clientSession.containsQueryId(statementId, req.queryId)) {
+        // The query is still running, but it was submitted by another 
session: do not stream its
+        // result and do not release it, so that the query which owns it is 
left untouched.
+        return RpcUtils.getTSFetchResultsResp(
+            RpcUtils.getStatus(
+                TSStatusCode.NO_PERMISSION,
+                
DataNodeMiscMessages.MESSAGE_QUERY_DOES_NOT_BELONG_TO_CURRENT_SESSION_A1198237));
+      }
+      queryOwnedBySession = true;
+
       TSFetchResultsResp resp = 
RpcUtils.getTSFetchResultsResp(TSStatusCode.SUCCESS_STATUS);
 
       queryExecution.updateCurrentRpcStartTime(startTime);
@@ -1577,19 +1587,21 @@ public class ClientRPCServiceImpl implements 
IClientRPCServiceWithHandler {
       throw error;
     } finally {
 
-      long currentOperationCost = System.nanoTime() - startTime;
-      COORDINATOR.recordExecutionTime(req.queryId, currentOperationCost);
-
-      // record each operation time cost
-      CommonUtils.addStatementExecutionLatency(
-          OperationType.FETCH_RESULTS, statementType, currentOperationCost);
+      if (queryOwnedBySession) {
+        long currentOperationCost = System.nanoTime() - startTime;
+        COORDINATOR.recordExecutionTime(req.queryId, currentOperationCost);
 
-      if (finished) {
-        // record total time cost for one query
-        long executionTime = COORDINATOR.getTotalExecutionTime(req.queryId);
-        CommonUtils.addQueryLatency(
-            StatementType.QUERY, executionTime > 0 ? executionTime : 
currentOperationCost);
-        clearUp(clientSession, statementId, req.queryId, req, t);
+        // record each operation time cost
+        CommonUtils.addStatementExecutionLatency(
+            OperationType.FETCH_RESULTS, statementType, currentOperationCost);
+
+        if (finished) {
+          // record total time cost for one query
+          long executionTime = COORDINATOR.getTotalExecutionTime(req.queryId);
+          CommonUtils.addQueryLatency(
+              StatementType.QUERY, executionTime > 0 ? executionTime : 
currentOperationCost);
+          clearUp(clientSession, statementId, req.queryId, req, t);
+        }
       }
 
       SESSION_MANAGER.updateIdleTime();
@@ -1683,8 +1695,27 @@ public class ClientRPCServiceImpl implements 
IClientRPCServiceWithHandler {
 
   @Override
   public TSStatus closeOperation(TSCloseOperationReq req) {
+    IClientSession clientSession = SESSION_MANAGER.getCurrSession();
+    if (req.isSetQueryId()
+        && req.isSetStatementId()
+        && clientSession != null
+        && clientSession.isLogin()
+        && !clientSession.containsQueryId(req.getStatementId(), req.queryId)) {
+      // The queryId indexes the process-wide map of running queries, so only 
the session that
+      // submitted the query may release it.
+      if (COORDINATOR.getQueryExecution(req.queryId) != null) {
+        return RpcUtils.getStatus(
+            TSStatusCode.NO_PERMISSION,
+            
DataNodeMiscMessages.MESSAGE_QUERY_DOES_NOT_BELONG_TO_CURRENT_SESSION_A1198237);
+      }
+      // A queryId that is no longer running keeps the previous behaviour: 
releasing it stays a
+      // no-op. It must not fall through to the global cleanup below: query 
ids are allocated
+      // before their execution is published, so the session that owns this 
queryId can register
+      // it between the lookup above and the cleanup.
+      return RpcUtils.getStatus(TSStatusCode.SUCCESS_STATUS);
+    }
     return SESSION_MANAGER.closeOperation(
-        SESSION_MANAGER.getCurrSession(),
+        clientSession,
         req.queryId,
         req.statementId,
         req.isSetStatementId(),
@@ -2291,6 +2322,7 @@ public class ClientRPCServiceImpl implements 
IClientRPCServiceWithHandler {
     String statementType = null;
     Throwable t = null;
     IQueryExecution queryExecution = null;
+    boolean queryOwnedBySession = false;
     IClientSession clientSession = 
SESSION_MANAGER.getCurrSessionAndUpdateIdleTime();
     Long statementId = req.isSetStatementId() ? req.getStatementId() : null;
     try {
@@ -2305,6 +2337,17 @@ public class ClientRPCServiceImpl implements 
IClientRPCServiceWithHandler {
         noQueryExecutionStatus.setMessage(NO_QUERY_EXECUTION_ERR_MSG);
         return RpcUtils.getTSFetchResultsResp(noQueryExecutionStatus);
       }
+
+      if (!clientSession.containsQueryId(statementId, req.queryId)) {
+        // The query is still running, but it was submitted by another 
session: do not stream its
+        // result and do not release it, so that the query which owns it is 
left untouched.
+        return RpcUtils.getTSFetchResultsResp(
+            RpcUtils.getStatus(
+                TSStatusCode.NO_PERMISSION,
+                
DataNodeMiscMessages.MESSAGE_QUERY_DOES_NOT_BELONG_TO_CURRENT_SESSION_A1198237));
+      }
+      queryOwnedBySession = true;
+
       queryExecution.updateCurrentRpcStartTime(startTime);
       statementType = queryExecution.getStatementType();
 
@@ -2332,19 +2375,21 @@ public class ClientRPCServiceImpl implements 
IClientRPCServiceWithHandler {
       throw error;
     } finally {
 
-      long currentOperationCost = System.nanoTime() - startTime;
-      COORDINATOR.recordExecutionTime(req.queryId, currentOperationCost);
+      if (queryOwnedBySession) {
+        long currentOperationCost = System.nanoTime() - startTime;
+        COORDINATOR.recordExecutionTime(req.queryId, currentOperationCost);
 
-      // record each operation time cost
-      CommonUtils.addStatementExecutionLatency(
-          OperationType.FETCH_RESULTS, statementType, currentOperationCost);
-
-      if (finished) {
-        // record total time cost for one query
-        long executionTime = COORDINATOR.getTotalExecutionTime(req.queryId);
-        CommonUtils.addQueryLatency(
-            StatementType.QUERY, executionTime > 0 ? executionTime : 
currentOperationCost);
-        clearUp(clientSession, statementId, req.queryId, req, t);
+        // record each operation time cost
+        CommonUtils.addStatementExecutionLatency(
+            OperationType.FETCH_RESULTS, statementType, currentOperationCost);
+
+        if (finished) {
+          // record total time cost for one query
+          long executionTime = COORDINATOR.getTotalExecutionTime(req.queryId);
+          CommonUtils.addQueryLatency(
+              StatementType.QUERY, executionTime > 0 ? executionTime : 
currentOperationCost);
+          clearUp(clientSession, statementId, req.queryId, req, t);
+        }
       }
 
       SESSION_MANAGER.updateIdleTime();
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/protocol/session/QueryOwnershipTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/protocol/session/QueryOwnershipTest.java
new file mode 100644
index 00000000000..b8c53352c09
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/protocol/session/QueryOwnershipTest.java
@@ -0,0 +1,343 @@
+/*
+ * 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.protocol.session;
+
+import org.apache.iotdb.db.conf.IoTDBDescriptor;
+import org.apache.iotdb.db.protocol.thrift.impl.ClientRPCServiceImpl;
+import org.apache.iotdb.db.queryengine.plan.Coordinator;
+import org.apache.iotdb.db.queryengine.plan.execution.IQueryExecution;
+import org.apache.iotdb.rpc.TSStatusCode;
+import org.apache.iotdb.service.rpc.thrift.TSCloseOperationReq;
+import org.apache.iotdb.service.rpc.thrift.TSFetchResultsReq;
+import org.apache.iotdb.service.rpc.thrift.TSFetchResultsResp;
+
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.lang.reflect.Field;
+import java.net.Socket;
+import java.util.Map;
+import java.util.Optional;
+
+public class QueryOwnershipTest {
+
+  private static final long STATEMENT_ID = 1L;
+  private static final long QUERY_ID = 2L;
+
+  private static int previousDataNodeId;
+
+  @BeforeClass
+  public static void setUp() {
+    // the coordinator builds its query id generator from the data node id of 
this node
+    previousDataNodeId = 
IoTDBDescriptor.getInstance().getConfig().getDataNodeId();
+    IoTDBDescriptor.getInstance().getConfig().setDataNodeId(0);
+  }
+
+  @AfterClass
+  public static void tearDown() {
+    
IoTDBDescriptor.getInstance().getConfig().setDataNodeId(previousDataNodeId);
+  }
+
+  @Test
+  public void testQueryIdsAreBoundToTheSessionThatSubmittedTheQuery() {
+    ClientSession owner = createSession("user");
+    owner.addStatementId(STATEMENT_ID);
+    owner.addQueryId(STATEMENT_ID, QUERY_ID);
+
+    ClientSession anotherSession = createSession("user");
+    anotherSession.addStatementId(STATEMENT_ID);
+
+    Assert.assertTrue(owner.containsQueryId(STATEMENT_ID, QUERY_ID));
+    // clients that do not send a statement id together with the query id are 
still served
+    Assert.assertTrue(owner.containsQueryId(null, QUERY_ID));
+    Assert.assertFalse(anotherSession.containsQueryId(STATEMENT_ID, QUERY_ID));
+    Assert.assertFalse(anotherSession.containsQueryId(null, QUERY_ID));
+    Assert.assertFalse(owner.containsQueryId(STATEMENT_ID + 1, QUERY_ID));
+  }
+
+  @Test
+  public void testFetchResultsRejectsQueryOfAnotherSession() throws Exception {
+    ClientSession anotherSession = createSession("user");
+    anotherSession.addStatementId(STATEMENT_ID);
+    anotherSession.setLogin(true);
+
+    Map<Long, IQueryExecution> queryExecutionMap = getQueryExecutionMap();
+    queryExecutionMap.put(QUERY_ID, mockQueryExecution());
+    try {
+      withCurrentSession(
+          anotherSession,
+          () -> {
+            ClientRPCServiceImpl service = new ClientRPCServiceImpl();
+            Assert.assertEquals(
+                TSStatusCode.NO_PERMISSION.getStatusCode(),
+                
service.fetchResults(createFetchResultsReq(anotherSession)).getStatus().getCode());
+            Assert.assertEquals(
+                TSStatusCode.NO_PERMISSION.getStatusCode(),
+                service
+                    
.fetchResultsV2(createFetchResultsReqWithStatementId(anotherSession))
+                    .getStatus()
+                    .getCode());
+          });
+      // the rejected requests must not release the query of the session that 
submitted it
+      Assert.assertTrue(queryExecutionMap.containsKey(QUERY_ID));
+    } finally {
+      queryExecutionMap.remove(QUERY_ID);
+    }
+  }
+
+  @Test
+  public void testFetchResultsOfOwnQueryIsStillServed() throws Exception {
+    ClientSession owner = createSession("user");
+    owner.addStatementId(STATEMENT_ID);
+    owner.addQueryId(STATEMENT_ID, QUERY_ID);
+    owner.setLogin(true);
+
+    IQueryExecution queryExecution = mockQueryExecution();
+    Map<Long, IQueryExecution> queryExecutionMap = getQueryExecutionMap();
+    queryExecutionMap.put(QUERY_ID, queryExecution);
+    try {
+      withCurrentSession(
+          owner,
+          () ->
+              Assert.assertEquals(
+                  TSStatusCode.SUCCESS_STATUS.getStatusCode(),
+                  new ClientRPCServiceImpl()
+                      
.fetchResultsV2(createFetchResultsReqWithStatementId(owner))
+                      .getStatus()
+                      .getCode()));
+      // a fully consumed query is released, and the client closes it 
afterwards
+      Assert.assertFalse(queryExecutionMap.containsKey(QUERY_ID));
+      Assert.assertFalse(owner.containsQueryId(STATEMENT_ID, QUERY_ID));
+    } finally {
+      queryExecutionMap.remove(QUERY_ID);
+    }
+  }
+
+  @Test
+  public void testFetchResultsOfOwnQueryWithoutStatementIdIsStillServed() 
throws Exception {
+    ClientSession owner = createSession("user");
+    owner.addStatementId(STATEMENT_ID);
+    owner.addQueryId(STATEMENT_ID, QUERY_ID);
+    owner.setLogin(true);
+
+    Map<Long, IQueryExecution> queryExecutionMap = getQueryExecutionMap();
+    queryExecutionMap.put(QUERY_ID, mockQueryExecution());
+    try {
+      // the query id is bound to a statement of this session, the request 
does not mention it
+      withCurrentSession(
+          owner,
+          () ->
+              Assert.assertEquals(
+                  TSStatusCode.SUCCESS_STATUS.getStatusCode(),
+                  new ClientRPCServiceImpl()
+                      .fetchResults(createFetchResultsReq(owner))
+                      .getStatus()
+                      .getCode()));
+      // a fully consumed query is released for the session that submitted it
+      Assert.assertFalse(queryExecutionMap.containsKey(QUERY_ID));
+      Assert.assertFalse(owner.containsQueryId(STATEMENT_ID, QUERY_ID));
+    } finally {
+      queryExecutionMap.remove(QUERY_ID);
+    }
+  }
+
+  @Test
+  public void testFetchResultsOfQueryWithMoreDataKeepsQueryAndSessionBinding() 
throws Exception {
+    ClientSession owner = createSession("user");
+    owner.addStatementId(STATEMENT_ID);
+    owner.addQueryId(STATEMENT_ID, QUERY_ID);
+    owner.setLogin(true);
+
+    Map<Long, IQueryExecution> queryExecutionMap = getQueryExecutionMap();
+    queryExecutionMap.put(QUERY_ID, mockQueryExecution(true));
+    try {
+      withCurrentSession(
+          owner,
+          () -> {
+            TSFetchResultsResp response =
+                new ClientRPCServiceImpl()
+                    
.fetchResultsV2(createFetchResultsReqWithStatementId(owner));
+            Assert.assertEquals(
+                TSStatusCode.SUCCESS_STATUS.getStatusCode(), 
response.getStatus().getCode());
+            Assert.assertTrue(response.isMoreData());
+          });
+      // the result set is not consumed yet, so the query has to stay 
fetchable by its owner
+      Assert.assertTrue(queryExecutionMap.containsKey(QUERY_ID));
+      Assert.assertTrue(owner.containsQueryId(STATEMENT_ID, QUERY_ID));
+    } finally {
+      queryExecutionMap.remove(QUERY_ID);
+    }
+  }
+
+  @Test
+  public void testCloseOperationRejectsQueryOfAnotherSession() throws 
Exception {
+    ClientSession anotherSession = createSession("user");
+    anotherSession.addStatementId(STATEMENT_ID);
+    anotherSession.setLogin(true);
+
+    Map<Long, IQueryExecution> queryExecutionMap = getQueryExecutionMap();
+    queryExecutionMap.put(QUERY_ID, mockQueryExecution());
+    try {
+      withCurrentSession(
+          anotherSession,
+          () ->
+              Assert.assertEquals(
+                  TSStatusCode.NO_PERMISSION.getStatusCode(),
+                  new 
ClientRPCServiceImpl().closeOperation(createCloseOperationReq()).getCode()));
+      // the rejected request must not release the query of the session that 
submitted it
+      Assert.assertTrue(queryExecutionMap.containsKey(QUERY_ID));
+    } finally {
+      queryExecutionMap.remove(QUERY_ID);
+    }
+  }
+
+  @Test
+  public void 
testCloseOperationDoesNotReleaseQueryRegisteredWhileTheRequestIsServed()
+      throws Exception {
+    Map<Long, IQueryExecution> queryExecutionMap = getQueryExecutionMap();
+    IQueryExecution queryOfAnotherSession = mockQueryExecution();
+    // Query ids are allocated before their execution is published, so the 
session that owns this
+    // queryId can register its execution between the ownership check of this 
request and the point
+    // where the request would release the query. Simulate that publication 
from inside the
+    // ownership check, which is where the request decides who may release the 
queryId.
+    ClientSession anotherSession =
+        new ClientSession(Mockito.mock(Socket.class)) {
+          @Override
+          public boolean containsQueryId(Long statementId, long queryId) {
+            queryExecutionMap.putIfAbsent(queryId, queryOfAnotherSession);
+            return super.containsQueryId(statementId, queryId);
+          }
+        };
+    anotherSession.setUsername("user");
+    anotherSession.addStatementId(STATEMENT_ID);
+    anotherSession.setLogin(true);
+
+    try {
+      withCurrentSession(
+          anotherSession,
+          () ->
+              Assert.assertEquals(
+                  TSStatusCode.NO_PERMISSION.getStatusCode(),
+                  new 
ClientRPCServiceImpl().closeOperation(createCloseOperationReq()).getCode()));
+      // the request must never release the query that was just registered by 
its owner
+      Assert.assertTrue(queryExecutionMap.containsKey(QUERY_ID));
+    } finally {
+      queryExecutionMap.remove(QUERY_ID);
+    }
+  }
+
+  @Test
+  public void testCloseOperationReleasesQueryOfOwnSession() throws Exception {
+    ClientSession owner = createSession("user");
+    owner.addStatementId(STATEMENT_ID);
+    owner.addQueryId(STATEMENT_ID, QUERY_ID);
+    owner.setLogin(true);
+
+    Map<Long, IQueryExecution> queryExecutionMap = getQueryExecutionMap();
+    queryExecutionMap.put(QUERY_ID, mockQueryExecution());
+    try {
+      withCurrentSession(
+          owner,
+          () ->
+              Assert.assertEquals(
+                  TSStatusCode.SUCCESS_STATUS.getStatusCode(),
+                  new 
ClientRPCServiceImpl().closeOperation(createCloseOperationReq()).getCode()));
+      Assert.assertFalse(queryExecutionMap.containsKey(QUERY_ID));
+      Assert.assertFalse(owner.containsQueryId(STATEMENT_ID, QUERY_ID));
+    } finally {
+      queryExecutionMap.remove(QUERY_ID);
+    }
+  }
+
+  @Test
+  public void testCloseOperationOfQueryThatIsNoLongerRunningStaysANoOp() {
+    // a client that consumed a result set completely sends closeOperation 
after the query has
+    // already been released, and that request has to succeed as it always did
+    ClientSession session = createSession("user");
+    session.addStatementId(STATEMENT_ID);
+    session.setLogin(true);
+
+    withCurrentSession(
+        session,
+        () ->
+            Assert.assertEquals(
+                TSStatusCode.SUCCESS_STATUS.getStatusCode(),
+                new 
ClientRPCServiceImpl().closeOperation(createCloseOperationReq()).getCode()));
+  }
+
+  private IQueryExecution mockQueryExecution() throws Exception {
+    return mockQueryExecution(false);
+  }
+
+  /**
+   * A mocked execution. {@code hasNextResult} says whether the mocked query 
still holds data after
+   * the batch that is about to be read, which decides if a single fetch 
consumes the whole result
+   * set and therefore releases the query.
+   */
+  private IQueryExecution mockQueryExecution(boolean hasNextResult) throws 
Exception {
+    IQueryExecution queryExecution = Mockito.mock(IQueryExecution.class);
+    Mockito.when(queryExecution.getQueryId()).thenReturn("query");
+    // the mock carries no buffered batch, only the "is there anything left" 
flag
+    
Mockito.when(queryExecution.getByteBufferBatchResult()).thenReturn(Optional.empty());
+    Mockito.when(queryExecution.hasNextResult()).thenReturn(hasNextResult);
+    return queryExecution;
+  }
+
+  /** The V1 fetch request of the legacy JDBC data set, which does not send a 
statement id. */
+  private TSFetchResultsReq createFetchResultsReq(ClientSession session) {
+    return new TSFetchResultsReq(session.getId(), "select 1", 1024, QUERY_ID, 
true);
+  }
+
+  /** The V2 fetch request, which always carries the statement id. */
+  private TSFetchResultsReq createFetchResultsReqWithStatementId(ClientSession 
session) {
+    return createFetchResultsReq(session).setStatementId(STATEMENT_ID);
+  }
+
+  private TSCloseOperationReq createCloseOperationReq() {
+    return new 
TSCloseOperationReq().setStatementId(STATEMENT_ID).setQueryId(QUERY_ID);
+  }
+
+  private void withCurrentSession(ClientSession session, Runnable body) {
+    SessionManager sessionManager = SessionManager.getInstance();
+    IClientSession previousSession = sessionManager.getCurrSession();
+    sessionManager.setCurrSession(session);
+    try {
+      body.run();
+    } finally {
+      sessionManager.restoreSession(previousSession, session);
+    }
+  }
+
+  private ClientSession createSession(String username) {
+    ClientSession session = new ClientSession(Mockito.mock(Socket.class));
+    session.setUsername(username);
+    return session;
+  }
+
+  @SuppressWarnings("unchecked")
+  private Map<Long, IQueryExecution> getQueryExecutionMap() throws Exception {
+    Field field = Coordinator.class.getDeclaredField("queryExecutionMap");
+    field.setAccessible(true);
+    return (Map<Long, IQueryExecution>) field.get(Coordinator.getInstance());
+  }
+}

Reply via email to