diff --git a/.github/scripts/test-edge-windows.ps1 b/.github/scripts/test-edge-windows.ps1 new file mode 100644 index 0000000000000..7ca0f5be06e85 --- /dev/null +++ b/.github/scripts/test-edge-windows.ps1 @@ -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. +# + +$ErrorActionPreference = 'Stop' +$repositoryRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent +$testRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('iotdb-edge-windows-' + [guid]::NewGuid()) +New-Item -ItemType Directory -Path $testRoot | Out-Null +$javaStub = Join-Path $testRoot 'java.exe' +$script:caseCount = 0 +$portNames = @( + 'cn_internal_port', 'cn_consensus_port', 'dn_rpc_port', 'dn_internal_port', + 'dn_mpp_data_exchange_port', 'dn_schema_region_consensus_port', 'dn_data_region_consensus_port' +) + +function Get-FreePorts { + $result = @{} + $reservations = @() + try { + foreach ($name in $portNames) { + $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0) + $listener.Start() + $reservations += $listener + $result[$name] = $listener.LocalEndpoint.Port + } + return $result + } finally { + foreach ($listener in $reservations) { + $listener.Stop() + } + } +} + +function Invoke-EdgeLauncher { + param( + [System.Collections.IDictionary]$Ports, + [string]$Locale = '', + [string[]]$ExtraLines = @(), + [switch]$MissingConfig + ) + + $script:caseCount++ + $caseDir = Join-Path $testRoot "case-$script:caseCount" + $edgeHome = Join-Path $caseDir 'edge installation' + $configDir = Join-Path $caseDir 'custom configuration' + $launcherDir = Join-Path $edgeHome 'sbin/windows' + $envDir = Join-Path $configDir 'windows' + $javaHome = Join-Path $caseDir 'fake java' + $javaBin = Join-Path $javaHome 'bin' + New-Item -ItemType Directory -Path $launcherDir, $envDir, $javaBin -Force | Out-Null + foreach ($file in @('start-edge.bat', 'check-edge.ps1')) { + Copy-Item -LiteralPath (Join-Path $repositoryRoot "scripts/sbin/windows/$file") -Destination $launcherDir + } + Copy-Item -LiteralPath (Join-Path $repositoryRoot 'scripts/conf/windows/edge-env.bat') -Destination $envDir + $common = Get-Content -LiteralPath (Join-Path $repositoryRoot 'scripts/conf/windows/iotdb-common.bat') -Raw + Set-Content -LiteralPath (Join-Path $envDir 'iotdb-common.bat') -Value $common.Replace('@tsfile.locale.opt@', $Locale) -Encoding ASCII + if (-not $MissingConfig) { + $lines = @($ExtraLines) + foreach ($key in $Ports.Keys) { + $lines += " $key = $($Ports[$key]) " + } + Set-Content -LiteralPath (Join-Path $configDir 'iotdb-system.properties') -Value $lines -Encoding ASCII + } + + Copy-Item -LiteralPath $javaStub -Destination $javaBin + $argsFile = Join-Path $caseDir 'java-arguments.txt' + $startInfo = [System.Diagnostics.ProcessStartInfo]::new() + $startInfo.FileName = $env:ComSpec + $startInfo.Arguments = '/d /c call "' + (Join-Path $launcherDir 'start-edge.bat') + '"' + $startInfo.WorkingDirectory = $caseDir + $startInfo.UseShellExecute = $false + $startInfo.CreateNoWindow = $true + $startInfo.RedirectStandardOutput = $true + $startInfo.RedirectStandardError = $true + $startInfo.RedirectStandardInput = $true + $startInfo.EnvironmentVariables['IOTDB_HOME'] = $edgeHome + $startInfo.EnvironmentVariables['IOTDB_CONF'] = $configDir + $startInfo.EnvironmentVariables['JAVA_HOME'] = $javaHome + $startInfo.EnvironmentVariables['EDGE_TEST_JAVA_ARGS'] = $argsFile + $startInfo.EnvironmentVariables['IOTDB_JMX_OPTS'] = '' + $startInfo.EnvironmentVariables['TSFILE_LOCALE_JVM_OPT'] = '-Dtsfile.locale=stale' + $process = [System.Diagnostics.Process]::new() + $process.StartInfo = $startInfo + try { + [void]$process.Start() + $stdout = $process.StandardOutput.ReadToEndAsync() + $stderr = $process.StandardError.ReadToEndAsync() + $process.StandardInput.Close() + if (-not $process.WaitForExit(30000)) { + $process.Kill() + throw 'Timed out running the Edge Windows launcher' + } + return [pscustomobject]@{ + ExitCode = $process.ExitCode + Output = $stdout.Result + $stderr.Result + JavaInvoked = Test-Path -LiteralPath $argsFile + Arguments = if (Test-Path -LiteralPath $argsFile) { Get-Content -LiteralPath $argsFile -Raw } else { '' } + } + } finally { + $process.Dispose() + } +} + +function Assert-LaunchResult { + param($Result, [bool]$ShouldLaunch, [string]$Name) + if ($ShouldLaunch) { + if ($Result.ExitCode -ne 0 -or -not $Result.JavaInvoked) { + throw "${Name}: expected a successful Java launch. $($Result.Output)" + } + if ($Result.Arguments -notmatch 'org\.apache\.iotdb\.edge\.EdgeNode') { + throw "${Name}: the Edge main class was not invoked" + } + } elseif ($Result.ExitCode -eq 0 -or $Result.JavaInvoked) { + throw "${Name}: expected rejection before starting Java. $($Result.Output)" + } + Write-Host "PASS: $Name" +} + +try { + # Use an executable stub so batch control flow and argument quoting match a real JVM. + Add-Type -OutputAssembly $javaStub -OutputType ConsoleApplication -TypeDefinition @' +using System; +using System.IO; + +internal static class EdgeJavaStub +{ + private static int Main(string[] args) + { + if (args.Length == 1 && args[0] == "-fullversion") + { + Console.Error.WriteLine("openjdk full version \"17.0.5+8\""); + return 0; + } + File.WriteAllLines(Environment.GetEnvironmentVariable("EDGE_TEST_JAVA_ARGS"), args); + return 0; + } +} +'@ + + $freePorts = Get-FreePorts + foreach ($locale in @('', '-Dtsfile.locale=zh')) { + $result = Invoke-EdgeLauncher -Ports $freePorts -Locale $locale + Assert-LaunchResult $result $true "locale '$locale', custom config and paths with spaces" + if ($locale -eq '') { + if ($result.Arguments -match '-Dtsfile\.locale=') { + throw 'The default package inherited a stale TsFile locale option' + } + } elseif ([regex]::Matches($result.Arguments, '-Dtsfile\.locale=zh').Count -ne 1 -or $result.Arguments -match 'tsfile\.locale=stale') { + throw 'The zh package did not apply exactly one filtered TsFile locale option' + } + } + + foreach ($name in $portNames) { + $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0) + $listener.Start() + try { + $ports = $freePorts.Clone() + $ports[$name] = $listener.LocalEndpoint.Port + $result = Invoke-EdgeLauncher -Ports $ports + Assert-LaunchResult $result $false "occupied $name" + if ($result.Output -notmatch "The $name $($ports[$name]) is already occupied") { + throw "The occupied port was not identified correctly: $($result.Output)" + } + } finally { + $listener.Stop() + } + } + + $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0) + $listener.Start() + try { + $result = Invoke-EdgeLauncher -Ports $freePorts -ExtraLines @( + "# cn_internal_port=$($listener.LocalEndpoint.Port)", + "! dn_rpc_port=$($listener.LocalEndpoint.Port)", + "cn_internal_port=$($listener.LocalEndpoint.Port)" + ) + Assert-LaunchResult $result $true 'comments, whitespace and the last value of a repeated property' + } finally { + $listener.Stop() + } + + $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 10720) + $listener.Start() + try { + $ports = $freePorts.Clone() + $ports.Remove('cn_consensus_port') + $result = Invoke-EdgeLauncher -Ports $ports + Assert-LaunchResult $result $false 'default port for an omitted property' + $result = Invoke-EdgeLauncher -Ports @{} -MissingConfig + Assert-LaunchResult $result $false 'default ports when the configuration file is absent' + if ($result.Output -notmatch 'cn_consensus_port 10720 is already occupied') { + throw 'The missing-file fallback did not check the default ConfigNode consensus port' + } + } finally { + $listener.Stop() + } + + foreach ($value in @('0', '65536', 'not-a-port')) { + $ports = $freePorts.Clone() + $ports['dn_rpc_port'] = $value + $result = Invoke-EdgeLauncher -Ports $ports + Assert-LaunchResult $result $false "invalid port '$value'" + } + Write-Host "All $script:caseCount Windows Edge launcher cases passed." +} finally { + Remove-Item -LiteralPath $testRoot -Recurse -Force +} diff --git a/.github/workflows/edge-it.yml b/.github/workflows/edge-it.yml new file mode 100644 index 0000000000000..641584170111c --- /dev/null +++ b/.github/workflows/edge-it.yml @@ -0,0 +1,94 @@ +name: Edge IT + +on: + push: + branches: + - master + - "rel/*" + - "rc/*" + paths-ignore: + - "docs/**" + - "site/**" + - "iotdb-client/client-cpp/**" + - ".github/workflows/client-cpp-package.yml" + - ".github/scripts/package-client-cpp-*.sh" + - ".github/workflows/multi-language-client.yml" + pull_request: + branches: + - master + - "rel/*" + - "rc/*" + paths-ignore: + - "docs/**" + - "site/**" + - "iotdb-client/client-cpp/**" + - ".github/workflows/client-cpp-package.yml" + - ".github/scripts/package-client-cpp-*.sh" + - ".github/workflows/multi-language-client.yml" + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + MAVEN_OPTS: -Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false -Dmaven.wagon.http.retryHandler.class=standard -Dmaven.wagon.http.retryHandler.count=3 + MAVEN_ARGS: --batch-mode --no-transfer-progress + DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + +jobs: + Ubuntu: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v5 + - name: Set up JDK + uses: actions/setup-java@v5 + with: + distribution: corretto + java-version: 17 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Cache Maven packages + uses: actions/cache@v5 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2- + - name: Build Edge distribution + shell: bash + run: | + mvn clean package \ + -DskipTests \ + -pl distribution \ + -am + - name: Edge IT + shell: bash + run: | + mvn verify \ + -P with-integration-tests,EdgeIT \ + -DskipUTs \ + -Dit.test=IoTDBEdgeBasicIT \ + -DEdgeConfigNodeAddress=127.0.0.2 \ + -DfailIfNoTests=false \ + -Dfailsafe.failIfNoSpecifiedTests=false \ + -pl integration-test \ + -am + - name: Upload Artifact + if: failure() + uses: actions/upload-artifact@v6 + with: + name: edge-log-Linux + path: | + integration-test/target/edge-it/**/*.log + integration-test/target/failsafe-reports + if-no-files-found: ignore + retention-days: 1 + + WindowsScripts: + runs-on: windows-latest + steps: + - uses: actions/checkout@v5 + - name: Test Edge Windows launchers + shell: powershell + run: .github/scripts/test-edge-windows.ps1 diff --git a/CLAUDE.md b/CLAUDE.md index c1852372d90ce..33015b568d962 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -92,6 +92,10 @@ mvn clean verify -DskipUTs -Dit.test=ClassName -DfailIfNoTests=false -Dfailsafe. # Run a single test method within an IT class (use ClassName#methodName) mvn clean verify -DskipUTs -Dit.test=ClassName#methodName -DfailIfNoTests=false -Dfailsafe.failIfNoSpecifiedTests=false -pl integration-test -am -PTableSimpleIT -P with-integration-tests + +# Build the Edge distribution and run its dedicated Tree/Table read-write IT +mvn clean package -DskipTests -pl distribution -am +mvn verify -DskipUTs -Dit.test=IoTDBEdgeBasicIT -DfailIfNoTests=false -Dfailsafe.failIfNoSpecifiedTests=false -pl integration-test -am -P EdgeIT -P with-integration-tests ``` When verifying a new feature, only run the specific IT classes/methods that were added or modified in the current branch — do not run all ITs. @@ -113,6 +117,15 @@ To run integration tests from IntelliJ: enable the `with-integration-tests` prof - **DataNode** (`iotdb-core/datanode`): Handles data storage, query execution, and client connections. The main server component. - **AINode** (`iotdb-core/ainode`): Python-based node for AI/ML inference tasks. +### Edge Distribution + +- `EdgeNode` (`iotdb-core/confignode/src/main/java/org/apache/iotdb/edge/EdgeNode.java`) starts the ConfigNode and DataNode in one JVM for resource-constrained single-node deployments. +- `distribution/src/assembly/edge.xml` produces the additional `apache-iotdb--edge-bin.zip` artifact. Keep its `edge-bin` assembly id unique so it cannot replace an existing attached artifact. +- Edge-specific system defaults live under `iotdb-core/node-commons/src/assembly/resources/conf/edge/`. Standard node, all-in-one, and integration-test assemblies must continue to exclude `edge/**`. +- Edge launchers must reuse the configuration-aware port checks in `scripts/conf/iotdb-common.sh`. Stop scripts must match both the `EdgeNode` main class and the exact `IOTDB_HOME`; never fall back to a global process-name kill. +- Package node-independent tools that work with the combined process. Exclude scripts that require standalone node launchers or environment files, including destructive, daemon-management, and health-check scripts. +- After packaging changes, run a clean distribution build, check that every existing distribution artifact is still produced, and inspect the Edge zip contents and startup/shutdown behavior. + ### Dual Data Model IoTDB supports two data models operating on the same storage: diff --git a/README.md b/README.md index 8d6923fbdb2cc..d444e312d519c 100644 --- a/README.md +++ b/README.md @@ -303,6 +303,18 @@ Under the root path of iotdb: After being built, the IoTDB distribution is located at the folder: "distribution/target". +### Build IoTDB Edge + +The distribution build also produces `apache-iotdb--edge-bin.zip`. IoTDB Edge runs the ConfigNode and DataNode in one JVM for resource-constrained, single-node deployments. It is an additional artifact and does not replace any existing distribution package. + +After extracting the package, configure `conf/iotdb-system.properties`, then start or stop the Edge process with: + +```bash +sbin/start-edge.sh +sbin/stop-edge.sh +``` + +On Windows, use `sbin\windows\start-edge.bat` and `sbin\windows\stop-edge.bat`. The package retains tools that are compatible with the combined Edge process. ### Only build cli diff --git a/README_ZH.md b/README_ZH.md index 73ea63cf67d52..415bb42d31201 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -160,6 +160,19 @@ git checkout rel/x.x 编译完成后, IoTDB 二进制包将生成在: "distribution/target". +### 源码编译 IoTDB Edge + +上述发行版构建还会生成 `apache-iotdb--edge-bin.zip`。IoTDB Edge 在同一个 JVM 中运行 ConfigNode 和 DataNode,适用于资源受限的单机部署。它是新增制品,不会替换任何已有发行包。 + +解压后,可在 `conf/iotdb-system.properties` 中调整配置,并使用以下脚本启停 Edge 进程: + +```bash +sbin/start-edge.sh +sbin/stop-edge.sh +``` + +Windows 环境请使用 `sbin\windows\start-edge.bat` 和 `sbin\windows\stop-edge.bat`。Edge 包会保留与合并进程兼容的工具脚本。 + ### 只编译 cli 在 iotdb/iotdb-client 目录下执行: diff --git a/distribution/pom.xml b/distribution/pom.xml index 3e618451f58f0..4a8b1c33c947e 100644 --- a/distribution/pom.xml +++ b/distribution/pom.xml @@ -89,6 +89,7 @@ src/assembly/confignode.xml src/assembly/cli.xml src/assembly/library-udf.xml + src/assembly/edge.xml apache-iotdb-${project.version} @@ -123,6 +124,7 @@ apache-iotdb-${project.version}-confignode-bin.zip apache-iotdb-${project.version}-library-udf-bin.zip apache-iotdb-${project.version}-external-service-impl-bin.zip + apache-iotdb-${project.version}-edge-bin.zip diff --git a/distribution/src/assembly/all.xml b/distribution/src/assembly/all.xml index c8b583de66460..610e53edbfcb3 100644 --- a/distribution/src/assembly/all.xml +++ b/distribution/src/assembly/all.xml @@ -57,6 +57,9 @@ conf ${project.basedir}/../iotdb-core/node-commons/src/assembly/resources/conf + + edge/** + conf @@ -64,6 +67,8 @@ ainode-env.* **/ainode-env.* + edge-env.* + **/edge-env.* iotdb-common.sh **/iotdb-common.bat @@ -85,6 +90,8 @@ *ainode.* **/*ainode.* + *edge.* + **/*edge.* 0755 diff --git a/distribution/src/assembly/confignode.xml b/distribution/src/assembly/confignode.xml index 6c3d04558eb65..ee107cf17f5ac 100644 --- a/distribution/src/assembly/confignode.xml +++ b/distribution/src/assembly/confignode.xml @@ -44,6 +44,9 @@ conf ${project.basedir}/../iotdb-core/node-commons/src/assembly/resources/conf + + edge/** + ${project.basedir}/../scripts/conf diff --git a/distribution/src/assembly/datanode.xml b/distribution/src/assembly/datanode.xml index 225fa5a7e7d28..d3996da97863b 100644 --- a/distribution/src/assembly/datanode.xml +++ b/distribution/src/assembly/datanode.xml @@ -41,6 +41,9 @@ conf ${project.basedir}/../iotdb-core/node-commons/src/assembly/resources/conf + + edge/** + ${project.basedir}/../scripts/conf diff --git a/distribution/src/assembly/edge.xml b/distribution/src/assembly/edge.xml new file mode 100644 index 0000000000000..54c598e80b529 --- /dev/null +++ b/distribution/src/assembly/edge.xml @@ -0,0 +1,147 @@ + + + + edge-bin + + dir + zip + + apache-iotdb-${project.version}-edge-bin + + + + *:iotdb-server:zip:* + *:iotdb-cli:zip:* + *:iotdb-confignode:zip:* + + ${file.separator} + ${artifact.artifactId}.${artifact.extension} + true + + + tools/** + conf/** + sbin/** + + + + + + + + conf + ${project.basedir}/../iotdb-core/datanode/src/assembly/resources/conf + + logback-datanode.xml + + + + + conf + ${project.basedir}/../iotdb-core/node-commons/src/assembly/resources/conf + + iotdb-system.properties + edge/** + + + + + conf + ${project.basedir}/../iotdb-core/node-commons/src/assembly/resources/conf/edge + + + + conf + ${project.basedir}/src/assembly/resources/conf-edge + + + conf + ${project.basedir}/../scripts/conf + + edge-env.sh + windows/edge-env.bat + + 0755 + + + conf + ${project.basedir}/../scripts/conf + + iotdb-common.sh + windows/iotdb-common.bat + + true + 0755 + + + + sbin + ${project.basedir}/../scripts/sbin + + start-edge.sh + stop-edge.sh + start-cli.sh + windows/start-edge.bat + windows/check-edge.ps1 + windows/stop-edge.bat + windows/start-cli.bat + windows/start-cli-table.bat + + 0755 + + + + tools + ${project.basedir}/../scripts/tools + + *ainode.* + **/*ainode.* + ops/daemon-*.sh + ops/destroy-*.sh + ops/health_check.sh + windows/ops/destroy-*.bat + windows/ops/health_check.bat + + 0755 + + + + + ${project.basedir}/../iotdb-client/cli/src/assembly/resources/conf/logback-backup.xml + conf + 0755 + + + ${maven.multiModuleProjectDirectory}/external-service-impl/mqtt/target/mqtt-${project.version}-jar-with-dependencies.jar + lib + + + ${maven.multiModuleProjectDirectory}/external-service-impl/rest/target/rest-${project.version}-jar-with-dependencies.jar + lib + + + + common-files.xml + + diff --git a/distribution/src/assembly/resources/conf-edge/logback-edge.xml b/distribution/src/assembly/resources/conf-edge/logback-edge.xml new file mode 100644 index 0000000000000..3d36fbf1406f6 --- /dev/null +++ b/distribution/src/assembly/resources/conf-edge/logback-edge.xml @@ -0,0 +1,244 @@ + + + + + + + + ${IOTDB_HOME}/logs/log_edge_error.log + + ${IOTDB_HOME}/logs/log-edge-error-%d{yyyyMMdd}.log.gz + 30 + + true + + %d [%t] %-5p %C{25}:%L - %m %n + utf-8 + + + error + ACCEPT + DENY + + + + ${IOTDB_HOME}/logs/log_edge_warn.log + + ${IOTDB_HOME}/logs/log-edge-warn-%d{yyyyMMdd}.log.gz + 30 + + true + + %d [%t] %-5p %C{25}:%L - %m %n + utf-8 + + + WARN + ACCEPT + DENY + + + + ${IOTDB_HOME}/logs/log_edge_debug.log + + ${IOTDB_HOME}/logs/log-edge-debug-%d{yyyyMMdd}.log.gz + 30 + + true + + %d [%t] %-5p %C{25}:%L - %m %n + utf-8 + + + DEBUG + ACCEPT + DENY + + + + ${IOTDB_HOME}/logs/log_edge_trace.log + + ${IOTDB_HOME}/logs/log-edge-trace-%d{yyyyMMdd}.log.gz + 30 + + true + + %d [%t] %-5p %C{25}:%L - %m %n + utf-8 + + + TRACE + ACCEPT + DENY + + + + System.out + + %d [%t] %-5p %C{25}:%L - %m %n + utf-8 + + + ${CONSOLE_LOG_LEVEL:-DEBUG} + + + + + ${IOTDB_HOME}/logs/log_edge_all.log + + ${IOTDB_HOME}/logs/log-edge-all-%d{yyyyMMdd}.log.gz + 30 + + true + + %d [%t] %-5p %C{25}:%L - %m %n + utf-8 + + + INFO + + + + ${IOTDB_HOME}/logs/log_edge_measure.log + + ${IOTDB_HOME}/logs/log-edge-measure-%d{yyyyMMdd}.log.gz + 30 + + true + + %d [%t] %-5p %C{25}:%L - %m %n + utf-8 + + + INFO + + + + ${IOTDB_HOME}/logs/log_edge_query_debug.log + + ${IOTDB_HOME}/logs/log-edge-query-debug-%d{yyyyMMdd}.log.gz + 30 + + true + + %d [%t] %C{25}:%L - %m %n + utf-8 + + + INFO + + + + ${IOTDB_HOME}/logs/log_edge_slow_sql.log + + ${IOTDB_HOME}/logs/log-edge-slow-sql-%d{yyyyMMdd}.log.gz + 30 + + true + + %d [%t] %-5p %C{25}:%L - %m %n + utf-8 + + + INFO + + + + ${IOTDB_HOME}/logs/log_edge_sampled_queries.log + + ${IOTDB_HOME}/logs/log-edge-sampled-queries-%d{yyyyMMdd}.log.gz + 30 + + true + + %d %m %n + utf-8 + + + INFO + + + + ${IOTDB_HOME}/logs/log_edge_compaction.log + + ${IOTDB_HOME}/logs/log-edge-compaction-%d{yyyyMMdd}.log.gz + 30 + + true + + %d [%t] %-5p %C{25}:%L - %m %n + utf-8 + + + INFO + + + + ${IOTDB_HOME}/logs/log_explain_analyze.log + + ${IOTDB_HOME}/logs/log-edge-explain-%d{yyyyMMdd}.log.gz + 30 + + true + + %d [%t] %-5p %C{25}:%L - %m %n + utf-8 + + + INFO + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/integration-test/pom.xml b/integration-test/pom.xml index 2152a5e113475..f6624fbd65d65 100644 --- a/integration-test/pom.xml +++ b/integration-test/pom.xml @@ -30,6 +30,7 @@ IoTDB: Integration-Test + ${maven.multiModuleProjectDirectory}/distribution/target/apache-iotdb-${project.version}-edge-bin.zip 1 true @@ -333,6 +334,7 @@ ${integrationTest.forkCount} false + ${integrationTest.edgePackage} ${integrationTest.testEnv} ${integrationTest.randomSelectWriteNode} ${integrationTest.readAndVerifyWithMultiNode} @@ -447,6 +449,20 @@ + + EdgeIT + + false + + + org.apache.iotdb.itbase.category.ManualIT + org.apache.iotdb.itbase.category.EdgeIT + true + false + false + Simple + + SimpleIT diff --git a/integration-test/src/assembly/mpp-test.xml b/integration-test/src/assembly/mpp-test.xml index 58bb8da0e1562..e6fa76d31d81e 100644 --- a/integration-test/src/assembly/mpp-test.xml +++ b/integration-test/src/assembly/mpp-test.xml @@ -37,6 +37,9 @@ conf ${project.basedir}/../iotdb-core/node-commons/src/assembly/resources/conf + + edge/** + conf diff --git a/integration-test/src/main/java/org/apache/iotdb/itbase/category/EdgeIT.java b/integration-test/src/main/java/org/apache/iotdb/itbase/category/EdgeIT.java new file mode 100644 index 0000000000000..f70516bd11155 --- /dev/null +++ b/integration-test/src/main/java/org/apache/iotdb/itbase/category/EdgeIT.java @@ -0,0 +1,21 @@ +/* + * 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.itbase.category; + +public interface EdgeIT {} diff --git a/integration-test/src/test/java/org/apache/iotdb/edge/it/IoTDBEdgeBasicIT.java b/integration-test/src/test/java/org/apache/iotdb/edge/it/IoTDBEdgeBasicIT.java new file mode 100644 index 0000000000000..8bfaf3b0ebd43 --- /dev/null +++ b/integration-test/src/test/java/org/apache/iotdb/edge/it/IoTDBEdgeBasicIT.java @@ -0,0 +1,396 @@ +/* + * 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.edge.it; + +import org.apache.iotdb.isession.SessionConfig; +import org.apache.iotdb.it.env.cluster.EnvUtils; +import org.apache.iotdb.it.framework.IoTDBTestRunner; +import org.apache.iotdb.itbase.category.EdgeIT; +import org.apache.iotdb.jdbc.Config; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilderFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +@RunWith(IoTDBTestRunner.class) +@Category(EdgeIT.class) +public class IoTDBEdgeBasicIT { + + private static final Path WORK_DIR = + Paths.get("target", "edge-it", IoTDBEdgeBasicIT.class.getSimpleName()).toAbsolutePath(); + private static final Path START_SCRIPT_LOG = WORK_DIR.resolve("start-edge-script.log"); + private static final Path STOP_SCRIPT_LOG = WORK_DIR.resolve("stop-edge-script.log"); + + private static final long SCRIPT_TIMEOUT_SECONDS = 45; + private static final long STARTUP_TIMEOUT_SECONDS = 120; + private static final Properties PACKAGED_SYSTEM_PROPERTIES = new Properties(); + + private static Path edgeHome; + private static int[] ports; + private static int rpcPort; + private static long edgePid = -1; + + @BeforeClass + public static void setUp() throws Exception { + deleteRecursively(WORK_DIR); + Files.createDirectories(WORK_DIR); + + final String packageProperty = System.getProperty("EdgePackage"); + assertTrue( + "The EdgePackage system property must point to the Edge zip", packageProperty != null); + final Path edgePackage = Paths.get(packageProperty).toAbsolutePath().normalize(); + assertTrue("Edge package does not exist: " + edgePackage, Files.isRegularFile(edgePackage)); + + final Path extractionDir = WORK_DIR.resolve("package"); + unzip(edgePackage, extractionDir); + edgeHome = findEdgeHome(extractionDir); + try (InputStream input = + Files.newInputStream(edgeHome.resolve("conf/iotdb-system.properties"))) { + PACKAGED_SYSTEM_PROPERTIES.load(input); + } + + ports = EnvUtils.searchAvailablePorts(); + rpcPort = ports[2]; + configurePorts(edgeHome.resolve("conf/iotdb-system.properties")); + + runScript(edgeHome.resolve("sbin/start-edge.sh"), START_SCRIPT_LOG); + edgePid = Long.parseLong(Files.readString(edgeHome.resolve("edge.pid")).trim()); + waitUntilReady(); + } + + @AfterClass + public static void tearDown() throws Exception { + AssertionError stopFailure = null; + try { + if (edgeHome != null && Files.isRegularFile(edgeHome.resolve("sbin/stop-edge.sh"))) { + try { + runScript(edgeHome.resolve("sbin/stop-edge.sh"), STOP_SCRIPT_LOG); + } catch (Exception | AssertionError e) { + stopFailure = new AssertionError("Failed to stop IoTDB Edge with stop-edge.sh", e); + } + } + } finally { + stopProcessForciblyIfNeeded(); + if (ports != null) { + Files.deleteIfExists(Paths.get(EnvUtils.getLockFilePath(ports[0]))); + } + } + if (stopFailure != null) { + throw stopFailure; + } + } + + @Test + public void testTreeModelReadWrite() throws SQLException { + try (Connection connection = openTreeConnection(); + Statement statement = connection.createStatement()) { + statement.execute("CREATE DATABASE root.edge_it"); + statement.execute( + "CREATE TIMESERIES root.edge_it.device.s1 WITH DATATYPE=INT32, ENCODING=PLAIN"); + statement.execute("INSERT INTO root.edge_it.device(time,s1) VALUES (1,42), (2,84)"); + + try (ResultSet resultSet = + statement.executeQuery("SELECT s1 FROM root.edge_it.device ORDER BY TIME")) { + assertTrue(resultSet.next()); + assertEquals(1, resultSet.getLong(1)); + assertEquals(42, resultSet.getInt(2)); + assertTrue(resultSet.next()); + assertEquals(2, resultSet.getLong(1)); + assertEquals(84, resultSet.getInt(2)); + assertFalse(resultSet.next()); + } + } + } + + @Test + public void testTableModelReadWrite() throws SQLException { + try (Connection connection = openTableConnection(); + Statement statement = connection.createStatement()) { + statement.execute("CREATE DATABASE edge_it_table"); + statement.execute("USE edge_it_table"); + statement.execute("CREATE TABLE sensor(device STRING TAG, value INT32 FIELD)"); + statement.execute("INSERT INTO sensor(time,device,value) VALUES (1,'d1',42), (2,'d2',84)"); + + try (ResultSet resultSet = + statement.executeQuery("SELECT device, value FROM sensor ORDER BY time")) { + assertTrue(resultSet.next()); + assertEquals("d1", resultSet.getString(1)); + assertEquals(42, resultSet.getInt(2)); + assertTrue(resultSet.next()); + assertEquals("d2", resultSet.getString(1)); + assertEquals(84, resultSet.getInt(2)); + assertFalse(resultSet.next()); + } + } + } + + private static Connection openTreeConnection() throws SQLException { + return DriverManager.getConnection( + jdbcUrl(), SessionConfig.DEFAULT_USER, SessionConfig.DEFAULT_PASSWORD); + } + + private static Connection openTableConnection() throws SQLException { + return DriverManager.getConnection( + jdbcUrl() + "?sql_dialect=table", + SessionConfig.DEFAULT_USER, + SessionConfig.DEFAULT_PASSWORD); + } + + @Test + public void testPackagedConfiguration() throws Exception { + assertFalse(PACKAGED_SYSTEM_PROPERTIES.containsKey("model_inference_execution_thread_count")); + assertTrue(Files.isRegularFile(edgeHome.resolve("sbin/windows/check-edge.ps1"))); + + final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); + final Document document; + try (InputStream input = Files.newInputStream(edgeHome.resolve("conf/logback-edge.xml"))) { + document = factory.newDocumentBuilder().parse(input); + } + final Set appenderNames = new HashSet<>(); + final NodeList appenders = document.getElementsByTagName("appender"); + for (int i = 0; i < appenders.getLength(); i++) { + appenderNames.add(((Element) appenders.item(i)).getAttribute("name")); + } + final NodeList references = document.getElementsByTagName("appender-ref"); + for (int i = 0; i < references.getLength(); i++) { + final String name = ((Element) references.item(i)).getAttribute("ref"); + assertTrue("Undefined Edge log appender: " + name, appenderNames.contains(name)); + } + } + + private static String jdbcUrl() { + return Config.IOTDB_URL_PREFIX + "127.0.0.1:" + rpcPort; + } + + private static void configurePorts(final Path configFile) throws IOException { + final String configNodeAddress = System.getProperty("EdgeConfigNodeAddress", "127.0.0.1"); + final Map replacements = new LinkedHashMap<>(); + replacements.put("cn_seed_config_node", configNodeAddress + ":" + ports[0]); + replacements.put("dn_seed_config_node", configNodeAddress + ":" + ports[0]); + replacements.put("cn_internal_address", configNodeAddress); + replacements.put("cn_internal_port", Integer.toString(ports[0])); + replacements.put("cn_consensus_port", Integer.toString(ports[1])); + replacements.put("dn_rpc_address", "127.0.0.1"); + replacements.put("dn_rpc_port", Integer.toString(ports[2])); + replacements.put("dn_internal_address", "127.0.0.1"); + replacements.put("dn_internal_port", Integer.toString(ports[3])); + replacements.put("dn_mpp_data_exchange_port", Integer.toString(ports[4])); + replacements.put("dn_schema_region_consensus_port", Integer.toString(ports[5])); + replacements.put("dn_data_region_consensus_port", Integer.toString(ports[6])); + replacements.put("cn_metric_prometheus_reporter_port", Integer.toString(ports[7])); + replacements.put("dn_metric_prometheus_reporter_port", Integer.toString(ports[8])); + + final Set replacedKeys = new HashSet<>(); + final List configuredLines = new ArrayList<>(); + for (final String line : Files.readAllLines(configFile, StandardCharsets.UTF_8)) { + final int separatorIndex = line.indexOf('='); + final String key = separatorIndex < 0 ? line : line.substring(0, separatorIndex).trim(); + if (replacements.containsKey(key)) { + configuredLines.add(key + "=" + replacements.get(key)); + replacedKeys.add(key); + } else { + configuredLines.add(line); + } + } + if (!replacedKeys.equals(replacements.keySet())) { + final Set missingKeys = new HashSet<>(replacements.keySet()); + missingKeys.removeAll(replacedKeys); + throw new IOException("Missing Edge configuration properties: " + missingKeys); + } + Files.write(configFile, configuredLines, StandardCharsets.UTF_8); + } + + private static void waitUntilReady() throws Exception { + Class.forName("org.apache.iotdb.jdbc.IoTDBDriver"); + final long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(STARTUP_TIMEOUT_SECONDS); + SQLException lastException = null; + while (System.nanoTime() < deadline) { + if (!ProcessHandle.of(edgePid).map(ProcessHandle::isAlive).orElse(false)) { + break; + } + try (Connection connection = openTreeConnection(); + Statement statement = connection.createStatement(); + ResultSet ignored = statement.executeQuery("SHOW DATABASES")) { + return; + } catch (SQLException e) { + lastException = e; + } + Thread.sleep(1000); + } + + final Path consoleLog = edgeHome.resolve("logs/log_edge_console.log"); + throw new AssertionError( + "IoTDB Edge did not become ready. Last JDBC error: " + + lastException + + System.lineSeparator() + + readLogTail(consoleLog)); + } + + private static void runScript(final Path script, final Path outputFile) throws Exception { + Files.createDirectories(outputFile.getParent()); + final ProcessBuilder processBuilder = new ProcessBuilder("bash", script.toString()); + processBuilder.directory(edgeHome.toFile()); + processBuilder.redirectErrorStream(true); + processBuilder.redirectOutput(ProcessBuilder.Redirect.appendTo(outputFile.toFile())); + processBuilder.environment().put("IOTDB_HOME", edgeHome.toString()); + processBuilder.environment().put("IOTDB_CONF", edgeHome.resolve("conf").toString()); + processBuilder.environment().put("IOTDB_DATA_HOME", edgeHome.toString()); + processBuilder.environment().put("IOTDB_LOG_DIR", edgeHome.resolve("logs").toString()); + + final Process process = processBuilder.start(); + if (!process.waitFor(SCRIPT_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + process.destroyForcibly(); + throw new AssertionError( + "Timed out running " + script + System.lineSeparator() + readLogTail(outputFile)); + } + if (process.exitValue() != 0) { + throw new AssertionError( + script + + " exited with code " + + process.exitValue() + + System.lineSeparator() + + readLogTail(outputFile)); + } + } + + private static void stopProcessForciblyIfNeeded() throws InterruptedException { + if (edgePid <= 0) { + return; + } + final ProcessHandle process = ProcessHandle.of(edgePid).orElse(null); + if (process == null || !process.isAlive()) { + return; + } + process.destroy(); + for (int i = 0; i < 10 && process.isAlive(); i++) { + Thread.sleep(1000); + } + if (process.isAlive()) { + process.destroyForcibly(); + } + } + + private static Path findEdgeHome(final Path extractionDir) throws IOException { + try (Stream paths = Files.list(extractionDir)) { + final List directories = paths.filter(Files::isDirectory).collect(Collectors.toList()); + if (directories.size() != 1) { + throw new IOException( + "Expected one top-level directory in the Edge package, but found " + directories); + } + return directories.get(0); + } + } + + private static void unzip(final Path zipFile, final Path destination) throws IOException { + Files.createDirectories(destination); + try (ZipInputStream input = new ZipInputStream(Files.newInputStream(zipFile))) { + ZipEntry entry; + while ((entry = input.getNextEntry()) != null) { + final Path output = destination.resolve(entry.getName()).normalize(); + if (!output.startsWith(destination)) { + throw new IOException("Zip entry escapes the extraction directory: " + entry.getName()); + } + if (entry.isDirectory()) { + Files.createDirectories(output); + } else { + Files.createDirectories(output.getParent()); + Files.copy(input, output, StandardCopyOption.REPLACE_EXISTING); + } + input.closeEntry(); + } + } + } + + private static String readLogTail(final Path logFile) { + if (!Files.isRegularFile(logFile)) { + return "Log file does not exist: " + logFile; + } + try { + final List lines = Files.readAllLines(logFile, StandardCharsets.UTF_8); + return lines.stream() + .skip(Math.max(0, lines.size() - 200)) + .collect(Collectors.joining(System.lineSeparator())); + } catch (IOException e) { + return "Could not read log file " + logFile + ": " + e; + } + } + + private static void deleteRecursively(final Path directory) throws IOException { + if (!Files.exists(directory)) { + return; + } + try (Stream paths = Files.walk(directory)) { + paths + .sorted(Comparator.reverseOrder()) + .forEach( + path -> { + try { + Files.delete(path); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } catch (UncheckedIOException e) { + throw e.getCause(); + } + } +} diff --git a/iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ConfigNodeMessages.java b/iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ConfigNodeMessages.java index c4832cd886c2b..800d59bf727ba 100644 --- a/iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ConfigNodeMessages.java +++ b/iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ConfigNodeMessages.java @@ -678,4 +678,13 @@ private ConfigNodeMessages() {} public static final String EXCEPTION_FAILED_TO_CREATE_OR_ALTER_TOPIC_SUBSCRIBING_ONLY_TO_THE_AUDIT_DATABASE_OR_PATHS_UNDER_IT_IS_NOT_ALLOWED_3E96A6BA = "Failed to create or alter topic, subscribing only to the __audit database or paths under it is not allowed"; + public static final String LOG_STARTING_IOTDB_EDGE_CONFIGNODE_AND_DATANODE_IN_ONE_77F32605 = + "Starting IoTDB Edge: ConfigNode and DataNode in one process"; + public static final String LOG_IOTDB_EDGE_CONFIGNODE_IS_READY_STARTING_DATANODE_6729159E = + "IoTDB Edge: ConfigNode is ready, starting DataNode"; + public static final String EXCEPTION_IOTDB_EDGE_CONFIGNODE_BOOTSTRAP_FAILED_02EEE59A = + "IoTDB Edge: ConfigNode bootstrap failed"; + public static final String + EXCEPTION_IOTDB_EDGE_CONFIGNODE_INTERNAL_PORT_ARG_IS_NOT_READY_WITHIN_03697FF5 = + "IoTDB Edge: ConfigNode internal port %s is not ready within %s ms"; } diff --git a/iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ConfigNodeMessages.java b/iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ConfigNodeMessages.java index 3029154ab3ead..14713cbb8011b 100644 --- a/iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ConfigNodeMessages.java +++ b/iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ConfigNodeMessages.java @@ -721,4 +721,13 @@ private ConfigNodeMessages() {} public static final String EXCEPTION_FAILED_TO_CREATE_OR_ALTER_TOPIC_SUBSCRIBING_ONLY_TO_THE_AUDIT_DATABASE_OR_PATHS_UNDER_IT_IS_NOT_ALLOWED_3E96A6BA = "创建或修改 topic 失败,不允许仅订阅 __audit 数据库或其下的路径"; + public static final String LOG_STARTING_IOTDB_EDGE_CONFIGNODE_AND_DATANODE_IN_ONE_77F32605 = + "正在启动 IoTDB Edge:ConfigNode 与 DataNode 运行于同一进程"; + public static final String LOG_IOTDB_EDGE_CONFIGNODE_IS_READY_STARTING_DATANODE_6729159E = + "IoTDB Edge:ConfigNode 已就绪,开始启动 DataNode"; + public static final String EXCEPTION_IOTDB_EDGE_CONFIGNODE_BOOTSTRAP_FAILED_02EEE59A = + "IoTDB Edge:ConfigNode 启动失败"; + public static final String + EXCEPTION_IOTDB_EDGE_CONFIGNODE_INTERNAL_PORT_ARG_IS_NOT_READY_WITHIN_03697FF5 = + "IoTDB Edge:ConfigNode 内部端口 %s 在 %s ms 内未就绪"; } diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/edge/EdgeNode.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/edge/EdgeNode.java new file mode 100644 index 0000000000000..04396a0be308f --- /dev/null +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/edge/EdgeNode.java @@ -0,0 +1,122 @@ +/* + * 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.edge; + +import org.apache.iotdb.confignode.conf.ConfigNodeDescriptor; +import org.apache.iotdb.confignode.i18n.ConfigNodeMessages; +import org.apache.iotdb.confignode.service.ConfigNode; +import org.apache.iotdb.db.service.DataNode; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Entry point of the IoTDB Edge distribution: starts the ConfigNode and the DataNode services + * inside ONE JVM process, so that a resource-constrained edge machine only pays a single JVM's + * fixed overhead (metaspace, code cache, GC structures, thread stacks). + * + *

