| /** |
| * 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. |
| */ |
| |
| import { SessionPool } from "../../src/client/SessionPool"; |
| import { TSDataType } from "../../src/utils/DataTypes"; |
| |
| describe("Multi-Node E2E Tests", () => { |
| const IOTDB_HOST = process.env.IOTDB_HOST || "localhost"; |
| const IOTDB_PORT_1 = parseInt(process.env.IOTDB_PORT || "6667", 10); |
| const IOTDB_PORT_2 = parseInt(process.env.IOTDB_PORT_2 || "6668", 10); |
| const IOTDB_PORT_3 = parseInt(process.env.IOTDB_PORT_3 || "6669", 10); |
| const IOTDB_USER = process.env.IOTDB_USER || "root"; |
| const IOTDB_PASSWORD = process.env.IOTDB_PASSWORD || "root"; |
| const IS_MULTI_NODE = process.env.MULTI_NODE === "true"; |
| |
| let pool1: SessionPool; |
| let pool2: SessionPool; |
| let pool3: SessionPool; |
| let isConnected = false; |
| |
| beforeAll(async () => { |
| // Skip multi-node tests if not in multi-node environment |
| if (!IS_MULTI_NODE) { |
| console.log( |
| "Skipping Multi-Node tests - not in multi-node environment (MULTI_NODE env var not set)", |
| ); |
| return; |
| } |
| |
| // For 3C3D setup, connect to all three DataNode ports |
| // This enables true multi-node load distribution and testing |
| console.log(`Connecting to IoTDB cluster:`); |
| console.log(` DataNode 1: ${IOTDB_HOST}:${IOTDB_PORT_1}`); |
| console.log(` DataNode 2: ${IOTDB_HOST}:${IOTDB_PORT_2}`); |
| console.log(` DataNode 3: ${IOTDB_HOST}:${IOTDB_PORT_3}`); |
| |
| try { |
| // Create three pools, each connected to a different DataNode |
| pool1 = new SessionPool(IOTDB_HOST, IOTDB_PORT_1, { |
| username: IOTDB_USER, |
| password: IOTDB_PASSWORD, |
| maxPoolSize: 5, |
| minPoolSize: 2, |
| }); |
| |
| pool2 = new SessionPool(IOTDB_HOST, IOTDB_PORT_2, { |
| username: IOTDB_USER, |
| password: IOTDB_PASSWORD, |
| maxPoolSize: 5, |
| minPoolSize: 2, |
| }); |
| |
| pool3 = new SessionPool(IOTDB_HOST, IOTDB_PORT_3, { |
| username: IOTDB_USER, |
| password: IOTDB_PASSWORD, |
| maxPoolSize: 5, |
| minPoolSize: 2, |
| }); |
| |
| await pool1.init(); |
| await pool2.init(); |
| await pool3.init(); |
| |
| isConnected = true; |
| console.log( |
| `Successfully connected to IoTDB cluster on ports ${IOTDB_PORT_1}, ${IOTDB_PORT_2}, ${IOTDB_PORT_3}`, |
| ); |
| } catch (error) { |
| console.warn( |
| "Could not connect to IoTDB. Multi-node E2E tests will be skipped.", |
| ); |
| console.warn("Error:", error); |
| } |
| }, 60000); |
| |
| afterAll(async () => { |
| if (!IS_MULTI_NODE) { |
| return; |
| } |
| if (isConnected) { |
| try { |
| await pool1.executeNonQueryStatement("DROP DATABASE root.test"); |
| } catch (error) { |
| // Ignore cleanup errors |
| } |
| |
| // Close pools in parallel with timeout protection |
| await Promise.allSettled([pool1.close(), pool2.close(), pool3.close()]); |
| } |
| }, 90000); // Increased timeout for multi-node cleanup |
| |
| test("Should initialize pools with connections to all three DataNodes", async () => { |
| if (!IS_MULTI_NODE || !isConnected) { |
| console.log( |
| "Skipping test - not in multi-node environment or no IoTDB connection", |
| ); |
| return; |
| } |
| |
| expect(pool1.getPoolSize()).toBeGreaterThanOrEqual(2); |
| expect(pool2.getPoolSize()).toBeGreaterThanOrEqual(2); |
| expect(pool3.getPoolSize()).toBeGreaterThanOrEqual(2); |
| console.log( |
| `Pools initialized: DataNode1=${pool1.getPoolSize()}, DataNode2=${pool2.getPoolSize()}, DataNode3=${pool3.getPoolSize()} connections`, |
| ); |
| }); |
| |
| test("Should create database and timeseries on first node", async () => { |
| if (!isConnected) { |
| console.log("Skipping test - no IoTDB connection"); |
| return; |
| } |
| |
| try { |
| await pool1.executeNonQueryStatement("CREATE DATABASE root.test"); |
| } catch (error: any) { |
| if (!error.message.includes("already exists")) { |
| throw error; |
| } |
| } |
| |
| await pool1.executeNonQueryStatement( |
| "CREATE TIMESERIES root.test.device1.temperature WITH DATATYPE=FLOAT", |
| ); |
| await pool1.executeNonQueryStatement( |
| "CREATE TIMESERIES root.test.device1.humidity WITH DATATYPE=FLOAT", |
| ); |
| }); |
| |
| test("Should handle concurrent load distributed across all three DataNodes", async () => { |
| if (!isConnected) { |
| console.log("Skipping test - no IoTDB connection"); |
| return; |
| } |
| |
| const operationsPerNode = 7; |
| const promises: Promise<any>[] = []; |
| |
| // Distribute operations across all three DataNode pools |
| for (let i = 0; i < operationsPerNode; i++) { |
| // Insert to DataNode 1 |
| promises.push( |
| pool1.insertTablet({ |
| deviceId: "root.test.device1", |
| measurements: ["temperature", "humidity"], |
| dataTypes: [TSDataType.FLOAT, TSDataType.FLOAT], |
| timestamps: [Date.now() + i * 1000], |
| values: [[20 + i * 0.1, 60 + i * 0.2]], |
| }), |
| ); |
| |
| // Query from DataNode 2 |
| promises.push( |
| pool2.executeQueryStatement("SELECT * FROM root.test.device1 LIMIT 10"), |
| ); |
| |
| // Insert to DataNode 3 |
| promises.push( |
| pool3.insertTablet({ |
| deviceId: "root.test.device1", |
| measurements: ["temperature", "humidity"], |
| dataTypes: [TSDataType.FLOAT, TSDataType.FLOAT], |
| timestamps: [Date.now() + i * 1000 + 500], |
| values: [[21 + i * 0.1, 61 + i * 0.2]], |
| }), |
| ); |
| } |
| |
| const results = await Promise.all(promises); |
| expect(results).toHaveLength(operationsPerNode * 3); |
| |
| console.log( |
| `Completed ${operationsPerNode * 3} concurrent operations across 3 DataNodes`, |
| ); |
| console.log( |
| `Pool stats: DN1=${pool1.getPoolSize()}/${pool1.getAvailableSize()}, DN2=${pool2.getPoolSize()}/${pool2.getAvailableSize()}, DN3=${pool3.getPoolSize()}/${pool3.getAvailableSize()}`, |
| ); |
| }); |
| |
| test("Should verify data replication across all DataNodes", async () => { |
| if (!isConnected) { |
| console.log("Skipping test - no IoTDB connection"); |
| return; |
| } |
| |
| // Insert data through DataNode 1 |
| await pool1.insertTablet({ |
| deviceId: "root.test.device1", |
| measurements: ["temperature", "humidity"], |
| dataTypes: [TSDataType.FLOAT, TSDataType.FLOAT], |
| timestamps: [Date.now()], |
| values: [[99.9, 99.9]], |
| }); |
| |
| // Wait a bit for replication |
| await new Promise((resolve) => setTimeout(resolve, 2000)); |
| |
| // Query from all three DataNodes - should see the same data |
| const result1 = await pool1.executeQueryStatement( |
| "SELECT COUNT(*) FROM root.test.device1", |
| ); |
| const result2 = await pool2.executeQueryStatement( |
| "SELECT COUNT(*) FROM root.test.device1", |
| ); |
| const result3 = await pool3.executeQueryStatement( |
| "SELECT COUNT(*) FROM root.test.device1", |
| ); |
| |
| expect(result1.rows.length).toBeGreaterThan(0); |
| expect(result2.rows.length).toBeGreaterThan(0); |
| expect(result3.rows.length).toBeGreaterThan(0); |
| |
| console.log( |
| `Data replicated across all DataNodes - verified queries from ports 6667, 6668, 6669`, |
| ); |
| }); |
| |
| test("Should handle large batch inserts across multiple DataNodes", async () => { |
| if (!isConnected) { |
| console.log("Skipping test - no IoTDB connection"); |
| return; |
| } |
| |
| const batchSize = 100; |
| const timestamps: number[] = []; |
| const values: number[][] = []; |
| const baseTime = Date.now(); |
| |
| for (let i = 0; i < batchSize; i++) { |
| timestamps.push(baseTime + i * 1000); |
| values.push([25 + Math.random() * 5, 65 + Math.random() * 10]); |
| } |
| |
| // Insert through different DataNodes |
| await pool1.insertTablet({ |
| deviceId: "root.test.device1", |
| measurements: ["temperature", "humidity"], |
| dataTypes: [TSDataType.FLOAT, TSDataType.FLOAT], |
| timestamps, |
| values, |
| }); |
| |
| // Verify data from different DataNode |
| const result = await pool2.executeQueryStatement( |
| "SELECT COUNT(*) FROM root.test.device1", |
| ); |
| |
| expect(result.rows.length).toBeGreaterThan(0); |
| console.log( |
| `Inserted ${batchSize} records via DataNode1, queried via DataNode2`, |
| ); |
| }); |
| |
| test("Should maintain all pool healths under stress", async () => { |
| if (!isConnected) { |
| console.log("Skipping test - no IoTDB connection"); |
| return; |
| } |
| |
| const initialSize1 = pool1.getPoolSize(); |
| const initialSize2 = pool2.getPoolSize(); |
| const initialSize3 = pool3.getPoolSize(); |
| |
| // Execute many operations across all DataNodes |
| const promises: Promise<any>[] = []; |
| for (let i = 0; i < 30; i++) { |
| promises.push(pool1.executeQueryStatement("SHOW DATABASES")); |
| promises.push(pool2.executeQueryStatement("SHOW DATABASES")); |
| promises.push(pool3.executeQueryStatement("SHOW DATABASES")); |
| } |
| |
| await Promise.all(promises); |
| |
| // Pools should maintain their sizes |
| expect(pool1.getPoolSize()).toBeGreaterThanOrEqual(initialSize1); |
| expect(pool2.getPoolSize()).toBeGreaterThanOrEqual(initialSize2); |
| expect(pool3.getPoolSize()).toBeGreaterThanOrEqual(initialSize3); |
| console.log("All three pool healths maintained after stress test"); |
| }); |
| |
| test("Should handle queries across all DataNodes simultaneously", async () => { |
| if (!isConnected) { |
| console.log("Skipping test - no IoTDB connection"); |
| return; |
| } |
| |
| // Execute queries simultaneously on all three DataNodes |
| const [result1, result2, result3] = await Promise.all([ |
| pool1.executeQueryStatement("SELECT * FROM root.test.device1 LIMIT 5"), |
| pool2.executeQueryStatement("SELECT * FROM root.test.device1 LIMIT 5"), |
| pool3.executeQueryStatement("SELECT * FROM root.test.device1 LIMIT 5"), |
| ]); |
| |
| expect(result1.rows.length).toBeGreaterThan(0); |
| expect(result2.rows.length).toBeGreaterThan(0); |
| expect(result3.rows.length).toBeGreaterThan(0); |
| console.log( |
| `Simultaneous queries across 3 DataNodes: DN1=${result1.rows.length}, DN2=${result2.rows.length}, DN3=${result3.rows.length} rows`, |
| ); |
| }); |
| |
| test("Should support nodeUrls configuration for multi-node setup", async () => { |
| if (!IS_MULTI_NODE) { |
| console.log("Skipping test - not in multi-node environment"); |
| return; |
| } |
| |
| // Create a pool using nodeUrls in string format (RECOMMENDED) |
| const nodeUrlsPool = new SessionPool({ |
| nodeUrls: [ |
| `${IOTDB_HOST}:${IOTDB_PORT_1}`, |
| `${IOTDB_HOST}:${IOTDB_PORT_2}`, |
| `${IOTDB_HOST}:${IOTDB_PORT_3}`, |
| ], |
| username: IOTDB_USER, |
| password: IOTDB_PASSWORD, |
| maxPoolSize: 6, |
| minPoolSize: 3, |
| }); |
| |
| try { |
| await nodeUrlsPool.init(); |
| expect(nodeUrlsPool.getPoolSize()).toBeGreaterThanOrEqual(3); |
| |
| // Execute a query to verify it works |
| const result = await nodeUrlsPool.executeQueryStatement("SHOW DATABASES"); |
| expect(result.rows).toBeDefined(); |
| |
| console.log("nodeUrls string format configuration working correctly"); |
| } finally { |
| await nodeUrlsPool.close(); |
| } |
| }); |
| |
| test("Should support nodeUrls configuration in object format", async () => { |
| if (!IS_MULTI_NODE) { |
| console.log("Skipping test - not in multi-node environment"); |
| return; |
| } |
| |
| // Create a pool using nodeUrls in object format (also supported) |
| const nodeUrlsPool = new SessionPool({ |
| nodeUrls: [ |
| { host: IOTDB_HOST, port: IOTDB_PORT_1 }, |
| { host: IOTDB_HOST, port: IOTDB_PORT_2 }, |
| { host: IOTDB_HOST, port: IOTDB_PORT_3 }, |
| ], |
| username: IOTDB_USER, |
| password: IOTDB_PASSWORD, |
| maxPoolSize: 6, |
| minPoolSize: 3, |
| }); |
| |
| try { |
| await nodeUrlsPool.init(); |
| expect(nodeUrlsPool.getPoolSize()).toBeGreaterThanOrEqual(3); |
| |
| // Execute a query to verify it works |
| const result = await nodeUrlsPool.executeQueryStatement("SHOW DATABASES"); |
| expect(result.rows).toBeDefined(); |
| |
| console.log("nodeUrls object format configuration working correctly"); |
| } finally { |
| await nodeUrlsPool.close(); |
| } |
| }); |
| }); |