This is an automated email from the ASF dual-hosted git repository. jt2594838 pushed a commit to branch insert_numeric_row in repository https://gitbox.apache.org/repos/asf/iotdb.git
commit 2794bfc6adcb444d3973f1a00e859384c6f4d3ec Author: Tian Jiang <[email protected]> AuthorDate: Wed Sep 30 11:09:20 2026 +0800 fix multiple issues --- .../manual/basic/IoTDBPipeNumericRowIT.java | 449 +++++++++++++++++++++ .../it/session/IoTDBTableModelSessionIT.java | 206 ++++++++-- .../it/session/WritePerformanceIoTracker.java | 215 ++++++++++ .../apache/iotdb/session/i18n/SessionMessages.java | 1 + .../apache/iotdb/session/i18n/SessionMessages.java | 1 + .../org/apache/iotdb/session/TableSession.java | 29 ++ .../iotdb/session/pool/TableSessionWrapper.java | 35 +- .../session/PreparedNumericRowMetadataTest.java | 218 ++++++++++ .../session/pool/PreparedNumericRowPoolTest.java | 222 ++++++++++ .../statement/PipeConvertedInsertRowStatement.java | 35 +- ...leStatementDataTypeConvertExecutionVisitor.java | 10 + .../protocol/thrift/impl/ClientRPCServiceImpl.java | 8 + .../PipeConvertedInsertRowStatementTest.java | 146 +++++++ ...ClientRPCServiceImplPreparedNumericRowTest.java | 195 +++++++++ .../plan/parser/NumericTableRowTest.java | 61 +++ 15 files changed, 1784 insertions(+), 47 deletions(-) diff --git a/integration-test/src/test/java/org/apache/iotdb/pipe/it/dual/tablemodel/manual/basic/IoTDBPipeNumericRowIT.java b/integration-test/src/test/java/org/apache/iotdb/pipe/it/dual/tablemodel/manual/basic/IoTDBPipeNumericRowIT.java new file mode 100644 index 00000000000..5211f290771 --- /dev/null +++ b/integration-test/src/test/java/org/apache/iotdb/pipe/it/dual/tablemodel/manual/basic/IoTDBPipeNumericRowIT.java @@ -0,0 +1,449 @@ +/* + * 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.pipe.it.dual.tablemodel.manual.basic; + +import org.apache.iotdb.common.rpc.thrift.TConsensusGroupType; +import org.apache.iotdb.common.rpc.thrift.TDataNodeLocation; +import org.apache.iotdb.commons.client.sync.SyncConfigNodeIServiceClient; +import org.apache.iotdb.commons.cluster.NodeStatus; +import org.apache.iotdb.commons.cluster.RegionRoleType; +import org.apache.iotdb.confignode.rpc.thrift.TCreatePipeReq; +import org.apache.iotdb.confignode.rpc.thrift.TRegionInfo; +import org.apache.iotdb.confignode.rpc.thrift.TShowClusterResp; +import org.apache.iotdb.confignode.rpc.thrift.TShowRegionReq; +import org.apache.iotdb.confignode.rpc.thrift.TShowRegionResp; +import org.apache.iotdb.consensus.ConsensusFactory; +import org.apache.iotdb.isession.ITableSession; +import org.apache.iotdb.isession.NumericRow; +import org.apache.iotdb.isession.NumericRowPreparedStatement; +import org.apache.iotdb.isession.SessionConfig; +import org.apache.iotdb.it.env.MultiEnvFactory; +import org.apache.iotdb.it.framework.IoTDBTestRunner; +import org.apache.iotdb.itbase.category.MultiClusterIT2DualTableManualBasic; +import org.apache.iotdb.itbase.env.BaseEnv; +import org.apache.iotdb.pipe.it.dual.tablemodel.manual.AbstractPipeTableModelDualManualIT; +import org.apache.iotdb.rpc.TSStatusCode; +import org.apache.iotdb.session.TableSessionBuilder; + +import org.apache.tsfile.enums.ColumnCategory; +import org.apache.tsfile.enums.TSDataType; +import org.apache.tsfile.utils.BitMap; +import org.junit.Before; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.time.LocalDate; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +@RunWith(IoTDBTestRunner.class) +@Category(MultiClusterIT2DualTableManualBasic.class) +public class IoTDBPipeNumericRowIT extends AbstractPipeTableModelDualManualIT { + private static final String DATABASE = "numeric_sync"; + private static final List<String> NAMES = + Arrays.asList("d", "device", "f", "l", "region", "i", "b", "event_date", "event_time"); + private static final List<TSDataType> TYPES = + Arrays.asList( + TSDataType.DOUBLE, + TSDataType.STRING, + TSDataType.FLOAT, + TSDataType.INT64, + TSDataType.STRING, + TSDataType.INT32, + TSDataType.BOOLEAN, + TSDataType.DATE, + TSDataType.TIMESTAMP); + private static final List<ColumnCategory> CATEGORIES = + Arrays.asList( + ColumnCategory.FIELD, + ColumnCategory.TAG, + ColumnCategory.FIELD, + ColumnCategory.FIELD, + ColumnCategory.TAG, + ColumnCategory.FIELD, + ColumnCategory.FIELD, + ColumnCategory.FIELD, + ColumnCategory.FIELD); + + @Override + @Before + public void setUp() { + MultiEnvFactory.createEnv(2); + senderEnv = MultiEnvFactory.getEnv(0); + receiverEnv = MultiEnvFactory.getEnv(1); + setupConfig(); + senderEnv + .getConfig() + .getCommonConfig() + .setDataRegionConsensusProtocolClass(ConsensusFactory.IOT_CONSENSUS) + .setDataRegionGroupExtensionPolicy("CUSTOM") + .setDefaultDataRegionGroupNumPerDatabase(1) + .setDataReplicationFactor(3) + .setSchemaReplicationFactor(3); + senderEnv + .getConfig() + .getDataNodeConfig() + .setMetricReporterType(Collections.singletonList("PROMETHEUS")); + receiverEnv + .getConfig() + .getCommonConfig() + .setDataRegionConsensusProtocolClass(ConsensusFactory.IOT_CONSENSUS); + senderEnv.initClusterEnvironment(1, 3); + receiverEnv.initClusterEnvironment(1, 1); + } + + @Test + public void testRealtimeWithoutBatch() throws Exception { + // Unflushed NumericRow and prepared rows must arrive through individual InsertNode requests. + testRealtime(false); + } + + @Test + public void testRealtimeWithBatch() throws Exception { + // Batch transfer must preserve all numeric types, FIELD null bits, and null/empty STRING TAGs. + testRealtime(true); + } + + private void testRealtime(boolean batch) throws Exception { + createTable(); + createPipe(batch, false); + writeRows(0, 32); + assertRowsEventually(receiverEnv, 0, 32); + // The destination must remain readable after its own MemTable has been flushed. + try (ITableSession session = receiverEnv.getTableSessionConnection()) { + session.executeNonQueryStatement("FLUSH"); + } + assertRowsEventually(receiverEnv, 0, 32); + } + + @Test + public void testHistoricalTsFile() throws Exception { + // Start the pipe after flushing so the historical TsFile path also carries numeric-row data. + createTable(); + writeRows(0, 32); + try (ITableSession session = senderEnv.getTableSessionConnection()) { + session.executeNonQueryStatement("FLUSH"); + } + createPipe(true, true); + assertRowsEventually(receiverEnv, 0, 32); + } + + @Test + public void testIoTConsensusReplicaAndRestart() throws Exception { + // Pin writes to the actual DataRegion leader, then stop that leader before querying followers. + // Merely stopping a client entry node can still leave all reads on the original leader. + createTable(); + writeRows(0, 16); + int leader = findDataRegionLeader(); + awaitReplication(leader, 16); + int offlineFollower = (leader + 1) % 3; + senderEnv.shutdownDataNode(offlineFollower); + try { + awaitNodeStatus(offlineFollower, NodeStatus.Unknown); + writeRows(16, 32, leader); + } finally { + senderEnv.startDataNode(offlineFollower); + } + awaitNodeStatus(offlineFollower, NodeStatus.Running); + // Running only confirms node health; IoTConsensus catch-up must finish before losing its + // sender. + awaitReplication(leader, 32); + // Resolve the leader again after restart instead of assuming its assignment is unchanged. + int stoppedLeader = findDataRegionLeader(); + assertEquals( + "The primary that accepted the second write batch must be stopped", leader, stoppedLeader); + senderEnv.shutdownDataNode(stoppedLeader); + try { + assertFalse(senderEnv.getDataNodeWrapper(stoppedLeader).isAlive()); + awaitNodeStatus(stoppedLeader, NodeStatus.Unknown); + for (int follower = 0; follower < 3; follower++) { + if (follower == stoppedLeader) { + continue; + } + assertFalse(senderEnv.getDataNodeWrapper(stoppedLeader).isAlive()); + assertRowsEventually(senderEnv, follower, 32); + assertFalse(senderEnv.getDataNodeWrapper(stoppedLeader).isAlive()); + System.out.printf( + "NumericRow IoTConsensus: stopped primary port=%d, query follower port=%d, verified rows=32%n", + senderEnv.getDataNodeWrapper(stoppedLeader).getPort(), + senderEnv.getDataNodeWrapper(follower).getPort()); + } + } finally { + senderEnv.startDataNode(stoppedLeader); + } + } + + private void awaitReplication(int leader, int expectedWrites) { + String metricsUrl = + "http://" + + senderEnv.getDataNodeWrapper(leader).getIp() + + ":" + + senderEnv.getDataNodeWrapper(leader).getMetricPort() + + "/metrics"; + await() + .atMost(90, TimeUnit.SECONDS) + .pollInterval(1, TimeUnit.SECONDS) + .untilAsserted( + () -> { + String metrics = senderEnv.getUrlContent(metricsUrl, null); + assertNotNull(metrics); + // Require the latest write index too: an old zero-lag sample is not catch-up proof. + // This database has exactly one DataRegion and each numeric row adds one write. + double searchIndex = -1; + double safeIndex = -1; + for (String line : metrics.split("\\R")) { + if (!line.startsWith("iot_consensus{")) { + continue; + } + double value = Double.parseDouble(line.substring(line.lastIndexOf(' ') + 1)); + if (line.contains("type=\"searchIndex\"")) { + searchIndex = value; + } else if (line.contains("type=\"safeIndex\"")) { + safeIndex = value; + } + } + assertTrue( + "Missing or stale searchIndex at " + metricsUrl, searchIndex >= expectedWrites); + assertEquals("Replication is incomplete at " + metricsUrl, searchIndex, safeIndex, 0); + }); + } + + private void awaitNodeStatus(int node, NodeStatus expected) { + // BaseEnv.ensureNodeStatus uses EnvFactory, which cannot be used with MultiCluster + // environments. + await() + .atMost(90, TimeUnit.SECONDS) + .pollInterval(1, TimeUnit.SECONDS) + .untilAsserted( + () -> { + try (SyncConfigNodeIServiceClient client = + (SyncConfigNodeIServiceClient) senderEnv.getLeaderConfigNodeConnection()) { + TShowClusterResp response = client.showCluster(); + assertEquals( + TSStatusCode.SUCCESS_STATUS.getStatusCode(), response.getStatus().getCode()); + List<TDataNodeLocation> matches = + response.getDataNodeList().stream() + .filter( + location -> + location.getClientRpcEndPoint().getPort() + == senderEnv.getDataNodeWrapper(node).getPort()) + .toList(); + assertEquals(1, matches.size()); + assertEquals( + expected.getStatus(), + response.getNodeStatus().get(matches.get(0).getDataNodeId())); + } + }); + } + + private int findDataRegionLeader() throws Exception { + try (SyncConfigNodeIServiceClient client = + (SyncConfigNodeIServiceClient) senderEnv.getLeaderConfigNodeConnection()) { + TShowRegionResp response = + client.showRegion( + new TShowRegionReq() + .setConsensusGroupType(TConsensusGroupType.DataRegion) + .setDatabases(Collections.singletonList(DATABASE)) + .setIsTableModel(true)); + assertEquals(TSStatusCode.SUCCESS_STATUS.getStatusCode(), response.getStatus().getCode()); + // Exactly one three-replica region ensures stopping this leader covers all inserted rows. + List<TRegionInfo> replicas = response.getRegionInfoList(); + assertEquals(3, replicas.size()); + for (TRegionInfo replica : replicas) { + assertEquals(replicas.get(0).getConsensusGroupId(), replica.getConsensusGroupId()); + } + List<TRegionInfo> leaders = + replicas.stream() + .filter(replica -> RegionRoleType.Leader.getRoleType().equals(replica.getRoleType())) + .toList(); + assertEquals(1, leaders.size()); + TRegionInfo leader = leaders.get(0); + for (int node = 0; node < 3; node++) { + if (senderEnv.getDataNodeWrapper(node).getPort() == leader.getClientRpcPort()) { + System.out.printf( + "NumericRow IoTConsensus: region=%d, primary DataNodeId=%d, port=%d%n", + leader.getConsensusGroupId().getId(), + leader.getDataNodeId(), + leader.getClientRpcPort()); + return node; + } + } + throw new AssertionError("No wrapper for DataRegion leader " + leader); + } + } + + private void createTable() throws Exception { + try (ITableSession session = senderEnv.getTableSessionConnection()) { + session.executeNonQueryStatement("CREATE DATABASE " + DATABASE); + session.executeNonQueryStatement("USE " + DATABASE); + session.executeNonQueryStatement( + "CREATE TABLE readings(region STRING TAG, device STRING TAG, missing STRING TAG, " + + "d DOUBLE FIELD, f FLOAT FIELD, l INT64 FIELD, i INT32 FIELD, b BOOLEAN FIELD, " + + "event_date DATE FIELD, event_time TIMESTAMP FIELD)"); + } + } + + private void createPipe(boolean batch, boolean history) throws Exception { + Map<String, String> source = new HashMap<>(); + source.put("capture.table", "true"); + source.put("capture.tree", "false"); + source.put("__system.sql-dialect", "table"); + source.put("user", "root"); + source.put("database-name", DATABASE); + source.put("table-name", "readings"); + source.put("source.realtime.mode", "log"); + source.put("source.history.enable", Boolean.toString(history)); + Map<String, String> sink = new HashMap<>(); + sink.put("sink", "iotdb-thrift-sink"); + sink.put("sink.ip", receiverEnv.getDataNodeWrapper(0).getIp()); + sink.put("sink.port", Integer.toString(receiverEnv.getDataNodeWrapper(0).getPort())); + sink.put("sink.batch.enable", Boolean.toString(batch)); + sink.put("sink.batch.max-delay-seconds", "1"); + sink.put("sink.format", history ? "tsfile" : "tablet"); + try (SyncConfigNodeIServiceClient client = + (SyncConfigNodeIServiceClient) senderEnv.getLeaderConfigNodeConnection()) { + assertEquals( + TSStatusCode.SUCCESS_STATUS.getStatusCode(), + client + .createPipe( + new TCreatePipeReq("numeric_pipe", sink) + .setExtractorAttributes(source) + .setProcessorAttributes(Collections.emptyMap())) + .getCode()); + assertEquals( + TSStatusCode.SUCCESS_STATUS.getStatusCode(), client.startPipe("numeric_pipe").getCode()); + } + } + + private void writeRows(int start, int end) throws Exception { + writeRows(start, end, 0); + } + + private void writeRows(int start, int end, int node) throws Exception { + // Pin the client to the selected endpoint; its index is not necessarily the DataRegion leader. + try (ITableSession session = + new TableSessionBuilder() + .nodeUrls( + Collections.singletonList( + senderEnv.getDataNodeWrapper(node).getIpAndPortString())) + .username(SessionConfig.DEFAULT_USER) + .password(SessionConfig.DEFAULT_PASSWORD) + .database(DATABASE) + .enableRedirection(false) + .build(); + NumericRowPreparedStatement prepared = + session.prepareInsertNumericRow("readings", NAMES, TYPES, CATEGORIES)) { + for (int row = start; row < end; row++) { + long[] values = { + Double.doubleToRawLongBits(row + 0.25), + Float.floatToRawIntBits(-row - 0.5f), + Long.MIN_VALUE + row, + Integer.MIN_VALUE + row, + row % 2, + 20260924, + 1000L + row + }; + String[] tags = {device(row), "region"}; + BitMap nulls = new BitMap(7); + if (row % 3 == 0) { + nulls.mark(row % 7); + } + if (row % 2 == 0) { + session.executePreparedInsertNumericRow(prepared, row, values, tags, nulls); + } else { + session.insert( + new NumericRow("readings", row, NAMES, TYPES, CATEGORIES, values, tags, nulls)); + } + } + } + } + + private static String device(int row) { + return row % 3 == 0 ? null : row % 3 == 1 ? "" : "device-\u8bbe\u5907"; + } + + private void assertRowsEventually(BaseEnv env, int node, int count) { + await() + // Restart and leader failover can temporarily reject JDBC connections or query RPCs. + .ignoreExceptionsInstanceOf(SQLException.class) + .atMost(90, TimeUnit.SECONDS) + .pollInterval(1, TimeUnit.SECONDS) + .untilAsserted( + () -> { + try (Connection connection = + env.getConnection( + env.getDataNodeWrapper(node), + SessionConfig.DEFAULT_USER, + SessionConfig.DEFAULT_PASSWORD, + "table"); + Statement statement = connection.createStatement()) { + statement.execute("USE " + DATABASE); + try (ResultSet result = + statement.executeQuery( + "SELECT time,region,device,missing,d,f,l,i,b,event_date,event_time FROM readings ORDER BY time")) { + for (int row = 0; row < count; row++) { + assertTrue("Missing row " + row, result.next()); + assertEquals(row, result.getLong(1)); + assertEquals("region", result.getString(2)); + assertEquals(device(row), result.getString(3)); + assertNull(result.getObject(4)); + for (int field = 0; field < 7; field++) { + int column = field + 5; + if (row % 3 == 0 && field == row % 7) { + assertNull(result.getObject(column)); + continue; + } + assertNotNull(result.getObject(column)); + switch (field) { + case 0 -> assertEquals(row + 0.25, result.getDouble(column), 0); + case 1 -> assertEquals(-row - 0.5f, result.getFloat(column), 0); + case 2 -> assertEquals(Long.MIN_VALUE + row, result.getLong(column)); + case 3 -> assertEquals(Integer.MIN_VALUE + row, result.getInt(column)); + case 4 -> assertEquals(row % 2 != 0, result.getBoolean(column)); + case 5 -> + assertEquals( + LocalDate.of(2026, 9, 24), result.getDate(column).toLocalDate()); + case 6 -> assertEquals(1000L + row, result.getLong(column)); + default -> throw new AssertionError(field); + } + } + } + assertFalse(result.next()); + } + } + }); + } +} diff --git a/integration-test/src/test/java/org/apache/iotdb/relational/it/session/IoTDBTableModelSessionIT.java b/integration-test/src/test/java/org/apache/iotdb/relational/it/session/IoTDBTableModelSessionIT.java index 3b0313ec006..e196ff1edf0 100644 --- a/integration-test/src/test/java/org/apache/iotdb/relational/it/session/IoTDBTableModelSessionIT.java +++ b/integration-test/src/test/java/org/apache/iotdb/relational/it/session/IoTDBTableModelSessionIT.java @@ -72,6 +72,16 @@ public class IoTDBTableModelSessionIT { @Before public void setUp() throws Exception { + if (Boolean.getBoolean("iotdb.numeric.row.insert.perf.enabled") + && Boolean.getBoolean("iotdb.numeric.row.insert.perf.io.enabled")) { + // Isolate per-round storage accounting from compaction of earlier rounds. + EnvFactory.getEnv() + .getConfig() + .getCommonConfig() + .setEnableSeqSpaceCompaction(false) + .setEnableUnseqSpaceCompaction(false) + .setEnableCrossSpaceCompaction(false); + } EnvFactory.getEnv().initClusterEnvironment(); } @@ -431,10 +441,11 @@ public class IoTDBTableModelSessionIT { @Test public void testPreparedNumericRowAndTabletWritePerformance() throws Exception { + // Compare synchronous single-row paths and optionally account for their Linux TCP/storage I/O. Assume.assumeTrue( "Performance benchmark disabled; set iotdb.numeric.row.insert.perf.enabled=true to run", Boolean.getBoolean("iotdb.numeric.row.insert.perf.enabled")); - final int concurrency = 16; + final int concurrency = Integer.getInteger("iotdb.numeric.row.insert.perf.concurrency", 16); // Compare sustained single-row writes after warming all three paths; verify all acknowledged // rows. final int warmupRounds = 2; @@ -497,30 +508,21 @@ public class IoTDBTableModelSessionIT { pathRows[0][threadId], numericValues[threadId], tags[threadId]); - pathRows[0][threadId]++; } catch (Exception e) { throw new RuntimeException(e); } }; - IntConsumer tabletWrite = + IntConsumer tabletPrepare = threadId -> { - try { - Tablet tablet = tablets[threadId]; - tablet.addTimestamp(0, pathRows[2][threadId]); - tablet.addValue("tag", 0, tags[threadId][0]); - for (int i = 0; i < fieldCount; i++) { - tablet.addValue(names.get(i + 1), 0, 1.0D); - } - sessions[threadId].insert(tablet); - tablet.reset(); - pathRows[2][threadId]++; - } catch (Exception e) { - throw new RuntimeException(e); + Tablet tablet = tablets[threadId]; + tablet.addTimestamp(0, pathRows[2][threadId]); + tablet.addValue("tag", 0, tags[threadId][0]); + for (int i = 0; i < fieldCount; i++) { + tablet.addValue(names.get(i + 1), 0, 1.0D); } }; - IntConsumer rowWrite = - threadId -> { - try { + IntConsumer rowPrepare = + threadId -> rows[threadId] = new NumericRow( "row_perf", @@ -530,51 +532,112 @@ public class IoTDBTableModelSessionIT { categories, numericValues[threadId], tags[threadId]); - sessions[threadId].insert(rows[threadId]); - pathRows[1][threadId]++; - } catch (Exception e) { - throw new RuntimeException(e); - } - }; - IntConsumer[] paths = {preparedWrite, rowWrite, tabletWrite}; + IntConsumer[] paths = { + preparedWrite, + threadId -> { + try { + sessions[threadId].insert(rows[threadId]); + } catch (Exception e) { + throw new RuntimeException(e); + } + }, + threadId -> { + try { + sessions[threadId].insert(tablets[threadId]); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }; + IntConsumer[] prepareWrites = {null, rowPrepare, tabletPrepare}; + IntConsumer[] completeWrites = { + threadId -> pathRows[0][threadId]++, + threadId -> pathRows[1][threadId]++, + threadId -> { + tablets[threadId].reset(); + pathRows[2][threadId]++; + } + }; String[] pathNames = {"prepared numeric row", "insert NumericRow", "insertTablet batch=1"}; System.out.printf("configuration: concurrency=%d, tabletBatch=1%n", concurrency); + WritePerformanceIoTracker ioTracker = null; + if (Boolean.getBoolean("iotdb.numeric.row.insert.perf.io.enabled")) { + assertEquals( + "I/O measurement requires one local DataNode", + 1, + EnvFactory.getEnv().getDataNodeWrapperList().size()); + assertEquals( + "I/O measurement requires one local ConfigNode", + 1, + EnvFactory.getEnv().getConfigNodeWrapperList().size()); + ioTracker = + new WritePerformanceIoTracker( + EnvFactory.getEnv().getDataNodeWrapperList().get(0).getPid(), + EnvFactory.getEnv().getConfigNodeWrapperList().get(0).getPid(), + EnvFactory.getEnv().getDataNodeWrapperList().get(0).getPort()); + } double[][] rates = new double[paths.length][measuredRounds]; long[] totalRows = new long[paths.length]; long[] totalNanos = new long[paths.length]; + LongSamples[] requestLatencies = new LongSamples[paths.length]; + for (int path = 0; path < paths.length; path++) { + requestLatencies[path] = new LongSamples(); + } for (int round = 0; round < warmupRounds + measuredRounds; round++) { boolean warmup = round < warmupRounds; int seconds = warmup ? 3 : 5; // Rotate all three paths so each occupies every position during measured rounds. for (int position = 0; position < paths.length; position++) { int path = (round + position) % paths.length; + WritePerformanceIoTracker.Snapshot ioBefore = + ioTracker == null ? null : ioTracker.beforeRound(sessions[0]); WritePerformance result = - runConcurrentWrites(executor, concurrency, seconds, paths[path]); + runConcurrentWrites( + executor, + concurrency, + seconds, + prepareWrites[path], + paths[path], + completeWrites[path]); + if (ioTracker != null) { + System.out.printf( + "io %s round %d position %d %s: rows=%d, %s%n", + warmup ? "warmup" : "measured", + warmup ? round + 1 : round - warmupRounds + 1, + position + 1, + pathNames[path], + result.rows(), + ioTracker.afterRound(ioBefore, sessions[0])); + } System.out.printf( - "%s round %d position %d %s: %d rows, %.3f s, %.1f rows/s%n", + "%s round %d position %d %s: %d rows, %.3f s, %.1f rows/s, API latency %s%n", warmup ? "warmup" : "measured", warmup ? round + 1 : round - warmupRounds + 1, position + 1, pathNames[path], result.rows(), result.nanos() / 1_000_000_000.0, - result.rowsPerSecond()); + result.rowsPerSecond(), + summarizeRequestLatencies(result.requestNanos())); if (!warmup) { rates[path][round - warmupRounds] = result.rowsPerSecond(); totalRows[path] += result.rows(); totalNanos[path] += result.nanos(); + requestLatencies[path].appendAll(result.requestNanos()); } } } for (int path = 0; path < paths.length; path++) { Arrays.sort(rates[path]); System.out.printf( - "summary %s: median %.1f rows/s, aggregate %.1f rows/s, min %.1f, max %.1f%n", + "summary %s: median %.1f rows/s, aggregate %.1f rows/s, min %.1f, max %.1f, " + + "measured API latency %s%n", pathNames[path], (rates[path][measuredRounds / 2 - 1] + rates[path][measuredRounds / 2]) / 2, totalRows[path] * 1_000_000_000.0 / totalNanos[path], rates[path][0], - rates[path][measuredRounds - 1]); + rates[path][measuredRounds - 1], + summarizeRequestLatencies(requestLatencies[path].toArray())); } // Per-device monotonic timestamps span warmup and measurement, so no rows overwrite others. for (int path = 0; path < paths.length; path++) { @@ -619,14 +682,80 @@ public class IoTDBTableModelSessionIT { } } - private record WritePerformance(long rows, long nanos) { + private record WritePerformance(long rows, long nanos, long[] requestNanos) { double rowsPerSecond() { return rows * 1_000_000_000.0 / nanos; } } + private record RequestLatency( + long count, double averageNanos, long p50Nanos, long p95Nanos, long p99Nanos) { + @Override + public String toString() { + return String.format( + "n=%d, avg=%.3f us, p50=%.3f us, p95=%.3f us, p99=%.3f us", + count, + averageNanos / 1_000.0, + p50Nanos / 1_000.0, + p95Nanos / 1_000.0, + p99Nanos / 1_000.0); + } + } + + private static class LongSamples { + private long[] values = new long[1024]; + private int size; + + void add(long value) { + if (size == values.length) { + values = Arrays.copyOf(values, values.length * 2); + } + values[size++] = value; + } + + void appendAll(long[] samples) { + while (values.length - size < samples.length) { + values = Arrays.copyOf(values, values.length * 2); + } + System.arraycopy(samples, 0, values, size, samples.length); + size += samples.length; + } + + long[] toArray() { + return Arrays.copyOf(values, size); + } + } + + private static RequestLatency summarizeRequestLatencies(long[] requestNanos) { + if (requestNanos.length == 0) { + return new RequestLatency(0, 0, 0, 0, 0); + } + long[] sorted = Arrays.copyOf(requestNanos, requestNanos.length); + Arrays.sort(sorted); + double sum = 0; + for (long latency : sorted) { + sum += latency; + } + return new RequestLatency( + sorted.length, + sum / sorted.length, + percentile(sorted, 0.50), + percentile(sorted, 0.95), + percentile(sorted, 0.99)); + } + + private static long percentile(long[] sorted, double quantile) { + return sorted[Math.max(0, (int) Math.ceil(quantile * sorted.length) - 1)]; + } + private static WritePerformance runConcurrentWrites( - ExecutorService executor, int concurrency, int seconds, IntConsumer write) throws Exception { + ExecutorService executor, + int concurrency, + int seconds, + IntConsumer prepare, + IntConsumer write, + IntConsumer complete) + throws Exception { CountDownLatch ready = new CountDownLatch(concurrency); CountDownLatch start = new CountDownLatch(1); AtomicLong deadline = new AtomicLong(); @@ -640,12 +769,19 @@ public class IoTDBTableModelSessionIT { ready.countDown(); start.await(); long rows = 0; + LongSamples requestNanos = new LongSamples(); while (!Thread.currentThread().isInterrupted() && System.nanoTime() < deadline.get()) { + if (prepare != null) { + prepare.accept(threadId); + } + long requestStart = System.nanoTime(); write.accept(threadId); + requestNanos.add(System.nanoTime() - requestStart); + complete.accept(threadId); rows++; } - return new WritePerformance(rows, System.nanoTime()); + return new WritePerformance(rows, System.nanoTime(), requestNanos.toArray()); })); } assertTrue(ready.await(30, TimeUnit.SECONDS)); @@ -654,13 +790,15 @@ public class IoTDBTableModelSessionIT { start.countDown(); long rows = 0; long endNanos = startNanos; + LongSamples requestNanos = new LongSamples(); for (Future<WritePerformance> future : futures) { WritePerformance result = future.get(seconds + 60, TimeUnit.SECONDS); rows += result.rows(); endNanos = Math.max(endNanos, result.nanos()); + requestNanos.appendAll(result.requestNanos()); } // Include completion of in-flight RPCs, but exclude main-thread collection/printing time. - return new WritePerformance(rows, endNanos - startNanos); + return new WritePerformance(rows, endNanos - startNanos, requestNanos.toArray()); } finally { start.countDown(); for (Future<WritePerformance> future : futures) { diff --git a/integration-test/src/test/java/org/apache/iotdb/relational/it/session/WritePerformanceIoTracker.java b/integration-test/src/test/java/org/apache/iotdb/relational/it/session/WritePerformanceIoTracker.java new file mode 100644 index 00000000000..bab6f0ad710 --- /dev/null +++ b/integration-test/src/test/java/org/apache/iotdb/relational/it/session/WritePerformanceIoTracker.java @@ -0,0 +1,215 @@ +/* + * 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.relational.it.session; + +import org.apache.iotdb.isession.ITableSession; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** Linux-only, opt-in counters sampled outside the single-row benchmark's timed write loops. */ +final class WritePerformanceIoTracker { + private static final Pattern TCP_COUNTER = + Pattern.compile("\\b(bytes_received|bytes_sent|bytes_retrans):(\\d+)\\b"); + + private final long dataNodePid; + private final long configNodePid; + private final int rpcPort; + + WritePerformanceIoTracker(long dataNodePid, long configNodePid, int rpcPort) throws IOException { + assertTrue("I/O measurement requires Linux /proc", Files.isDirectory(Path.of("/proc"))); + assertTrue("DataNode PID unavailable", dataNodePid > 0); + assertTrue("ConfigNode PID unavailable", configNodePid > 0); + this.dataNodePid = dataNodePid; + this.configNodePid = configNodePid; + this.rpcPort = rpcPort; + // Check readability early; unsupported counters must fail rather than appear as zero I/O. + readDisk(dataNodePid); + readDisk(configNodePid); + System.out.printf( + "io configuration: dataNodePid=%d, configNodePid=%d, rpcPort=%d, " + + "network=server TCP payload counters, disk=/proc/pid/io, " + + "flushBeforeAndAfter=true, compaction=false%n", + dataNodePid, configNodePid, rpcPort); + } + + Snapshot beforeRound(ITableSession session) throws Exception { + // Drain previous MemTables/WAL before the baseline; the following flush is charged to this + // round. + session.executeNonQueryStatement("FLUSH"); + return new Snapshot(readNetwork(), readDisk(dataNodePid), readDisk(configNodePid)); + } + + Result afterRound(Snapshot before, ITableSession session) throws Exception { + // Snapshot TCP before FLUSH so SQL/control traffic is excluded from write-request bytes. + Map<String, Traffic> network = readNetwork(); + assertEquals( + "RPC connections changed during I/O measurement", + before.network.keySet(), + network.keySet()); + Traffic traffic = new Traffic(0, 0, 0); + for (Map.Entry<String, Traffic> entry : network.entrySet()) { + traffic = traffic.plus(entry.getValue().minus(before.network.get(entry.getKey()))); + } + Disk activeDataNode = readDisk(dataNodePid).minus(before.dataNode); + long flushStart = System.nanoTime(); + session.executeNonQueryStatement("FLUSH"); + long flushNanos = System.nanoTime() - flushStart; + Disk dataNode = readDisk(dataNodePid).minus(before.dataNode); + Disk configNode = readDisk(configNodePid).minus(before.configNode); + return new Result(traffic, activeDataNode, dataNode, configNode, flushNanos); + } + + private Map<String, Traffic> readNetwork() throws Exception { + // Count only server endpoints on this test DataNode's RPC port. Loopback RX/TX are not doubled. + Process process = + new ProcessBuilder("ss", "-tinHO", "state", "established", "( sport = :" + rpcPort + " )") + .redirectErrorStream(true) + .start(); + CompletableFuture<String> output = + CompletableFuture.supplyAsync( + () -> { + try { + return new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new CompletionException(e); + } + }); + try { + assertTrue("ss did not finish", process.waitFor(10, TimeUnit.SECONDS)); + String text = output.get(10, TimeUnit.SECONDS); + assertEquals(text, 0, process.exitValue()); + Map<String, Traffic> result = new HashMap<>(); + for (String line : text.split("\\R")) { + if (line.isBlank()) continue; + String[] fields = line.trim().split("\\s+", 5); + assertTrue("Unexpected ss output: " + line, fields.length == 5); + Map<String, Long> counters = new HashMap<>(); + Matcher matcher = TCP_COUNTER.matcher(fields[4]); + while (matcher.find()) counters.put(matcher.group(1), Long.parseLong(matcher.group(2))); + assertTrue( + "TCP byte counters unavailable: " + line, + counters.containsKey("bytes_received") && counters.containsKey("bytes_sent")); + String connection = fields[2] + "->" + fields[3]; + assertFalse("Duplicate server socket: " + connection, result.containsKey(connection)); + // ss omits zero-valued bytes_retrans; the received/sent counters above are mandatory. + result.put( + connection, + new Traffic( + counters.get("bytes_received"), + counters.get("bytes_sent"), + counters.getOrDefault("bytes_retrans", 0L))); + } + assertFalse("No established test RPC connections", result.isEmpty()); + return result; + } finally { + process.destroyForcibly(); + output.cancel(true); + } + } + + private static Disk readDisk(long pid) throws IOException { + Map<String, Long> counters = new HashMap<>(); + for (String line : Files.readAllLines(Path.of("/proc", Long.toString(pid), "io"))) { + String[] fields = line.split(":\\s*", 2); + counters.put(fields[0], Long.parseLong(fields[1].trim())); + } + assertTrue( + "Process storage byte counters unavailable for PID " + pid, + counters.containsKey("read_bytes") + && counters.containsKey("write_bytes") + && counters.containsKey("cancelled_write_bytes")); + // These are kernel-accounted process storage bytes, not rchar/wchar (which include socket I/O), + // nor physical NAND bytes. Preserve cancelled writes separately instead of hiding them. + return new Disk( + counters.get("read_bytes"), + counters.get("write_bytes"), + counters.get("cancelled_write_bytes")); + } + + record Snapshot(Map<String, Traffic> network, Disk dataNode, Disk configNode) {} + + private record Traffic(long received, long sent, long retransmitted) { + Traffic minus(Traffic previous) { + assertTrue( + "TCP counters decreased", + received >= previous.received + && sent >= previous.sent + && retransmitted >= previous.retransmitted); + return new Traffic( + received - previous.received, + sent - previous.sent, + retransmitted - previous.retransmitted); + } + + Traffic plus(Traffic other) { + return new Traffic( + received + other.received, sent + other.sent, retransmitted + other.retransmitted); + } + } + + private record Disk(long read, long written, long cancelled) { + Disk minus(Disk previous) { + assertTrue( + "Process I/O counters decreased", + read >= previous.read && written >= previous.written && cancelled >= previous.cancelled); + return new Disk( + read - previous.read, written - previous.written, cancelled - previous.cancelled); + } + } + + record Result( + Traffic traffic, Disk activeDataNode, Disk dataNode, Disk configNode, long flushNanos) { + @Override + public String toString() { + return String.format( + "requestBytes=%d, responseBytes=%d, responseRetransmitBytes=%d, " + + "dnActiveReadBytes=%d, dnActiveWriteBytes=%d, dnActiveCancelledBytes=%d, " + + "dnReadBytes=%d, dnWriteBytes=%d, dnCancelledBytes=%d, " + + "cnReadBytes=%d, cnWriteBytes=%d, cnCancelledBytes=%d, flushNanos=%d", + traffic.received, + traffic.sent, + traffic.retransmitted, + activeDataNode.read, + activeDataNode.written, + activeDataNode.cancelled, + dataNode.read, + dataNode.written, + dataNode.cancelled, + configNode.read, + configNode.written, + configNode.cancelled, + flushNanos); + } + } +} diff --git a/iotdb-client/session/src/main/i18n/en/org/apache/iotdb/session/i18n/SessionMessages.java b/iotdb-client/session/src/main/i18n/en/org/apache/iotdb/session/i18n/SessionMessages.java index a1bdf45262e..2bcac9214ce 100644 --- a/iotdb-client/session/src/main/i18n/en/org/apache/iotdb/session/i18n/SessionMessages.java +++ b/iotdb-client/session/src/main/i18n/en/org/apache/iotdb/session/i18n/SessionMessages.java @@ -296,4 +296,5 @@ public final class SessionMessages { public static final String EXCEPTION_PREPARED_NUMERIC_ROW_PREPARE_RETURNED_NO_STATUS_6FF7BC24 = "Prepared numeric row prepare returned no status"; public static final String EXCEPTION_PREPARED_NUMERIC_ROW_VALUE_COUNT_MISMATCH_9ACD3A0C = "Prepared numeric row value count mismatch"; public static final String EXCEPTION_UNSUPPORTED_NUMERIC_TYPE_06DD3044 = "Unsupported numeric type"; + public static final String EXCEPTION_TABLE_SESSION_IS_CLOSED_77FE359B = "Table session is closed."; } diff --git a/iotdb-client/session/src/main/i18n/zh/org/apache/iotdb/session/i18n/SessionMessages.java b/iotdb-client/session/src/main/i18n/zh/org/apache/iotdb/session/i18n/SessionMessages.java index 260b000e61a..aa9c1055fd5 100644 --- a/iotdb-client/session/src/main/i18n/zh/org/apache/iotdb/session/i18n/SessionMessages.java +++ b/iotdb-client/session/src/main/i18n/zh/org/apache/iotdb/session/i18n/SessionMessages.java @@ -295,4 +295,5 @@ public final class SessionMessages { public static final String EXCEPTION_PREPARED_NUMERIC_ROW_PREPARE_RETURNED_NO_STATUS_6FF7BC24 = "预备数值行准备请求未返回状态"; public static final String EXCEPTION_PREPARED_NUMERIC_ROW_VALUE_COUNT_MISMATCH_9ACD3A0C = "预备数值行的值数量不匹配"; public static final String EXCEPTION_UNSUPPORTED_NUMERIC_TYPE_06DD3044 = "不支持的数值类型"; + public static final String EXCEPTION_TABLE_SESSION_IS_CLOSED_77FE359B = "表会话已关闭。"; } diff --git a/iotdb-client/session/src/main/java/org/apache/iotdb/session/TableSession.java b/iotdb-client/session/src/main/java/org/apache/iotdb/session/TableSession.java index 9d3e6d8b9bb..c453596c1ee 100644 --- a/iotdb-client/session/src/main/java/org/apache/iotdb/session/TableSession.java +++ b/iotdb-client/session/src/main/java/org/apache/iotdb/session/TableSession.java @@ -67,6 +67,35 @@ public class TableSession implements ITableSession { List<TSDataType> dataTypes, List<ColumnCategory> columnCategories) throws StatementExecutionException, IoTDBConnectionException { + // Validate before regrouping, which would otherwise discard unsupported categories or + // surplus metadata entries. + if (tableName == null + || columnNames == null + || dataTypes == null + || columnCategories == null + || columnNames.size() != dataTypes.size() + || columnNames.size() != columnCategories.size()) { + throw new IllegalArgumentException( + SessionMessages + .EXCEPTION_NUMERIC_ROW_ARGUMENTS_MUST_BE_NON_NULL_COLUMN_METADATA_LENGTHS_AND_PER_CATEGORY_VALUE_COUNTS_MUST_MATCH_EB12B233); + } + for (int i = 0; i < columnNames.size(); i++) { + if (columnNames.get(i) == null + || dataTypes.get(i) == null + || columnCategories.get(i) == null) { + throw new IllegalArgumentException( + SessionMessages + .EXCEPTION_NUMERIC_ROW_ARGUMENTS_MUST_BE_NON_NULL_COLUMN_METADATA_LENGTHS_AND_PER_CATEGORY_VALUE_COUNTS_MUST_MATCH_EB12B233); + } + if ((columnCategories.get(i) != ColumnCategory.TAG + && columnCategories.get(i) != ColumnCategory.FIELD) + || (columnCategories.get(i) == ColumnCategory.TAG + && dataTypes.get(i) != TSDataType.STRING)) { + throw new IllegalArgumentException( + SessionMessages + .EXCEPTION_NUMERIC_ROWS_SUPPORT_NUMERIC_FIELD_COLUMNS_AND_STRING_TAG_COLUMNS_ONLY_87159FFA); + } + } List<String> names = new ArrayList<>(); List<TSDataType> types = new ArrayList<>(); List<ColumnCategory> categories = new ArrayList<>(); diff --git a/iotdb-client/session/src/main/java/org/apache/iotdb/session/pool/TableSessionWrapper.java b/iotdb-client/session/src/main/java/org/apache/iotdb/session/pool/TableSessionWrapper.java index 7c950949d0d..7684beba9ab 100644 --- a/iotdb-client/session/src/main/java/org/apache/iotdb/session/pool/TableSessionWrapper.java +++ b/iotdb-client/session/src/main/java/org/apache/iotdb/session/pool/TableSessionWrapper.java @@ -80,6 +80,7 @@ public class TableSessionWrapper implements ITableSession { @Override public void insert(NumericRow row) throws StatementExecutionException, IoTDBConnectionException { + checkOpen(); try { session.insertRelationalRecord(row); } catch (IoTDBConnectionException e) { @@ -97,8 +98,14 @@ public class TableSessionWrapper implements ITableSession { List<TSDataType> dataTypes, List<ColumnCategory> columnCategories) throws StatementExecutionException, IoTDBConnectionException { - return tableSession.prepareInsertNumericRow( - tableName, columnNames, dataTypes, columnCategories); + checkOpen(); + try { + return tableSession.prepareInsertNumericRow( + tableName, columnNames, dataTypes, columnCategories); + } catch (IoTDBConnectionException e) { + invalidateSession(); + throw e; + } } @Override @@ -116,17 +123,32 @@ public class TableSessionWrapper implements ITableSession { String[] tagValues, BitMap nullBitmap) throws StatementExecutionException, IoTDBConnectionException { + checkOpen(); try { tableSession.executePreparedInsertNumericRow( prepared, timestamp, values, tagValues, nullBitmap); } catch (IoTDBConnectionException e) { - sessionPool.cleanSessionAndMayThrowConnectionException(session); - closed.set(true); - session = null; + invalidateSession(); throw e; } } + private void checkOpen() throws IoTDBConnectionException { + // The cached delegate still references the physical session after it returns to the pool. + if (closed.get() || session == null) { + throw new IoTDBConnectionException( + SessionMessages.EXCEPTION_TABLE_SESSION_IS_CLOSED_77FE359B); + } + } + + private void invalidateSession() { + Session failedSession = session; + // Invalidate before cleanup so close() cannot return a failed connection to the pool. + closed.set(true); + session = null; + sessionPool.cleanSessionAndMayThrowConnectionException(failedSession); + } + @Override public SessionDataSet executeQueryStatement(String sql) throws StatementExecutionException, IoTDBConnectionException { @@ -168,6 +190,9 @@ public class TableSessionWrapper implements ITableSession { @Override public void close() throws IoTDBConnectionException { + if (closed.get() || session == null) { + return; + } if (!Objects.equals(session.getSqlDialect(), sessionPool.sqlDialect)) { try { session.executeNonQueryStatement("set sql_dialect=" + sessionPool.sqlDialect); diff --git a/iotdb-client/session/src/test/java/org/apache/iotdb/session/PreparedNumericRowMetadataTest.java b/iotdb-client/session/src/test/java/org/apache/iotdb/session/PreparedNumericRowMetadataTest.java new file mode 100644 index 00000000000..b47b71a961d --- /dev/null +++ b/iotdb-client/session/src/test/java/org/apache/iotdb/session/PreparedNumericRowMetadataTest.java @@ -0,0 +1,218 @@ +/* + * 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.session; + +import org.apache.iotdb.isession.NumericRowPreparedStatement; +import org.apache.iotdb.service.rpc.thrift.TSPrepareNumericRowReq; +import org.apache.iotdb.session.i18n.SessionMessages; + +import org.apache.tsfile.enums.ColumnCategory; +import org.apache.tsfile.enums.TSDataType; +import org.apache.tsfile.utils.ReadWriteIOUtils; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +public class PreparedNumericRowMetadataTest { + private static final String INVALID_ARGUMENTS = + SessionMessages + .EXCEPTION_NUMERIC_ROW_ARGUMENTS_MUST_BE_NON_NULL_COLUMN_METADATA_LENGTHS_AND_PER_CATEGORY_VALUE_COUNTS_MUST_MATCH_EB12B233; + private static final String INVALID_CATEGORIES = + SessionMessages + .EXCEPTION_NUMERIC_ROWS_SUPPORT_NUMERIC_FIELD_COLUMNS_AND_STRING_TAG_COLUMNS_ONLY_87159FFA; + + @Test + public void testNullMetadataRejectedBeforeRpc() throws Exception { + assertInvalid( + null, + List.of("v"), + List.of(TSDataType.DOUBLE), + List.of(ColumnCategory.FIELD), + INVALID_ARGUMENTS); + assertInvalid( + "t", null, List.of(TSDataType.DOUBLE), List.of(ColumnCategory.FIELD), INVALID_ARGUMENTS); + assertInvalid("t", List.of("v"), null, List.of(ColumnCategory.FIELD), INVALID_ARGUMENTS); + assertInvalid("t", List.of("v"), List.of(TSDataType.DOUBLE), null, INVALID_ARGUMENTS); + } + + @Test + public void testNullMetadataElementsRejectedBeforeRpc() throws Exception { + assertInvalid( + "t", + Arrays.asList("tag", null), + List.of(TSDataType.STRING, TSDataType.DOUBLE), + List.of(ColumnCategory.TAG, ColumnCategory.FIELD), + INVALID_ARGUMENTS); + assertInvalid( + "t", + List.of("tag", "v"), + Arrays.asList(TSDataType.STRING, null), + List.of(ColumnCategory.TAG, ColumnCategory.FIELD), + INVALID_ARGUMENTS); + assertInvalid( + "t", + List.of("tag", "v"), + List.of(TSDataType.STRING, TSDataType.DOUBLE), + Arrays.asList(ColumnCategory.TAG, null), + INVALID_ARGUMENTS); + } + + @Test + public void testMetadataLengthMismatchRejectedBeforeRpc() throws Exception { + final List<String> names = List.of("a", "b"); + final List<TSDataType> types = List.of(TSDataType.DOUBLE, TSDataType.DOUBLE); + final List<ColumnCategory> categories = List.of(ColumnCategory.FIELD, ColumnCategory.FIELD); + assertInvalid("t", names.subList(0, 1), types, categories, INVALID_ARGUMENTS); + assertInvalid("t", names, types.subList(0, 1), categories, INVALID_ARGUMENTS); + assertInvalid("t", names, types, categories.subList(0, 1), INVALID_ARGUMENTS); + assertInvalid( + "t", + names, + List.of(TSDataType.DOUBLE, TSDataType.DOUBLE, TSDataType.DOUBLE), + categories, + INVALID_ARGUMENTS); + assertInvalid( + "t", + names, + types, + List.of(ColumnCategory.FIELD, ColumnCategory.FIELD, ColumnCategory.FIELD), + INVALID_ARGUMENTS); + } + + @Test + public void testAttributeColumnRejectedBeforeRpc() throws Exception { + assertInvalid( + "t", + List.of("tag", "attr", "v"), + List.of(TSDataType.STRING, TSDataType.STRING, TSDataType.DOUBLE), + List.of(ColumnCategory.TAG, ColumnCategory.ATTRIBUTE, ColumnCategory.FIELD), + INVALID_CATEGORIES); + } + + @Test + public void testNonStringTagRejectedBeforeRpc() throws Exception { + for (TSDataType type : List.of(TSDataType.INT64, TSDataType.TEXT)) { + assertInvalid( + "t", + List.of("tag", "v"), + List.of(type, TSDataType.DOUBLE), + List.of(ColumnCategory.TAG, ColumnCategory.FIELD), + INVALID_CATEGORIES); + } + } + + @Test + public void testMixedColumnOrderPreservesMetadataAndValues() throws Exception { + final Session session = mock(Session.class); + final SessionConnection connection = mock(SessionConnection.class); + when(session.getDefaultSessionConnection()).thenReturn(connection); + when(session.getDatabase()).thenReturn("db"); + when(connection.getSessionId()).thenReturn(10L); + when(connection.prepareNumericRow(any())).thenReturn(42L); + final TableSession tableSession = new TableSession(session); + final List<String> names = List.of("v1", "tag1", "v2", "tag2"); + final List<TSDataType> types = + List.of(TSDataType.DOUBLE, TSDataType.STRING, TSDataType.INT32, TSDataType.STRING); + final List<ColumnCategory> categories = + List.of(ColumnCategory.FIELD, ColumnCategory.TAG, ColumnCategory.FIELD, ColumnCategory.TAG); + final NumericRowPreparedStatement prepared = + tableSession.prepareInsertNumericRow("t", names, types, categories); + final ArgumentCaptor<TSPrepareNumericRowReq> request = + ArgumentCaptor.forClass(TSPrepareNumericRowReq.class); + verify(connection).prepareNumericRow(request.capture()); + assertMetadata(prepared, request.getValue()); + doAnswer( + invocation -> { + final ByteBuffer values = invocation.getArgument(2); + final ByteBuffer tags = invocation.getArgument(3); + assertEquals("a", ReadWriteIOUtils.readString(tags)); + assertEquals("b", ReadWriteIOUtils.readString(tags)); + assertFalse(tags.hasRemaining()); + assertEquals(12.5, values.getDouble(), 0.0); + assertEquals(7, values.getInt()); + assertFalse(values.hasRemaining()); + return null; + }) + .when(connection) + .executePreparedNumericRow(eq(42L), eq(123L), any(ByteBuffer.class), any(ByteBuffer.class)); + tableSession.executePreparedInsertNumericRow( + prepared, 123L, new long[] {Double.doubleToRawLongBits(12.5), 7}, new String[] {"a", "b"}); + verify(connection) + .executePreparedNumericRow(eq(42L), eq(123L), any(ByteBuffer.class), any(ByteBuffer.class)); + assertEquals(List.of("v1", "tag1", "v2", "tag2"), names); + assertEquals( + List.of(TSDataType.DOUBLE, TSDataType.STRING, TSDataType.INT32, TSDataType.STRING), types); + assertEquals( + List.of(ColumnCategory.FIELD, ColumnCategory.TAG, ColumnCategory.FIELD, ColumnCategory.TAG), + categories); + } + + private static void assertMetadata( + NumericRowPreparedStatement prepared, TSPrepareNumericRowReq request) { + assertEquals("t", prepared.getTableName()); + assertEquals("db", prepared.getDatabase()); + assertEquals(2, prepared.getTagCount()); + assertEquals(List.of("tag1", "tag2", "v1", "v2"), prepared.getColumnNames()); + assertEquals( + List.of(TSDataType.STRING, TSDataType.STRING, TSDataType.DOUBLE, TSDataType.INT32), + prepared.getDataTypes()); + assertEquals( + List.of(ColumnCategory.TAG, ColumnCategory.TAG, ColumnCategory.FIELD, ColumnCategory.FIELD), + prepared.getColumnCategories()); + assertEquals("t", request.getTableName()); + assertEquals(prepared.getColumnNames(), request.getMeasurements()); + assertEquals( + prepared.getDataTypes().stream().map(TSDataType::ordinal).toList(), request.getDataTypes()); + assertEquals( + prepared.getColumnCategories().stream().map(c -> (byte) c.ordinal()).toList(), + request.getColumnCategories()); + } + + private static void assertInvalid( + String table, + List<String> names, + List<TSDataType> types, + List<ColumnCategory> categories, + String message) + throws Exception { + final Session session = mock(Session.class); + try { + new TableSession(session).prepareInsertNumericRow(table, names, types, categories); + fail("Invalid metadata must be rejected before preparing a statement"); + } catch (IllegalArgumentException e) { + assertEquals(message, e.getMessage()); + } + verifyNoMoreInteractions(session); + } +} diff --git a/iotdb-client/session/src/test/java/org/apache/iotdb/session/pool/PreparedNumericRowPoolTest.java b/iotdb-client/session/src/test/java/org/apache/iotdb/session/pool/PreparedNumericRowPoolTest.java new file mode 100644 index 00000000000..f65f761c7b9 --- /dev/null +++ b/iotdb-client/session/src/test/java/org/apache/iotdb/session/pool/PreparedNumericRowPoolTest.java @@ -0,0 +1,222 @@ +/* + * 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.session.pool; + +import org.apache.iotdb.common.rpc.thrift.TSStatus; +import org.apache.iotdb.isession.NumericRow; +import org.apache.iotdb.isession.NumericRowPreparedStatement; +import org.apache.iotdb.rpc.IoTDBConnectionException; +import org.apache.iotdb.rpc.StatementExecutionException; +import org.apache.iotdb.rpc.TSStatusCode; +import org.apache.iotdb.service.rpc.thrift.IClientRPCService; +import org.apache.iotdb.service.rpc.thrift.TSExecutePreparedNumericRowReq; +import org.apache.iotdb.service.rpc.thrift.TSPrepareNumericRowResp; +import org.apache.iotdb.session.Session; +import org.apache.iotdb.session.SessionConnection; +import org.apache.iotdb.session.i18n.SessionMessages; + +import org.apache.thrift.TException; +import org.apache.tsfile.enums.ColumnCategory; +import org.apache.tsfile.enums.TSDataType; +import org.apache.tsfile.utils.BitMap; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.powermock.reflect.Whitebox; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +public class PreparedNumericRowPoolTest { + private Session session; + private SessionPool pool; + private TableSessionWrapper wrapper; + private IClientRPCService.Iface client; + + @Before + public void setUp() throws Exception { + session = new Session.Builder().enableRedirection(false).enableAutoFetch(false).build(); + client = mock(IClientRPCService.Iface.class); + SessionConnection connection = new SessionConnection("table"); + Whitebox.setInternalState(connection, "client", client); + Whitebox.setInternalState(connection, "sessionId", 11L); + Whitebox.setInternalState(session, "defaultSessionConnection", connection); + pool = mock(SessionPool.class); + pool.sqlDialect = session.getSqlDialect(); + wrapper = new TableSessionWrapper(session, pool); + when(client.prepareNumericRow(any())) + .thenReturn(new TSPrepareNumericRowResp(success()).setPreparedId(101)) + .thenReturn(new TSPrepareNumericRowResp(success()).setPreparedId(102)); + when(client.executePreparedNumericRow(any())).thenReturn(success()); + when(client.closePreparedNumericRow(anyLong(), anyLong())).thenReturn(success()); + } + + @Test + public void testClosedWrapperCannotUseReborrowedSession() throws Exception { + NumericRowPreparedStatement oldPrepared = prepare(wrapper); + wrapper.close(); + // A new borrower owns the same physical connection; the previous wrapper must stay closed. + try (TableSessionWrapper next = new TableSessionWrapper(session, pool); + NumericRowPreparedStatement nextPrepared = prepare(next)) { + clearInvocations(client); + IoTDBConnectionException error = + assertThrows(IoTDBConnectionException.class, () -> prepare(wrapper)); + assertEquals(SessionMessages.EXCEPTION_TABLE_SESSION_IS_CLOSED_77FE359B, error.getMessage()); + assertThrows( + IoTDBConnectionException.class, + () -> + wrapper.executePreparedInsertNumericRow( + oldPrepared, 1L, new long[] {42}, new String[] {"a"})); + assertThrows( + IoTDBConnectionException.class, + () -> + wrapper.executePreparedInsertNumericRow( + oldPrepared, 1L, new long[] {42}, new String[] {"a"}, new BitMap(1))); + verifyZeroInteractions(client); + verify(pool, never()).cleanSessionAndMayThrowConnectionException(any()); + + next.executePreparedInsertNumericRow(nextPrepared, 2L, new long[] {43}, new String[] {"a"}); + ArgumentCaptor<TSExecutePreparedNumericRowReq> request = + ArgumentCaptor.forClass(TSExecutePreparedNumericRowReq.class); + verify(client).executePreparedNumericRow(request.capture()); + assertEquals(102L, request.getValue().getPreparedId()); + } + wrapper.close(); + verify(pool, times(2)).putBack(session); + } + + @Test + public void testClosedWrapperCannotInsertNumericRow() throws Exception { + wrapper.close(); + + IoTDBConnectionException error = + assertThrows( + IoTDBConnectionException.class, + () -> + wrapper.insert( + new NumericRow( + "t", + 1L, + List.of("tag", "v"), + List.of(TSDataType.STRING, TSDataType.INT64), + List.of(ColumnCategory.TAG, ColumnCategory.FIELD), + new long[] {42L}, + new String[] {"a"}))); + + assertEquals(SessionMessages.EXCEPTION_TABLE_SESSION_IS_CLOSED_77FE359B, error.getMessage()); + verifyZeroInteractions(client); + } + + @Test + public void testPrepareTransportFailureDiscardsSession() throws Exception { + TException transportFailure = new TException("transport closed"); + when(client.prepareNumericRow(any())).thenThrow(transportFailure); + IoTDBConnectionException error = + assertThrows( + IoTDBConnectionException.class, + () -> { + try (TableSessionWrapper borrowed = wrapper) { + prepare(borrowed); + } + }); + assertSame(transportFailure, error.getCause()); + // Automatic close must neither mask the transport failure nor return the broken session. + assertEquals(0, error.getSuppressed().length); + assertThrows(IoTDBConnectionException.class, () -> prepare(wrapper)); + wrapper.close(); + verify(pool).cleanSessionAndMayThrowConnectionException(session); + verify(pool, never()).putBack(any()); + verify(client).prepareNumericRow(any()); + } + + @Test + public void testExecuteTransportFailureDiscardsSession() throws Exception { + NumericRowPreparedStatement prepared = prepare(wrapper); + TException transportFailure = new TException("transport closed"); + when(client.executePreparedNumericRow(any())).thenThrow(transportFailure); + IoTDBConnectionException error = + assertThrows( + IoTDBConnectionException.class, + () -> { + try (TableSessionWrapper borrowed = wrapper) { + borrowed.executePreparedInsertNumericRow( + prepared, 1L, new long[] {42}, new String[] {"a"}); + } + }); + assertSame(transportFailure, error.getCause()); + assertEquals(0, error.getSuppressed().length); + assertThrows( + IoTDBConnectionException.class, + () -> + wrapper.executePreparedInsertNumericRow( + prepared, 2L, new long[] {43}, new String[] {"a"})); + verify(pool).cleanSessionAndMayThrowConnectionException(session); + verify(pool, never()).putBack(any()); + verify(client).executePreparedNumericRow(any()); + } + + @Test + public void testPrepareStatementErrorPreservesHealthySession() throws Exception { + when(client.prepareNumericRow(any())) + .thenReturn( + new TSPrepareNumericRowResp( + new TSStatus(TSStatusCode.EXECUTE_STATEMENT_ERROR.getStatusCode()) + .setMessage("invalid prepared metadata"))); + try (TableSessionWrapper borrowed = wrapper) { + assertThrows(StatementExecutionException.class, () -> prepare(borrowed)); + } + verify(pool, never()).cleanSessionAndMayThrowConnectionException(any()); + verify(pool).putBack(session); + } + + @Test + public void testCloseReturnsSessionOnlyOnce() throws Exception { + wrapper.close(); + wrapper.close(); + verify(pool).putBack(session); + verify(pool, never()).cleanSessionAndMayThrowConnectionException(any()); + verifyZeroInteractions(client); + } + + private static NumericRowPreparedStatement prepare(TableSessionWrapper borrowed) + throws Exception { + return borrowed.prepareInsertNumericRow( + "t", + List.of("tag", "v"), + List.of(TSDataType.STRING, TSDataType.INT64), + List.of(ColumnCategory.TAG, ColumnCategory.FIELD)); + } + + private static TSStatus success() { + return new TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode()); + } +} diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/transform/statement/PipeConvertedInsertRowStatement.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/transform/statement/PipeConvertedInsertRowStatement.java index 99edef690b5..e83d2afc53b 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/transform/statement/PipeConvertedInsertRowStatement.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/transform/statement/PipeConvertedInsertRowStatement.java @@ -26,6 +26,8 @@ import org.apache.iotdb.db.conf.IoTDBDescriptor; import org.apache.iotdb.db.exception.metadata.PathNotExistException; import org.apache.iotdb.db.i18n.DataNodePipeMessages; import org.apache.iotdb.db.pipe.receiver.transform.converter.ValueConverter; +import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertBaseStatement; +import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertNumericRowStatement; import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertRowStatement; import org.apache.tsfile.annotations.TableModel; @@ -45,6 +47,31 @@ public class PipeConvertedInsertRowStatement extends InsertRowStatement { LoggerFactory.getLogger(PipeConvertedInsertRowStatement.class); public PipeConvertedInsertRowStatement(final InsertRowStatement insertRowStatement) { + this((InsertBaseStatement) insertRowStatement); + time = insertRowStatement.getTime(); + values = insertRowStatement.getValues(); + isNeedInferType = insertRowStatement.isNeedInferType(); + deviceID = insertRowStatement.getRawTableDeviceID(); + removeAllFailedMeasurementMarks(); + } + + public PipeConvertedInsertRowStatement(final InsertNumericRowStatement insertRowStatement) { + this((InsertBaseStatement) insertRowStatement); + time = insertRowStatement.getTime(); + // Box only at the Pipe conversion boundary: the receiver may require non-numeric FIELDs. + values = new Object[insertRowStatement.getValueCount()]; + for (int i = 0; i < values.length; i++) { + values[i] = insertRowStatement.getFirstValueOfIndex(i); + } + isNeedInferType = false; + // Schema analysis and failed-column recovery must not mutate the original numeric row. + columnCategories = columnCategories == null ? null : columnCategories.clone(); + tagColumnIndices = null; + attrColumnIndices = null; + removeAllFailedMeasurementMarks(); + } + + private PipeConvertedInsertRowStatement(final InsertBaseStatement insertRowStatement) { super(); // Statement isDebug = insertRowStatement.isDebug(); @@ -59,12 +86,6 @@ public class PipeConvertedInsertRowStatement extends InsertRowStatement { attrColumnIndices = insertRowStatement.getAttrColumnIndices(); writeToTable = insertRowStatement.isWriteToTable(); databaseName = insertRowStatement.getDatabaseName().orElse(null); - // InsertRowStatement - time = insertRowStatement.getTime(); - values = insertRowStatement.getValues(); - isNeedInferType = insertRowStatement.isNeedInferType(); - deviceID = insertRowStatement.getRawTableDeviceID(); - // To ensure that the measurement remains unchanged during the WAL writing process, the array // needs to be copied before the failed Measurement mark can be deleted. final MeasurementSchema[] measurementSchemas = insertRowStatement.getMeasurementSchemas(); @@ -87,8 +108,6 @@ public class PipeConvertedInsertRowStatement extends InsertRowStatement { if (failedMeasurementIndex2Info != null) { this.failedMeasurementIndex2Info = new HashMap<>(failedMeasurementIndex2Info); } - - removeAllFailedMeasurementMarks(); } @Override diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/visitor/PipeTableStatementDataTypeConvertExecutionVisitor.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/visitor/PipeTableStatementDataTypeConvertExecutionVisitor.java index 5a80da6f6e6..d76428d4813 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/visitor/PipeTableStatementDataTypeConvertExecutionVisitor.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/visitor/PipeTableStatementDataTypeConvertExecutionVisitor.java @@ -34,6 +34,7 @@ import org.apache.iotdb.db.queryengine.plan.statement.Statement; import org.apache.iotdb.db.queryengine.plan.statement.StatementNode; import org.apache.iotdb.db.queryengine.plan.statement.StatementVisitor; import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertMultiTabletsStatement; +import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertNumericRowStatement; import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertRowStatement; import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertRowsOfOneDeviceStatement; import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertRowsStatement; @@ -221,6 +222,15 @@ public class PipeTableStatementDataTypeConvertExecutionVisitor new PipeConvertedInsertRowStatement(insertRowStatement), statusAndDatabaseName.getRight()); } + @Override + public Optional<TSStatus> visitInsertNumericRow( + final InsertNumericRowStatement insertNumericRowStatement, + final Pair<TSStatus, String> statusAndDatabaseName) { + return tryExecute( + new PipeConvertedInsertRowStatement(insertNumericRowStatement), + statusAndDatabaseName.getRight()); + } + @Override public Optional<TSStatus> visitInsertRows( final InsertRowsStatement insertRowsStatement, 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 7435e901d2f..d96df20b784 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 @@ -2614,6 +2614,7 @@ public class ClientRPCServiceImpl implements IClientRPCServiceWithHandler { IClientSession session = SESSION_MANAGER.getCurrSessionAndUpdateIdleTime(); if (!SESSION_MANAGER.checkLogin(session)) return getNotLoggedInStatus(); InsertNumericRowStatement statement = null; + OperationQuota quota = null; try { if (req.getSessionId() != session.getId()) return RpcUtils.getStatus(TSStatusCode.NOT_LOGIN); statement = @@ -2624,6 +2625,8 @@ public class ClientRPCServiceImpl implements IClientRPCServiceWithHandler { ByteBuffer.wrap(req.getTagValues()), ByteBuffer.wrap(req.getValues()), req.bufferForNullBitmap()); + quota = + DataNodeThrottleQuotaManager.getInstance().checkQuota(session.getUsername(), statement); long queryId = SESSION_MANAGER.requestQueryId(); return COORDINATOR.executeForTableModel( statement, @@ -2635,6 +2638,8 @@ public class ClientRPCServiceImpl implements IClientRPCServiceWithHandler { metadata, config.getConnectionTimeoutInMS()) .status; + } catch (IoTDBException e) { + return onIoTDBException(e, OperationType.INSERT_RECORD, e.getErrorCode()); } catch (Exception e) { return onQueryException( e, OperationType.INSERT_RECORD.getName(), TSStatusCode.EXECUTE_STATEMENT_ERROR); @@ -2643,6 +2648,9 @@ public class ClientRPCServiceImpl implements IClientRPCServiceWithHandler { // Remove only this session's invalid handle, releasing its reservation exactly once. session.removePreparedNumericRow(req.getPreparedId()); } + if (quota != null) { + quota.close(); + } } } diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/receiver/transform/statement/PipeConvertedInsertRowStatementTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/receiver/transform/statement/PipeConvertedInsertRowStatementTest.java index 8c563a84b1d..a58a05af70a 100644 --- a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/receiver/transform/statement/PipeConvertedInsertRowStatementTest.java +++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/receiver/transform/statement/PipeConvertedInsertRowStatementTest.java @@ -19,15 +19,26 @@ package org.apache.iotdb.db.pipe.receiver.transform.statement; +import org.apache.iotdb.common.rpc.thrift.TSStatus; import org.apache.iotdb.commons.path.PartialPath; +import org.apache.iotdb.commons.schema.table.column.TsTableColumnCategory; +import org.apache.iotdb.db.pipe.receiver.visitor.PipeTableStatementDataTypeConvertExecutionVisitor; +import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertNumericRowStatement; import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertRowStatement; +import org.apache.iotdb.db.utils.NumericRowValues; +import org.apache.iotdb.rpc.TSStatusCode; +import org.apache.tsfile.common.conf.TSFileConfig; import org.apache.tsfile.enums.TSDataType; +import org.apache.tsfile.utils.Binary; +import org.apache.tsfile.utils.Pair; import org.apache.tsfile.write.schema.MeasurementSchema; import org.junit.Assert; import org.junit.Test; import java.time.ZoneId; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; public class PipeConvertedInsertRowStatementTest { @@ -52,4 +63,139 @@ public class PipeConvertedInsertRowStatementTest { Assert.assertNull(convertedStatement.getValues()[0]); Assert.assertFalse(convertedStatement.isNeedInferType()); } + + @Test + public void testNumericRowDispatchConvertsNumericAndStringFields() throws Exception { + final InsertNumericRowStatement original = numericRow(); + final long[] originalValues = original.getValues().clone(); + final AtomicInteger executions = new AtomicInteger(); + final TSStatus success = new TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode()); + final PipeTableStatementDataTypeConvertExecutionVisitor visitor = + new PipeTableStatementDataTypeConvertExecutionVisitor( + (statement, database) -> { + executions.incrementAndGet(); + Assert.assertEquals("db", database); + Assert.assertTrue(statement instanceof PipeConvertedInsertRowStatement); + final PipeConvertedInsertRowStatement converted = + (PipeConvertedInsertRowStatement) statement; + Assert.assertTrue(converted.isForceTypeConversion()); + Assert.assertFalse(converted.isNeedInferType()); + Assert.assertTrue(converted.isAligned()); + Assert.assertTrue(converted.isWriteToTable()); + Assert.assertEquals(original.getTime(), converted.getTime()); + Assert.assertEquals(original.getDatabaseName(), converted.getDatabaseName()); + Assert.assertEquals(original.getTableDeviceID(), converted.getTableDeviceID()); + Assert.assertEquals(original.getFirstValueOfIndex(0), converted.getValues()[0]); + Assert.assertTrue(converted.checkAndCastDataType(1, TSDataType.INT32)); + Assert.assertTrue(converted.checkAndCastDataType(2, TSDataType.STRING)); + Assert.assertTrue(converted.checkAndCastDataType(3, TSDataType.STRING)); + Assert.assertEquals(12, converted.getValues()[1]); + Assert.assertEquals( + new Binary("8.5", TSFileConfig.STRING_CHARSET), converted.getValues()[2]); + Assert.assertNull(converted.getValues()[3]); + Assert.assertArrayEquals( + new TSDataType[] { + TSDataType.STRING, TSDataType.INT32, TSDataType.STRING, TSDataType.STRING + }, + converted.getDataTypes()); + return success; + }); + + Assert.assertEquals(success, visitor.process(original, new Pair<>(success, "db")).orElse(null)); + Assert.assertEquals(1, executions.get()); + Assert.assertArrayEquals(originalValues, original.getValues()); + Assert.assertArrayEquals( + new TSDataType[] { + TSDataType.STRING, TSDataType.DOUBLE, TSDataType.DOUBLE, TSDataType.INT64 + }, + original.getDataTypes()); + Assert.assertTrue(original.getNumericValues().isNull(3)); + } + + @Test + public void testNumericRowRetryRestoresFailuresOnlyInCopy() throws Exception { + final InsertNumericRowStatement original = numericRow(); + original.markFailedMeasurement(0, new IllegalArgumentException("tag failure")); + original.markFailedMeasurement(1, new IllegalArgumentException("field failure")); + final long[] failedValues = original.getValues().clone(); + final PipeConvertedInsertRowStatement converted = new PipeConvertedInsertRowStatement(original); + + Assert.assertFalse(converted.hasFailedMeasurements()); + Assert.assertEquals(List.of(0), converted.getTagColumnIndices()); + Assert.assertEquals("tag", converted.getMeasurements()[0]); + Assert.assertEquals("count", converted.getMeasurements()[1]); + Assert.assertEquals(new Binary("d1", TSFileConfig.STRING_CHARSET), converted.getValues()[0]); + Assert.assertEquals(12.5, converted.getValues()[1]); + Assert.assertTrue(converted.checkAndCastDataType(1, TSDataType.INT32)); + Assert.assertEquals(12, converted.getValues()[1]); + + Assert.assertEquals(2, original.getFailedMeasurementNumber()); + Assert.assertNotSame( + original.getFailedMeasurementInfoMap(), converted.getFailedMeasurementInfoMap()); + Assert.assertNull(original.getMeasurements()[0]); + Assert.assertNull(original.getMeasurements()[1]); + Assert.assertNull(original.getDataTypes()[1]); + Assert.assertTrue(original.getNumericValues().isNull(0)); + Assert.assertTrue(original.getNumericValues().isNull(1)); + Assert.assertArrayEquals(failedValues, original.getValues()); + Assert.assertEquals(12.5, original.getFailedMeasurementInfoMap().get(1).getValue()); + } + + @Test + public void testNumericRowConversionOwnsMutableMetadataAndPreservesNulls() throws Exception { + final InsertNumericRowStatement original = numericRow(); + original.getNumericValues().setTag(0, null); + final PipeConvertedInsertRowStatement converted = new PipeConvertedInsertRowStatement(original); + Assert.assertNull(converted.getValues()[0]); + Assert.assertNull(converted.getValues()[3]); + Assert.assertEquals(original.getTableDeviceID(), converted.getTableDeviceID()); + Assert.assertNotSame(original.getMeasurements(), converted.getMeasurements()); + Assert.assertNotSame(original.getDataTypes(), converted.getDataTypes()); + Assert.assertNotSame(original.getMeasurementSchemas(), converted.getMeasurementSchemas()); + Assert.assertNotSame(original.getColumnCategories(), converted.getColumnCategories()); + converted.swapColumn(1, 2); + converted.setColumnCategory(TsTableColumnCategory.ATTRIBUTE, 1); + converted.markFailedMeasurement(2, new IllegalArgumentException("conversion failure")); + + Assert.assertArrayEquals( + new String[] {"tag", "count", "text", "nullable"}, original.getMeasurements()); + Assert.assertEquals(TsTableColumnCategory.FIELD, original.getColumnCategory(1)); + Assert.assertEquals("count", original.getMeasurementSchemas()[1].getMeasurementName()); + Assert.assertEquals(12.5, original.getFirstValueOfIndex(1)); + Assert.assertEquals(8.5, original.getFirstValueOfIndex(2)); + Assert.assertFalse(original.hasFailedMeasurements()); + } + + private static InsertNumericRowStatement numericRow() throws Exception { + final InsertNumericRowStatement statement = new InsertNumericRowStatement(); + statement.setDevicePath(new PartialPath("t")); + statement.setDatabaseName("db"); + statement.setTime(123L); + statement.setMeasurements(new String[] {"tag", "count", "text", "nullable"}); + statement.setDataTypes( + new TSDataType[] { + TSDataType.STRING, TSDataType.DOUBLE, TSDataType.DOUBLE, TSDataType.INT64 + }); + statement.setColumnCategories( + new TsTableColumnCategory[] { + TsTableColumnCategory.TAG, + TsTableColumnCategory.FIELD, + TsTableColumnCategory.FIELD, + TsTableColumnCategory.FIELD + }); + statement.setMeasurementSchemas( + new MeasurementSchema[] { + new MeasurementSchema("tag", TSDataType.STRING), + new MeasurementSchema("count", TSDataType.DOUBLE), + new MeasurementSchema("text", TSDataType.DOUBLE), + new MeasurementSchema("nullable", TSDataType.INT64) + }); + final NumericRowValues values = new NumericRowValues(1, 3); + values.setTag(0, "d1"); + values.setObject(1, TSDataType.DOUBLE, 12.5); + values.setObject(2, TSDataType.DOUBLE, 8.5); + values.setNull(3); + statement.setNumericValues(values); + return statement; + } } diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/protocol/thrift/impl/ClientRPCServiceImplPreparedNumericRowTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/protocol/thrift/impl/ClientRPCServiceImplPreparedNumericRowTest.java new file mode 100644 index 00000000000..917bcfa4d76 --- /dev/null +++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/protocol/thrift/impl/ClientRPCServiceImplPreparedNumericRowTest.java @@ -0,0 +1,195 @@ +/* + * 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.thrift.impl; + +import org.apache.iotdb.common.rpc.thrift.TSStatus; +import org.apache.iotdb.commons.exception.RpcThrottlingException; +import org.apache.iotdb.commons.exception.SemanticException; +import org.apache.iotdb.db.conf.IoTDBDescriptor; +import org.apache.iotdb.db.protocol.session.IClientSession; +import org.apache.iotdb.db.protocol.session.PreparedNumericRowInfo; +import org.apache.iotdb.db.protocol.session.SessionManager; +import org.apache.iotdb.db.queryengine.common.QueryId; +import org.apache.iotdb.db.queryengine.plan.Coordinator; +import org.apache.iotdb.db.queryengine.plan.execution.ExecutionResult; +import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertNumericRowStatement; +import org.apache.iotdb.db.storageengine.rescon.quotas.DataNodeThrottleQuotaManager; +import org.apache.iotdb.db.storageengine.rescon.quotas.OperationQuota; +import org.apache.iotdb.rpc.TSStatusCode; +import org.apache.iotdb.service.rpc.thrift.TSExecutePreparedNumericRowReq; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InOrder; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; + +import java.nio.ByteBuffer; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +@RunWith(PowerMockRunner.class) +@PowerMockIgnore({"com.sun.org.apache.xerces.*", "javax.xml.*", "org.xml.*", "javax.management.*"}) +@PrepareForTest({ + ClientRPCServiceImpl.class, + DataNodeThrottleQuotaManager.class, + PreparedNumericRowInfo.class +}) +public class ClientRPCServiceImplPreparedNumericRowTest { + private ClientRPCServiceImpl service; + private int originalDataNodeId; + private Coordinator coordinator; + private Coordinator originalCoordinator; + private SessionManager originalSessionManager; + private IClientSession session; + private PreparedNumericRowInfo prepared; + private InsertNumericRowStatement statement; + private DataNodeThrottleQuotaManager quotaManager; + private OperationQuota quota; + private TSExecutePreparedNumericRowReq request; + private TSStatus success; + + @Before + public void setUp() throws Exception { + // Coordinator initialization requires a DataNode ID even when query execution is mocked. + originalDataNodeId = IoTDBDescriptor.getInstance().getConfig().getDataNodeId(); + IoTDBDescriptor.getInstance().getConfig().setDataNodeId(0); + // Exercise the actual RPC method while isolating query execution and per-user quota state. + service = mock(ClientRPCServiceImpl.class, CALLS_REAL_METHODS); + originalCoordinator = Whitebox.getInternalState(ClientRPCServiceImpl.class, "COORDINATOR"); + originalSessionManager = + Whitebox.getInternalState(ClientRPCServiceImpl.class, "SESSION_MANAGER"); + coordinator = mock(Coordinator.class); + SessionManager sessions = mock(SessionManager.class); + Whitebox.setInternalState(ClientRPCServiceImpl.class, "COORDINATOR", coordinator); + Whitebox.setInternalState(ClientRPCServiceImpl.class, "SESSION_MANAGER", sessions); + session = mock(IClientSession.class); + prepared = PowerMockito.mock(PreparedNumericRowInfo.class); + statement = new InsertNumericRowStatement(); + quotaManager = mock(DataNodeThrottleQuotaManager.class); + quota = mock(OperationQuota.class); + PowerMockito.mockStatic(DataNodeThrottleQuotaManager.class); + when(DataNodeThrottleQuotaManager.getInstance()).thenReturn(quotaManager); + when(sessions.getCurrSessionAndUpdateIdleTime()).thenReturn(session); + when(sessions.checkLogin(session)).thenReturn(true); + when(session.getId()).thenReturn(11L); + when(session.getUsername()).thenReturn("quota-user"); + when(session.getPreparedNumericRow(101L)).thenReturn(prepared); + when(prepared.bind(anyLong(), any(), any(), any())).thenReturn(statement); + when(quotaManager.checkQuota("quota-user", statement)).thenReturn(quota); + success = new TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode()); + when(coordinator.executeForTableModel( + same(statement), any(), same(session), anyLong(), any(), eq(""), any(), anyLong())) + .thenReturn(new ExecutionResult(new QueryId("prepared_quota"), success)); + request = + new TSExecutePreparedNumericRowReq( + 11L, 101L, 1L, ByteBuffer.allocate(0), ByteBuffer.allocate(0)); + } + + @After + public void tearDown() { + IoTDBDescriptor.getInstance().getConfig().setDataNodeId(originalDataNodeId); + Whitebox.setInternalState(ClientRPCServiceImpl.class, "COORDINATOR", originalCoordinator); + Whitebox.setInternalState( + ClientRPCServiceImpl.class, "SESSION_MANAGER", originalSessionManager); + } + + @Test + public void testChecksQuotaBeforeExecutionAndClosesOnSuccess() throws Exception { + assertSame(success, service.executePreparedNumericRow(request)); + InOrder order = inOrder(quotaManager, coordinator, quota); + order.verify(quotaManager).checkQuota("quota-user", statement); + order + .verify(coordinator) + .executeForTableModel( + same(statement), any(), same(session), anyLong(), any(), eq(""), any(), anyLong()); + order.verify(quota).close(); + verify(session, never()).removePreparedNumericRow(anyLong()); + } + + @Test + public void testQuotaRejectionDoesNotExecuteOrInvalidatePreparedRow() throws Exception { + int code = TSStatusCode.NUM_WRITE_REQUESTS_EXCEEDED.getStatusCode(); + when(quotaManager.checkQuota("quota-user", statement)) + .thenThrow(new RpcThrottlingException("write request quota exceeded", code)); + TSStatus status = service.executePreparedNumericRow(request); + assertEquals(code, status.getCode()); + // Standard RPC handling adds an operation prefix while retaining the quota failure detail. + assertTrue(status.getMessage().endsWith("write request quota exceeded")); + verifyZeroInteractions(coordinator, quota); + verify(session, never()).removePreparedNumericRow(anyLong()); + } + + @Test + public void testExecutionExceptionClosesQuota() throws Exception { + when(coordinator.executeForTableModel( + same(statement), any(), same(session), anyLong(), any(), eq(""), any(), anyLong())) + .thenThrow(new IllegalStateException("executor failure")); + TSStatus status = service.executePreparedNumericRow(request); + assertNotEquals(TSStatusCode.SUCCESS_STATUS.getStatusCode(), status.getCode()); + verify(quota).close(); + verify(session, never()).removePreparedNumericRow(anyLong()); + } + + @Test + public void testSchemaFailureClosesQuotaAndRemovesPreparedRow() throws Exception { + TSStatus schemaFailure = new TSStatus(TSStatusCode.SEMANTIC_ERROR.getStatusCode()); + when(coordinator.executeForTableModel( + same(statement), any(), same(session), anyLong(), any(), eq(""), any(), anyLong())) + .thenAnswer( + invocation -> { + statement.markTableSchemaValidationFailed(); + return new ExecutionResult(new QueryId("prepared_quota"), schemaFailure); + }); + assertSame(schemaFailure, service.executePreparedNumericRow(request)); + verify(quotaManager).checkQuota("quota-user", statement); + verify(quota).close(); + verify(session).removePreparedNumericRow(101L); + } + + @Test + public void testInvalidPayloadDoesNotConsumeQuota() throws Exception { + when(prepared.bind(anyLong(), any(), any(), any())) + .thenThrow(new SemanticException("invalid numeric payload")); + TSStatus status = service.executePreparedNumericRow(request); + assertNotEquals(TSStatusCode.SUCCESS_STATUS.getStatusCode(), status.getCode()); + verifyZeroInteractions(quotaManager, coordinator, quota); + verify(session, never()).removePreparedNumericRow(anyLong()); + } +} diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/parser/NumericTableRowTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/parser/NumericTableRowTest.java index 70c39d90072..fbb69b4be04 100644 --- a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/parser/NumericTableRowTest.java +++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/parser/NumericTableRowTest.java @@ -20,10 +20,18 @@ package org.apache.iotdb.db.queryengine.plan.parser; import org.apache.iotdb.common.rpc.thrift.TEndPoint; import org.apache.iotdb.common.rpc.thrift.TSStatus; +import org.apache.iotdb.commons.consensus.index.impl.IoTProgressIndex; import org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNodeId; import org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNodeType; import org.apache.iotdb.commons.queryengine.plan.relational.metadata.ColumnSchema; +import org.apache.iotdb.commons.request.IConsensusRequest; import org.apache.iotdb.commons.schema.table.column.TsTableColumnCategory; +import org.apache.iotdb.consensus.common.request.BatchIndexedConsensusRequest; +import org.apache.iotdb.consensus.common.request.ByteBufferConsensusRequest; +import org.apache.iotdb.consensus.common.request.DeserializedBatchIndexedConsensusRequest; +import org.apache.iotdb.consensus.common.request.IndexedConsensusRequest; +import org.apache.iotdb.consensus.common.request.IoTConsensusRequest; +import org.apache.iotdb.db.consensus.statemachine.dataregion.IoTConsensusDataRegionStateMachine; import org.apache.iotdb.db.pipe.event.common.tablet.parser.TabletInsertionEventTablePatternParser; import org.apache.iotdb.db.pipe.sink.payload.evolvable.request.PipeTransferTabletBatchReqV2; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowNode; @@ -35,6 +43,7 @@ import org.apache.iotdb.db.queryengine.plan.statement.StatementType; import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertBaseStatement; import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertNumericRowStatement; import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertRowStatement; +import org.apache.iotdb.db.storageengine.dataregion.DataRegion; import org.apache.iotdb.db.storageengine.dataregion.memtable.AlignedWritableMemChunkGroup; import org.apache.iotdb.db.storageengine.dataregion.memtable.PrimitiveMemTable; import org.apache.iotdb.db.storageengine.dataregion.wal.buffer.WALEntry; @@ -299,6 +308,58 @@ public class NumericTableRowTest { } } + @Test + public void testIoTConsensusBatchFromMemoryAndWal() throws Exception { + // The real state machine must accept both live PlanNode bytes and WAL catch-up envelopes, + // attach the sender progress index, and dispatch each numeric row to DataRegion.insert. + RelationalInsertNumericRowNode original = nodeWithNullTag(); + original.markFailedMeasurement(2); + original.setSearchIndex(42); + original.setLastFragment(true); + WALInfoEntry entry = new WALInfoEntry(7, original); + ByteBuffer wal = ByteBuffer.allocate(entry.serializedSize()); + entry.serialize(new WALByteBufferForTest(wal)); + wal.flip(); + BatchIndexedConsensusRequest batch = new BatchIndexedConsensusRequest(3); + batch.add( + new IndexedConsensusRequest( + -1, + 41, + Collections.singletonList( + new ByteBufferConsensusRequest(original.serializeToByteBuffer())))); + batch.add( + new IndexedConsensusRequest( + -1, 42, Collections.singletonList(new IoTConsensusRequest(wal)))); + DataRegion region = Mockito.mock(DataRegion.class); + IoTConsensusDataRegionStateMachine machine = new IoTConsensusDataRegionStateMachine(region); + DeserializedBatchIndexedConsensusRequest decoded = + (DeserializedBatchIndexedConsensusRequest) machine.deserializeRequest(batch); + assertEquals(2, decoded.getInsertNodes().size()); + for (int i = 0; i < 2; i++) { + IConsensusRequest request = decoded.getInsertNodes().get(i); + assertTrue(request instanceof RelationalInsertNumericRowNode); + RelationalInsertNumericRowNode row = (RelationalInsertNumericRowNode) request; + assertEquals(original.getDeviceID(), row.getDeviceID()); + assertEquals(original.getTime(), row.getTime()); + assertEquals(original.getValidMeasurementNumber(false), row.getMeasurements().length); + assertEquals( + ((RelationalInsertNumericRowNode) decoded.getInsertNodes().get(0)).getNumericValues(), + row.getNumericValues()); + assertEquals(new IoTProgressIndex(3, 41L + i), row.getProgressIndex()); + assertObjectStorageAbsent(RelationalInsertNumericRowNode.class, row); + } + TSStatus status = machine.write(decoded); + assertEquals(2, status.getSubStatusSize()); + for (int i = 0; i < 2; i++) { + assertEquals( + TSStatusCode.SUCCESS_STATUS.getStatusCode(), status.getSubStatus().get(i).getCode()); + RelationalInsertNumericRowNode row = + (RelationalInsertNumericRowNode) decoded.getInsertNodes().get(i); + Mockito.verify(region).insert(Mockito.same(row)); + Mockito.verify(region).insertSeparatorToWAL(Mockito.same(row)); + } + } + @Test public void testPipeBatchRetainsIndependentNumericStatements() throws Exception { RelationalInsertNumericRowNode node = nodeWithNullTag();