The ConfigNode is bootstrapped on a background thread first; once its internal RPC port + * accepts connections (i.e. the seed ConfigNode finished its consensus initialization), the + * DataNode is started on the main thread. Both services then keep the JVM alive with their own + * non-daemon threads. If either node fails fatally, its own error handling terminates the whole + * process, which is the intended single-process semantics of the edge deployment. + * + *

Note for launchers: both {@code CONFIGNODE_HOME} and {@code IOTDB_HOME} system properties must + * point to the installation directory (see {@code sbin/start-edge.sh}), otherwise the ConfigNode + * resolves its data directories against the process working directory. + */ +public final class EdgeNode { + + private static final Logger LOGGER = LoggerFactory.getLogger(EdgeNode.class); + + /** Max duration to wait for the ConfigNode internal RPC port to accept connections. */ + private static final long CONFIG_NODE_READY_TIMEOUT_MS = 300_000L; + + private static final long PORT_PROBE_INTERVAL_MS = 500L; + + /** Extra delay after the port opens, leaving time for the leader election to settle. */ + private static final long LEADER_ELECTION_GRACE_MS = 5_000L; + + private EdgeNode() {} + + public static void main(String[] args) throws Exception { + LOGGER.info(ConfigNodeMessages.LOG_STARTING_IOTDB_EDGE_CONFIGNODE_AND_DATANODE_IN_ONE_77F32605); + + final AtomicReference configNodeError = new AtomicReference<>(); + Thread configNodeThread = + new Thread( + () -> { + try { + ConfigNode.main(new String[] {"-s"}); + } catch (Throwable t) { + configNodeError.set(t); + } + }, + "EdgeNode-ConfigNode-Bootstrap"); + configNodeThread.start(); + + String internalAddress = ConfigNodeDescriptor.getInstance().getConf().getInternalAddress(); + int internalPort = ConfigNodeDescriptor.getInstance().getConf().getInternalPort(); + waitPortOpen(internalAddress, internalPort, configNodeError); + throwIfConfigNodeBootstrapFailed(configNodeError); + Thread.sleep(LEADER_ELECTION_GRACE_MS); + throwIfConfigNodeBootstrapFailed(configNodeError); + LOGGER.info(ConfigNodeMessages.LOG_IOTDB_EDGE_CONFIGNODE_IS_READY_STARTING_DATANODE_6729159E); + + // DataNode.main returns after a successful start; the services of both nodes keep the JVM + // alive with non-daemon threads afterwards. + DataNode.main(new String[] {"-s"}); + } + + private static void throwIfConfigNodeBootstrapFailed(AtomicReference configNodeError) { + Throwable error = configNodeError.get(); + if (error != null) { + throw new IllegalStateException( + ConfigNodeMessages.EXCEPTION_IOTDB_EDGE_CONFIGNODE_BOOTSTRAP_FAILED_02EEE59A, error); + } + } + + private static void waitPortOpen( + String address, int port, AtomicReference configNodeError) + throws InterruptedException { + long deadline = System.currentTimeMillis() + CONFIG_NODE_READY_TIMEOUT_MS; + while (System.currentTimeMillis() < deadline) { + if (configNodeError.get() != null) { + return; + } + try (Socket socket = new Socket()) { + socket.connect(new InetSocketAddress(address, port), 1000); + return; + } catch (Exception e) { + Thread.sleep(PORT_PROBE_INTERVAL_MS); + } + } + throw new IllegalStateException( + String.format( + ConfigNodeMessages + .EXCEPTION_IOTDB_EDGE_CONFIGNODE_INTERNAL_PORT_ARG_IS_NOT_READY_WITHIN_03697FF5, + port, + CONFIG_NODE_READY_TIMEOUT_MS)); + } +} diff --git a/iotdb-core/node-commons/src/assembly/resources/conf/edge/iotdb-system.properties b/iotdb-core/node-commons/src/assembly/resources/conf/edge/iotdb-system.properties new file mode 100644 index 0000000000000..13c87b5295b42 --- /dev/null +++ b/iotdb-core/node-commons/src/assembly/resources/conf/edge/iotdb-system.properties @@ -0,0 +1,125 @@ +# +# 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. +# + +#################### +### Cluster Configuration +#################### + +cluster_name=defaultCluster + +#################### +### Seed ConfigNode +#################### + +cn_seed_config_node=127.0.0.1:10710 + +dn_seed_config_node=127.0.0.1:10710 + +#################### +### Node RPC Configuration +#################### + +cn_internal_address=127.0.0.1 +cn_internal_port=10710 +cn_consensus_port=10720 + +dn_rpc_address=127.0.0.1 +dn_rpc_port=6667 +dn_internal_address=127.0.0.1 +dn_internal_port=10730 +dn_mpp_data_exchange_port=10740 +dn_schema_region_consensus_port=10750 +dn_data_region_consensus_port=10760 + +#################### +### Replication configuration +#################### + +schema_replication_factor=1 +data_replication_factor=1 + +#################### +### Directory Configuration +#################### + +# dn_data_dirs=data/datanode/data +# dn_wal_dirs=data/datanode/wal + +#################### +### Metric Configuration +#################### + +# cn_metric_reporter_list= +cn_metric_prometheus_reporter_port=9091 + +# dn_metric_reporter_list= +dn_metric_prometheus_reporter_port=9092 + +#################### +### IoTDB Edge Tuning +#################### +# The following defaults are tuned for the edge distribution: ConfigNode and +# DataNode run in ONE JVM with a small fixed memory budget (see conf/edge-env.sh), +# sharing the machine with other processes. Validated on x86 and Raspberry Pi 4B. + +# ---- thread pools (small fixed sizes instead of CPU-core-based defaults) ---- +query_thread_count=2 +degree_of_query_parallelism=1 +mpp_data_exchange_core_pool_size=2 +mpp_data_exchange_max_pool_size=2 +flush_thread_count=2 +compaction_thread_count=2 +sub_compaction_thread_count=1 +compaction_schedule_thread_num=1 +pipe_subtask_executor_max_thread_num=2 +pipe_sink_selector_number=1 +pipe_sink_max_client_number=8 +continuous_query_submit_thread_count=1 +into_operation_execution_thread_count=1 +procedure_core_worker_thread_count=2 +partition_table_recover_worker_num=2 +max_sub_task_num_for_information_table_scan=1 +load_active_listening_max_thread_num=1 +dn_selector_thread_nums_of_client_manager=1 +cn_selector_thread_nums_of_client_manager=1 +max_allowed_concurrent_queries=100 + +# ---- memory / file buffers ---- +wal_buffer_size_in_byte=1048576 +group_size_in_byte=4194304 +target_compaction_file_size=134217728 +into_operation_buffer_size_in_byte=8388608 +batch_size=10000 +schema_region_ratis_log_appender_buffer_size_max=4194304 +config_node_ratis_log_appender_buffer_size_max=4194304 + +# ---- background io politeness (share disks with other processes) ---- +compaction_write_throughput_mb_per_sec=8 +partition_table_recover_max_read_mb_per_sec=5 + +# ---- single-node region / partition layout ---- +schema_region_group_extension_policy=CUSTOM +default_schema_region_group_num_per_database=1 +data_region_group_extension_policy=CUSTOM +default_data_region_group_num_per_database=1 +series_slot_num=1 + +# ---- metrics off (the internal MetricService is shared by both nodes) ---- +cn_metric_level=OFF +dn_metric_level=OFF diff --git a/scripts/conf/edge-env.sh b/scripts/conf/edge-env.sh new file mode 100644 index 0000000000000..627623047d46e --- /dev/null +++ b/scripts/conf/edge-env.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# +# 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. +# + +# IoTDB Edge runs ConfigNode and DataNode inside ONE JVM with a fixed, small +# memory budget so that it can share a machine with other processes. +# The defaults below target a total process RSS of about 512 MB and were +# validated on x86 servers and Raspberry Pi 4B class devices. + +# On-heap memory of the merged process. Example values: '224M', '512M'. +ON_HEAP_MEMORY="${ON_HEAP_MEMORY:-224M}" +# Initial heap. Kept small so an idle edge instance stays light. +INIT_HEAP_MEMORY="${INIT_HEAP_MEMORY:-64M}" +# Off-heap (direct buffer) memory. +OFF_HEAP_MEMORY="${OFF_HEAP_MEMORY:-96M}" + +if [ "${OFF_HEAP_MEMORY%"G"}" != "$OFF_HEAP_MEMORY" ]; then + off_heap_memory_size_in_mb=$(expr ${OFF_HEAP_MEMORY%"G"} \* 1024) +else + off_heap_memory_size_in_mb=$(expr ${OFF_HEAP_MEMORY%"M"}) +fi +# Max cached buffer size, which equals OFF_HEAP_MEMORY / io threads number (200) +MAX_CACHED_BUFFER_SIZE=$(expr $off_heap_memory_size_in_mb \* 1024 \* 1024 / 200) + +IOTDB_JMX_OPTS="$IOTDB_JMX_OPTS -Diotdb.jmx.local=true" +IOTDB_JMX_OPTS="$IOTDB_JMX_OPTS -Xms${INIT_HEAP_MEMORY}" +IOTDB_JMX_OPTS="$IOTDB_JMX_OPTS -Xmx${ON_HEAP_MEMORY}" +IOTDB_JMX_OPTS="$IOTDB_JMX_OPTS -XX:MaxDirectMemorySize=${OFF_HEAP_MEMORY}" +IOTDB_JMX_OPTS="$IOTDB_JMX_OPTS -Djdk.nio.maxCachedBufferSize=${MAX_CACHED_BUFFER_SIZE}" +IOTDB_JMX_OPTS="$IOTDB_JMX_OPTS -XX:+CrashOnOutOfMemoryError" +# Serial GC has the lowest fixed memory overhead; typical edge write rates leave +# plenty of latency headroom for its pauses. +IOTDB_JMX_OPTS="$IOTDB_JMX_OPTS -XX:+UseSerialGC" +IOTDB_JMX_OPTS="$IOTDB_JMX_OPTS -Xss320k" +IOTDB_JMX_OPTS="$IOTDB_JMX_OPTS -XX:MaxMetaspaceSize=160m" +IOTDB_JMX_OPTS="$IOTDB_JMX_OPTS -XX:CompressedClassSpaceSize=40m" +IOTDB_JMX_OPTS="$IOTDB_JMX_OPTS -XX:ReservedCodeCacheSize=64m" +# Cap the processors the JVM sees, shrinking internal thread pools on big hosts. +IOTDB_JMX_OPTS="$IOTDB_JMX_OPTS -XX:ActiveProcessorCount=2" +IOTDB_JMX_OPTS="$IOTDB_JMX_OPTS -XX:+UnlockDiagnosticVMOptions" +IOTDB_JMX_OPTS="$IOTDB_JMX_OPTS -XX:+UseCRC32Intrinsics" +IOTDB_JMX_OPTS="$IOTDB_JMX_OPTS -XX:SafepointTimeoutDelay=1000" +IOTDB_JMX_OPTS="$IOTDB_JMX_OPTS -XX:+SafepointTimeout" +IOTDB_JMX_OPTS="$IOTDB_JMX_OPTS -Dsun.jnu.encoding=UTF-8 -Dfile.encoding=UTF-8" + +# Append tsfile locale option populated by Maven at package time +# (see conf/iotdb-common.sh; empty in default build, "-Dtsfile.locale=zh" under with-zh-locale). +if [ -n "$TSFILE_LOCALE_JVM_OPT" ]; then + IOTDB_JMX_OPTS="$IOTDB_JMX_OPTS $TSFILE_LOCALE_JVM_OPT" +fi + +echo "IoTDB Edge on heap memory size = ${ON_HEAP_MEMORY}B, off heap memory size = ${OFF_HEAP_MEMORY}B" +echo "If you want to change this configuration, please check conf/edge-env.sh." diff --git a/scripts/conf/windows/edge-env.bat b/scripts/conf/windows/edge-env.bat new file mode 100644 index 0000000000000..fccaebcef1230 --- /dev/null +++ b/scripts/conf/windows/edge-env.bat @@ -0,0 +1,48 @@ +@echo off +@REM +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM + +@REM IoTDB Edge runs ConfigNode and DataNode inside ONE JVM with a fixed, small +@REM memory budget (about 512 MB total process RSS by default). + +if "%ON_HEAP_MEMORY%"=="" set ON_HEAP_MEMORY=224M +if "%INIT_HEAP_MEMORY%"=="" set INIT_HEAP_MEMORY=64M +if "%OFF_HEAP_MEMORY%"=="" set OFF_HEAP_MEMORY=96M + +set IOTDB_JMX_OPTS=%IOTDB_JMX_OPTS% -Diotdb.jmx.local=true +set IOTDB_JMX_OPTS=%IOTDB_JMX_OPTS% -Xms%INIT_HEAP_MEMORY% +set IOTDB_JMX_OPTS=%IOTDB_JMX_OPTS% -Xmx%ON_HEAP_MEMORY% +set IOTDB_JMX_OPTS=%IOTDB_JMX_OPTS% -XX:MaxDirectMemorySize=%OFF_HEAP_MEMORY% +set IOTDB_JMX_OPTS=%IOTDB_JMX_OPTS% -XX:+CrashOnOutOfMemoryError +set IOTDB_JMX_OPTS=%IOTDB_JMX_OPTS% -XX:+UseSerialGC +set IOTDB_JMX_OPTS=%IOTDB_JMX_OPTS% -Xss320k +set IOTDB_JMX_OPTS=%IOTDB_JMX_OPTS% -XX:MaxMetaspaceSize=160m +set IOTDB_JMX_OPTS=%IOTDB_JMX_OPTS% -XX:CompressedClassSpaceSize=40m +set IOTDB_JMX_OPTS=%IOTDB_JMX_OPTS% -XX:ReservedCodeCacheSize=64m +set IOTDB_JMX_OPTS=%IOTDB_JMX_OPTS% -XX:ActiveProcessorCount=2 +set IOTDB_JMX_OPTS=%IOTDB_JMX_OPTS% -XX:+UnlockDiagnosticVMOptions +set IOTDB_JMX_OPTS=%IOTDB_JMX_OPTS% -XX:+UseCRC32Intrinsics +set IOTDB_JMX_OPTS=%IOTDB_JMX_OPTS% -Dsun.jnu.encoding=UTF-8 -Dfile.encoding=UTF-8 + +@REM Load the Maven-filtered locale before expanding its value in a separate command. +if EXIST "%IOTDB_CONF%\windows\iotdb-common.bat" call "%IOTDB_CONF%\windows\iotdb-common.bat" +if DEFINED TSFILE_LOCALE_JVM_OPT set "IOTDB_JMX_OPTS=%IOTDB_JMX_OPTS% %TSFILE_LOCALE_JVM_OPT%" + +echo IoTDB Edge on heap memory size = %ON_HEAP_MEMORY%B, off heap memory size = %OFF_HEAP_MEMORY%B +echo If you want to change this configuration, please check conf\windows\edge-env.bat. diff --git a/scripts/sbin/start-edge.sh b/scripts/sbin/start-edge.sh new file mode 100644 index 0000000000000..be1a70ea6f795 --- /dev/null +++ b/scripts/sbin/start-edge.sh @@ -0,0 +1,96 @@ +#!/bin/bash +# +# 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. +# + +# Start IoTDB Edge: ConfigNode + DataNode in one JVM process. + +if [ -z "${IOTDB_HOME}" ]; then + export IOTDB_HOME="$(cd "$(dirname "$0")"/.. && pwd)" +fi +if [ -z "${IOTDB_CONF}" ]; then + export IOTDB_CONF=${IOTDB_HOME}/conf +fi +export IOTDB_DATA_HOME=${IOTDB_DATA_HOME:-${IOTDB_HOME}} +export IOTDB_LOG_DIR=${IOTDB_LOG_DIR:-${IOTDB_HOME}/logs} +mkdir -p "${IOTDB_LOG_DIR}" + +source "$(dirname "$0")/../conf/iotdb-common.sh" +export CONFIGNODE_HOME=${IOTDB_HOME} +export CONFIGNODE_DATA_HOME=${IOTDB_DATA_HOME} +export CONFIGNODE_CONF=${IOTDB_CONF} +export CONFIGNODE_LOG_DIR=${IOTDB_LOG_DIR} + +# Reuse the same configuration-aware port checks as the standard launchers. +checkAllVariables +checkAllConfigNodeVariables +checkConfigNodePortUsages +checkDataNodePortUsages + +. "${IOTDB_CONF}/edge-env.sh" + +# find java in JAVA_HOME +if [ -n "$JAVA_HOME" ]; then + for java in "$JAVA_HOME"/bin/amd64/java "$JAVA_HOME"/bin/java; do + if [ -x "$java" ]; then + JAVA="$java" + break + fi + done +else + JAVA=java +fi +if [ -z "$JAVA" ]; then + echo "Unable to find java executable. Check JAVA_HOME and PATH environment variables." > /dev/stderr + exit 1 +fi + +illegal_access_params="" +illegal_access_params="$illegal_access_params --add-opens=java.base/java.util.concurrent=ALL-UNNAMED" +illegal_access_params="$illegal_access_params --add-opens=java.base/java.lang=ALL-UNNAMED" +illegal_access_params="$illegal_access_params --add-opens=java.base/java.util=ALL-UNNAMED" +illegal_access_params="$illegal_access_params --add-opens=java.base/java.nio=ALL-UNNAMED" +illegal_access_params="$illegal_access_params --add-opens=java.base/java.io=ALL-UNNAMED" +illegal_access_params="$illegal_access_params --add-opens=java.base/java.net=ALL-UNNAMED" + +CLASSPATH="" +for f in "${IOTDB_HOME}"/lib/*.jar; do + CLASSPATH=${CLASSPATH}":"$f +done + +iotdb_parms="-Dlogback.configurationFile=${IOTDB_CONF}/logback-edge.xml" +iotdb_parms="$iotdb_parms -DIOTDB_HOME=${IOTDB_HOME}" +# CONFIGNODE_HOME must also point to the installation directory, otherwise the +# ConfigNode part resolves its data directories against the working directory. +iotdb_parms="$iotdb_parms -DCONFIGNODE_HOME=${IOTDB_HOME}" +iotdb_parms="$iotdb_parms -DIOTDB_DATA_HOME=${IOTDB_DATA_HOME}" +iotdb_parms="$iotdb_parms -DTSFILE_HOME=${IOTDB_HOME}" +iotdb_parms="$iotdb_parms -DIOTDB_CONF=${IOTDB_CONF}" +iotdb_parms="$iotdb_parms -DCONFIGNODE_CONF=${IOTDB_CONF}" +iotdb_parms="$iotdb_parms -DTSFILE_CONF=${IOTDB_CONF}" +iotdb_parms="$iotdb_parms -Dname=iotdb.EdgeNode" +iotdb_parms="$iotdb_parms -DIOTDB_LOG_DIR=${IOTDB_LOG_DIR}" +iotdb_parms="$iotdb_parms -DCONFIGNODE_LOG_DIR=${IOTDB_LOG_DIR}" +iotdb_parms="$iotdb_parms -DOFF_HEAP_MEMORY=${OFF_HEAP_MEMORY}" + +classname=org.apache.iotdb.edge.EdgeNode + +echo "Starting IoTDB Edge (ConfigNode + DataNode in one process)" +nohup "$JAVA" $illegal_access_params $iotdb_parms $IOTDB_JMX_OPTS -cp "$CLASSPATH" "$classname" -s > "${IOTDB_LOG_DIR}/log_edge_console.log" 2>&1 & +echo $! > "${IOTDB_HOME}/edge.pid" +echo "IoTDB Edge started, pid $(cat "${IOTDB_HOME}/edge.pid"), console log: ${IOTDB_LOG_DIR}/log_edge_console.log" diff --git a/scripts/sbin/stop-edge.sh b/scripts/sbin/stop-edge.sh new file mode 100644 index 0000000000000..8f8f98ac64b14 --- /dev/null +++ b/scripts/sbin/stop-edge.sh @@ -0,0 +1,114 @@ +#!/bin/bash +# +# 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. +# + +# Stop IoTDB Edge (the merged ConfigNode + DataNode process). + +IOTDB_HOME="$(cd "$(dirname "$0")"/.. && pwd)" + +PID_FILE="${IOTDB_HOME}/edge.pid" + +is_same_edge_home() { + local command_line="$1" + case "$command_line" in + *"-DIOTDB_HOME=${IOTDB_HOME} "*|*"-DIOTDB_HOME=${IOTDB_HOME}") + return 0 + ;; + *) + return 1 + ;; + esac +} + +is_edge_process() { + local pid="$1" + local command_line + command_line=$(ps -ww -p "$pid" -o command= 2>/dev/null) + [ -n "$command_line" ] || return 1 + printf '%s\n' "$command_line" | grep -F -- "org.apache.iotdb.edge.EdgeNode" >/dev/null || return 1 + is_same_edge_home "$command_line" +} + +find_edge_processes() { + local process_line + local pid + while IFS= read -r process_line; do + printf '%s\n' "$process_line" | grep -F -- "org.apache.iotdb.edge.EdgeNode" >/dev/null || continue + is_same_edge_home "$process_line" || continue + pid=$(printf '%s\n' "$process_line" | awk '{print $1}') + printf '%s\n' "$pid" + done < <(ps -axww -o pid= -o command= 2>/dev/null) +} + +stop_edge_process() { + local pid="$1" + if ! is_edge_process "$pid"; then + echo "Refusing to stop PID $pid because it is not IoTDB Edge from ${IOTDB_HOME}." + return 1 + fi + if ! kill "$pid" 2>/dev/null; then + echo "Failed to stop IoTDB Edge process $pid." + return 1 + fi + for i in $(seq 1 30); do + kill -0 "$pid" 2>/dev/null || break + sleep 1 + done + if kill -0 "$pid" 2>/dev/null; then + if ! is_edge_process "$pid"; then + echo "Refusing to force-stop PID $pid because it no longer belongs to this IoTDB Edge installation." + return 1 + fi + kill -9 "$pid" 2>/dev/null + fi + echo "IoTDB Edge process $pid stopped." +} + +PID="" +if [ -f "$PID_FILE" ]; then + PID=$(cat "$PID_FILE") + case "$PID" in + ''|*[!0-9]*) + echo "Ignoring invalid PID file ${PID_FILE}." + PID="" + ;; + esac + if [ -n "$PID" ] && ! is_edge_process "$PID"; then + echo "Ignoring stale PID file ${PID_FILE}; PID $PID does not belong to this IoTDB Edge installation." + PID="" + fi + rm -f "$PID_FILE" +fi + +if [ -n "$PID" ]; then + stop_edge_process "$PID" + exit $? +fi + +FOUND=false +while IFS= read -r PID; do + [ -n "$PID" ] || continue + FOUND=true + stop_edge_process "$PID" || exit 1 +done < <(find_edge_processes) + +if [ "$FOUND" = false ]; then + echo "No IoTDB Edge process from ${IOTDB_HOME} is running." +fi +exit 0 diff --git a/scripts/sbin/windows/check-edge.ps1 b/scripts/sbin/windows/check-edge.ps1 new file mode 100644 index 0000000000000..f6f9037bb547c --- /dev/null +++ b/scripts/sbin/windows/check-edge.ps1 @@ -0,0 +1,66 @@ +# +# 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. +# + +param( + [Parameter(Mandatory = $true)] + [string]$ConfigFile +) + +$ErrorActionPreference = 'Stop' + +# Match the ConfigNode and DataNode defaults when a property is absent. +$ports = [ordered]@{ + cn_internal_port = 10710 + cn_consensus_port = 10720 + dn_rpc_port = 6667 + dn_internal_port = 10730 + dn_mpp_data_exchange_port = 10740 + dn_schema_region_consensus_port = 10750 + dn_data_region_consensus_port = 10760 +} + +if (Test-Path -LiteralPath $ConfigFile -PathType Leaf) { + foreach ($line in Get-Content -LiteralPath $ConfigFile) { + if ($line -cmatch '^\s*([^#!\s=]+)\s*=\s*(.*?)\s*$' -and $ports.Keys -ccontains $Matches[1]) { + $name = $Matches[1] + $value = $Matches[2] + $port = 0 + if (-not [int]::TryParse($value, [ref]$port) -or $port -lt 1 -or $port -gt 65535) { + throw "Invalid port for ${name}: $value" + } + $ports[$name] = $port + } + } +} else { + Write-Host "Cannot find $ConfigFile; checking the default ports." +} + +Write-Host 'Checking whether the ConfigNode and DataNode ports are already occupied...' +$listeners = [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties().GetActiveTcpListeners() +$occupied = $false +foreach ($entry in $ports.GetEnumerator()) { + if ($listeners.Port -contains $entry.Value) { + Write-Host "The $($entry.Key) $($entry.Value) is already occupied." + $occupied = $true + } +} +if ($occupied) { + exit 1 +} +exit 0 diff --git a/scripts/sbin/windows/start-edge.bat b/scripts/sbin/windows/start-edge.bat new file mode 100644 index 0000000000000..af5f7cb5ca291 --- /dev/null +++ b/scripts/sbin/windows/start-edge.bat @@ -0,0 +1,109 @@ +@echo off +@REM +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM + +setlocal + +@REM set cmd format +powershell -NoProfile -Command "$v=(Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion').CurrentMajorVersionNumber; if($v -gt 6) { cmd /c 'chcp 65001' }" + +title IoTDB Edge + +echo ```````````````````````` +echo Starting IoTDB Edge (ConfigNode + DataNode in one process) +echo ```````````````````````` + +@REM ----------------------------------------------------------------------------- +@REM SET JAVA +if DEFINED JAVA_HOME set "PATH=%JAVA_HOME%\bin;%PATH%" +set "FULL_VERSION=" +set "MAJOR_VERSION=" +set "MINOR_VERSION=" + +for /f tokens^=2-5^ delims^=.-_+^" %%j in ('java -fullversion 2^>^&1') do ( + set "FULL_VERSION=%%j-%%k-%%l-%%m" + IF "%%j" == "1" ( + set "MAJOR_VERSION=%%k" + set "MINOR_VERSION=%%l" + ) else ( + set "MAJOR_VERSION=%%j" + set "MINOR_VERSION=%%k" + ) +) + +set JAVA_VERSION=%MAJOR_VERSION% + +@REM IoTDB requires JDK 17 or later. +IF "%JAVA_VERSION%" == "" ( + echo Failed to determine Java version. IoTDB only supports jdk ^>= 17, please check your java installation. + exit /b 1 +) +IF %JAVA_VERSION% LSS 17 ( + echo IoTDB only supports jdk ^>= 17, please check your java version. + exit /b 1 +) + +@REM ----------------------------------------------------------------------------- +@REM SET DIRS +pushd "%~dp0..\.." +if NOT DEFINED IOTDB_HOME set "IOTDB_HOME=%cd%" +popd +if NOT DEFINED IOTDB_CONF set "IOTDB_CONF=%IOTDB_HOME%\conf" +set "IOTDB_LOG_DIR=%IOTDB_HOME%\logs" +if NOT EXIST "%IOTDB_LOG_DIR%" mkdir "%IOTDB_LOG_DIR%" + +@REM Check both nodes' configured ports before starting the merged process. +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0check-edge.ps1" -ConfigFile "%IOTDB_CONF%\iotdb-system.properties" +if ERRORLEVEL 1 exit /b 1 + +@REM ----------------------------------------------------------------------------- +@REM SET JVM OPTIONS +if EXIST "%IOTDB_CONF%\windows\edge-env.bat" ( + call "%IOTDB_CONF%\windows\edge-env.bat" +) else ( + echo Can't find %IOTDB_CONF%\windows\edge-env.bat + exit /b 1 +) + +set illegal_access_params=--add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.nio=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.net=ALL-UNNAMED + +set CLASSPATH=%IOTDB_HOME%\lib\* +set MAIN_CLASS=org.apache.iotdb.edge.EdgeNode + +@REM CONFIGNODE_HOME must also point to the installation directory, otherwise the +@REM ConfigNode part resolves its data directories against the working directory. +set iotdb_parms=-Dlogback.configurationFile="%IOTDB_CONF%\logback-edge.xml" +set iotdb_parms=%iotdb_parms% -DIOTDB_HOME="%IOTDB_HOME%" +set iotdb_parms=%iotdb_parms% -DCONFIGNODE_HOME="%IOTDB_HOME%" +set iotdb_parms=%iotdb_parms% -DIOTDB_DATA_HOME="%IOTDB_HOME%" +set iotdb_parms=%iotdb_parms% -DTSFILE_HOME="%IOTDB_HOME%" +set iotdb_parms=%iotdb_parms% -DIOTDB_CONF="%IOTDB_CONF%" +set iotdb_parms=%iotdb_parms% -DCONFIGNODE_CONF="%IOTDB_CONF%" +set iotdb_parms=%iotdb_parms% -DTSFILE_CONF="%IOTDB_CONF%" +set iotdb_parms=%iotdb_parms% -Dname=iotdb.EdgeNode +set iotdb_parms=%iotdb_parms% -DIOTDB_LOG_DIR="%IOTDB_LOG_DIR%" +set iotdb_parms=%iotdb_parms% -DCONFIGNODE_LOG_DIR="%IOTDB_LOG_DIR%" +set iotdb_parms=%iotdb_parms% -DOFF_HEAP_MEMORY=%OFF_HEAP_MEMORY% + +@REM ----------------------------------------------------------------------------- +@REM START +java %illegal_access_params% %iotdb_parms% %IOTDB_JMX_OPTS% -cp "%CLASSPATH%" %MAIN_CLASS% -s +set "EDGE_EXIT_CODE=%ERRORLEVEL%" +pause +exit /b %EDGE_EXIT_CODE% diff --git a/scripts/sbin/windows/stop-edge.bat b/scripts/sbin/windows/stop-edge.bat new file mode 100644 index 0000000000000..4eed010be6218 --- /dev/null +++ b/scripts/sbin/windows/stop-edge.bat @@ -0,0 +1,26 @@ +@echo off +@REM +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM + +echo Stopping IoTDB Edge (the merged ConfigNode + DataNode process) +pushd %~dp0\..\.. +set "IOTDB_HOME=%cd%" +popd +powershell -NoProfile -Command "$plain='-DIOTDB_HOME=' + $env:IOTDB_HOME; $quoted='-DIOTDB_HOME=' + [char]34 + $env:IOTDB_HOME + [char]34; Get-CimInstance Win32_Process -Filter \"name='java.exe'\" | Where-Object { $line=$_.CommandLine; $sameHome=$line -and ($line.Contains($plain + ' ') -or $line.EndsWith($plain) -or $line.Contains($quoted + ' ') -or $line.EndsWith($quoted)); $sameHome -and $line.Contains('org.apache.iotdb.edge.EdgeNode') } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force; Write-Host ('IoTDB Edge process ' + $_.ProcessId + ' stopped.') }" +pause