This is an automated email from the ASF dual-hosted git repository.
JackieTien97 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git
The following commit(s) were added to refs/heads/master by this push:
new 1106d858f10 Add IoTDB Edge distribution: ConfigNode + DataNode merged
in one JVM (#18538)
1106d858f10 is described below
commit 1106d858f10b567a7f089eae7e518a3c66871c7b
Author: Jackie Tien <[email protected]>
AuthorDate: Mon Aug 31 11:48:52 2026 +0800
Add IoTDB Edge distribution: ConfigNode + DataNode merged in one JVM
(#18538)
---
.github/scripts/test-edge-windows.ps1 | 222 ++++++++++++
.github/workflows/edge-it.yml | 94 +++++
CLAUDE.md | 13 +
README.md | 12 +
README_ZH.md | 13 +
distribution/pom.xml | 2 +
distribution/src/assembly/all.xml | 7 +
distribution/src/assembly/confignode.xml | 3 +
distribution/src/assembly/datanode.xml | 3 +
distribution/src/assembly/{all.xml => edge.xml} | 56 ++-
.../assembly/resources/conf-edge/logback-edge.xml | 244 +++++++++++++
integration-test/pom.xml | 16 +
integration-test/src/assembly/mpp-test.xml | 3 +
.../org/apache/iotdb/itbase/category/EdgeIT.java | 21 ++
.../org/apache/iotdb/edge/it/IoTDBEdgeBasicIT.java | 396 +++++++++++++++++++++
.../iotdb/confignode/i18n/ConfigNodeMessages.java | 9 +
.../iotdb/confignode/i18n/ConfigNodeMessages.java | 9 +
.../main/java/org/apache/iotdb/edge/EdgeNode.java | 122 +++++++
.../resources/conf/edge/iotdb-system.properties | 125 +++++++
scripts/conf/edge-env.sh | 69 ++++
scripts/conf/windows/edge-env.bat | 48 +++
scripts/sbin/start-edge.sh | 96 +++++
scripts/sbin/stop-edge.sh | 114 ++++++
scripts/sbin/windows/check-edge.ps1 | 66 ++++
scripts/sbin/windows/start-edge.bat | 109 ++++++
scripts/sbin/windows/stop-edge.bat | 26 ++
26 files changed, 1884 insertions(+), 14 deletions(-)
diff --git a/.github/scripts/test-edge-windows.ps1
b/.github/scripts/test-edge-windows.ps1
new file mode 100644
index 00000000000..7ca0f5be06e
--- /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 00000000000..64158417011
--- /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 c1852372d90..33015b568d9 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-<version>-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 8d6923fbdb2..d444e312d51 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-<version>-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 73ea63cf67d..415bb42d312 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-<version>-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 3e618451f58..4a8b1c33c94 100644
--- a/distribution/pom.xml
+++ b/distribution/pom.xml
@@ -89,6 +89,7 @@
<descriptor>src/assembly/confignode.xml</descriptor>
<descriptor>src/assembly/cli.xml</descriptor>
<descriptor>src/assembly/library-udf.xml</descriptor>
+ <descriptor>src/assembly/edge.xml</descriptor>
</descriptors>
<finalName>apache-iotdb-${project.version}</finalName>
</configuration>
@@ -123,6 +124,7 @@
<include>apache-iotdb-${project.version}-confignode-bin.zip</include>
<include>apache-iotdb-${project.version}-library-udf-bin.zip</include>
<include>apache-iotdb-${project.version}-external-service-impl-bin.zip</include>
+
<include>apache-iotdb-${project.version}-edge-bin.zip</include>
</includes>
</fileSet>
</fileSets>
diff --git a/distribution/src/assembly/all.xml
b/distribution/src/assembly/all.xml
index c8b583de664..610e53edbfc 100644
--- a/distribution/src/assembly/all.xml
+++ b/distribution/src/assembly/all.xml
@@ -57,6 +57,9 @@
<fileSet>
<outputDirectory>conf</outputDirectory>
<directory>${project.basedir}/../iotdb-core/node-commons/src/assembly/resources/conf</directory>
+ <excludes>
+ <exclude>edge/**</exclude>
+ </excludes>
</fileSet>
<fileSet>
<outputDirectory>conf</outputDirectory>
@@ -64,6 +67,8 @@
<excludes>
<exclude>ainode-env.*</exclude>
<exclude>**/ainode-env.*</exclude>
+ <exclude>edge-env.*</exclude>
+ <exclude>**/edge-env.*</exclude>
<exclude>iotdb-common.sh</exclude>
<exclude>**/iotdb-common.bat</exclude>
</excludes>
@@ -85,6 +90,8 @@
<excludes>
<exclude>*ainode.*</exclude>
<exclude>**/*ainode.*</exclude>
+ <exclude>*edge.*</exclude>
+ <exclude>**/*edge.*</exclude>
</excludes>
<fileMode>0755</fileMode>
</fileSet>
diff --git a/distribution/src/assembly/confignode.xml
b/distribution/src/assembly/confignode.xml
index 6c3d04558eb..ee107cf17f5 100644
--- a/distribution/src/assembly/confignode.xml
+++ b/distribution/src/assembly/confignode.xml
@@ -44,6 +44,9 @@
<fileSet>
<outputDirectory>conf</outputDirectory>
<directory>${project.basedir}/../iotdb-core/node-commons/src/assembly/resources/conf</directory>
+ <excludes>
+ <exclude>edge/**</exclude>
+ </excludes>
</fileSet>
<fileSet>
<directory>${project.basedir}/../scripts/conf</directory>
diff --git a/distribution/src/assembly/datanode.xml
b/distribution/src/assembly/datanode.xml
index 225fa5a7e7d..d3996da9786 100644
--- a/distribution/src/assembly/datanode.xml
+++ b/distribution/src/assembly/datanode.xml
@@ -41,6 +41,9 @@
<fileSet>
<outputDirectory>conf</outputDirectory>
<directory>${project.basedir}/../iotdb-core/node-commons/src/assembly/resources/conf</directory>
+ <excludes>
+ <exclude>edge/**</exclude>
+ </excludes>
</fileSet>
<fileSet>
<directory>${project.basedir}/../scripts/conf</directory>
diff --git a/distribution/src/assembly/all.xml
b/distribution/src/assembly/edge.xml
similarity index 66%
copy from distribution/src/assembly/all.xml
copy to distribution/src/assembly/edge.xml
index c8b583de664..54c598e80b5 100644
--- a/distribution/src/assembly/all.xml
+++ b/distribution/src/assembly/edge.xml
@@ -20,12 +20,12 @@
-->
<assembly>
- <id>all-bin</id>
+ <id>edge-bin</id>
<formats>
<format>dir</format>
<format>zip</format>
</formats>
- <baseDirectory>apache-iotdb-${project.version}-all-bin</baseDirectory>
+ <baseDirectory>apache-iotdb-${project.version}-edge-bin</baseDirectory>
<dependencySets>
<dependencySet>
<includes>
@@ -46,27 +46,41 @@
</dependencySet>
</dependencySets>
<fileSets>
+ <!-- jmx credentials and the tool logback come from the datanode
resources;
+ logback-datanode.xml is replaced by the merged logback-edge.xml
below. -->
<fileSet>
<outputDirectory>conf</outputDirectory>
<directory>${project.basedir}/../iotdb-core/datanode/src/assembly/resources/conf</directory>
+ <excludes>
+ <exclude>logback-datanode.xml</exclude>
+ </excludes>
+ </fileSet>
+ <!-- iotdb-system.properties is replaced by the edge-tuned one below.
-->
+ <fileSet>
+ <outputDirectory>conf</outputDirectory>
+
<directory>${project.basedir}/../iotdb-core/node-commons/src/assembly/resources/conf</directory>
+ <excludes>
+ <exclude>iotdb-system.properties</exclude>
+ <exclude>edge/**</exclude>
+ </excludes>
</fileSet>
+ <!-- edge-specific system configuration. -->
<fileSet>
<outputDirectory>conf</outputDirectory>
-
<directory>${project.basedir}/../iotdb-core/confignode/src/assembly/resources/conf</directory>
+
<directory>${project.basedir}/../iotdb-core/node-commons/src/assembly/resources/conf/edge</directory>
</fileSet>
+ <!-- merged logback configuration for the single Edge process. -->
<fileSet>
<outputDirectory>conf</outputDirectory>
-
<directory>${project.basedir}/../iotdb-core/node-commons/src/assembly/resources/conf</directory>
+
<directory>${project.basedir}/src/assembly/resources/conf-edge</directory>
</fileSet>
<fileSet>
<outputDirectory>conf</outputDirectory>
<directory>${project.basedir}/../scripts/conf</directory>
- <excludes>
- <exclude>ainode-env.*</exclude>
- <exclude>**/ainode-env.*</exclude>
- <exclude>iotdb-common.sh</exclude>
- <exclude>**/iotdb-common.bat</exclude>
- </excludes>
+ <includes>
+ <include>edge-env.sh</include>
+ <include>windows/edge-env.bat</include>
+ </includes>
<fileMode>0755</fileMode>
</fileSet>
<fileSet>
@@ -79,21 +93,35 @@
<filtered>true</filtered>
<fileMode>0755</fileMode>
</fileSet>
+ <!-- one pair of start/stop scripts for the merged process, plus the
cli. -->
<fileSet>
<outputDirectory>sbin</outputDirectory>
<directory>${project.basedir}/../scripts/sbin</directory>
- <excludes>
- <exclude>*ainode.*</exclude>
- <exclude>**/*ainode.*</exclude>
- </excludes>
+ <includes>
+ <include>start-edge.sh</include>
+ <include>stop-edge.sh</include>
+ <include>start-cli.sh</include>
+ <include>windows/start-edge.bat</include>
+ <include>windows/check-edge.ps1</include>
+ <include>windows/stop-edge.bat</include>
+ <include>windows/start-cli.bat</include>
+ <include>windows/start-cli-table.bat</include>
+ </includes>
<fileMode>0755</fileMode>
</fileSet>
+ <!-- Keep node-independent tools. Edge does not package the standalone
node launchers or
+ environment files required by the excluded destructive, daemon,
and health scripts. -->
<fileSet>
<outputDirectory>tools</outputDirectory>
<directory>${project.basedir}/../scripts/tools</directory>
<excludes>
<exclude>*ainode.*</exclude>
<exclude>**/*ainode.*</exclude>
+ <exclude>ops/daemon-*.sh</exclude>
+ <exclude>ops/destroy-*.sh</exclude>
+ <exclude>ops/health_check.sh</exclude>
+ <exclude>windows/ops/destroy-*.bat</exclude>
+ <exclude>windows/ops/health_check.bat</exclude>
</excludes>
<fileMode>0755</fileMode>
</fileSet>
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 00000000000..3d36fbf1406
--- /dev/null
+++ b/distribution/src/assembly/resources/conf-edge/logback-edge.xml
@@ -0,0 +1,244 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ 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.
+
+-->
+<configuration scan="true" scanPeriod="60 seconds">
+ <jmxConfigurator/>
+ <!-- prevent logback from outputting its own status at the start of every
log -->
+ <statusListener class="ch.qos.logback.core.status.NopStatusListener"/>
+ <appender class="ch.qos.logback.core.rolling.RollingFileAppender"
name="FILEERROR">
+ <file>${IOTDB_HOME}/logs/log_edge_error.log</file>
+ <rollingPolicy
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+
<fileNamePattern>${IOTDB_HOME}/logs/log-edge-error-%d{yyyyMMdd}.log.gz</fileNamePattern>
+ <maxHistory>30</maxHistory>
+ </rollingPolicy>
+ <append>true</append>
+ <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+ <pattern>%d [%t] %-5p %C{25}:%L - %m %n</pattern>
+ <charset>utf-8</charset>
+ </encoder>
+ <filter class="ch.qos.logback.classic.filter.LevelFilter">
+ <level>error</level>
+ <onMatch>ACCEPT</onMatch>
+ <onMismatch>DENY</onMismatch>
+ </filter>
+ </appender>
+ <appender class="ch.qos.logback.core.rolling.RollingFileAppender"
name="FILEWARN">
+ <file>${IOTDB_HOME}/logs/log_edge_warn.log</file>
+ <rollingPolicy
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+
<fileNamePattern>${IOTDB_HOME}/logs/log-edge-warn-%d{yyyyMMdd}.log.gz</fileNamePattern>
+ <maxHistory>30</maxHistory>
+ </rollingPolicy>
+ <append>true</append>
+ <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+ <pattern>%d [%t] %-5p %C{25}:%L - %m %n</pattern>
+ <charset>utf-8</charset>
+ </encoder>
+ <filter class="ch.qos.logback.classic.filter.LevelFilter">
+ <level>WARN</level>
+ <onMatch>ACCEPT</onMatch>
+ <onMismatch>DENY</onMismatch>
+ </filter>
+ </appender>
+ <appender class="ch.qos.logback.core.rolling.RollingFileAppender"
name="FILEDEBUG">
+ <file>${IOTDB_HOME}/logs/log_edge_debug.log</file>
+ <rollingPolicy
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+
<fileNamePattern>${IOTDB_HOME}/logs/log-edge-debug-%d{yyyyMMdd}.log.gz</fileNamePattern>
+ <maxHistory>30</maxHistory>
+ </rollingPolicy>
+ <append>true</append>
+ <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+ <pattern>%d [%t] %-5p %C{25}:%L - %m %n</pattern>
+ <charset>utf-8</charset>
+ </encoder>
+ <filter class="ch.qos.logback.classic.filter.LevelFilter">
+ <level>DEBUG</level>
+ <onMatch>ACCEPT</onMatch>
+ <onMismatch>DENY</onMismatch>
+ </filter>
+ </appender>
+ <appender class="ch.qos.logback.core.rolling.RollingFileAppender"
name="FILETRACE">
+ <file>${IOTDB_HOME}/logs/log_edge_trace.log</file>
+ <rollingPolicy
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+
<fileNamePattern>${IOTDB_HOME}/logs/log-edge-trace-%d{yyyyMMdd}.log.gz</fileNamePattern>
+ <maxHistory>30</maxHistory>
+ </rollingPolicy>
+ <append>true</append>
+ <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+ <pattern>%d [%t] %-5p %C{25}:%L - %m %n</pattern>
+ <charset>utf-8</charset>
+ </encoder>
+ <filter class="ch.qos.logback.classic.filter.LevelFilter">
+ <level>TRACE</level>
+ <onMatch>ACCEPT</onMatch>
+ <onMismatch>DENY</onMismatch>
+ </filter>
+ </appender>
+ <appender class="ch.qos.logback.core.ConsoleAppender" name="stdout">
+ <Target>System.out</Target>
+ <encoder>
+ <pattern>%d [%t] %-5p %C{25}:%L - %m %n</pattern>
+ <charset>utf-8</charset>
+ </encoder>
+ <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+ <level>${CONSOLE_LOG_LEVEL:-DEBUG}</level>
+ </filter>
+ </appender>
+ <!-- a log appender that collect all log records whose level is greater
than debug-->
+ <appender class="ch.qos.logback.core.rolling.RollingFileAppender"
name="FILEALL">
+ <file>${IOTDB_HOME}/logs/log_edge_all.log</file>
+ <rollingPolicy
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+
<fileNamePattern>${IOTDB_HOME}/logs/log-edge-all-%d{yyyyMMdd}.log.gz</fileNamePattern>
+ <maxHistory>30</maxHistory>
+ </rollingPolicy>
+ <append>true</append>
+ <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+ <pattern>%d [%t] %-5p %C{25}:%L - %m %n</pattern>
+ <charset>utf-8</charset>
+ </encoder>
+ <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+ <level>INFO</level>
+ </filter>
+ </appender>
+ <appender class="ch.qos.logback.core.rolling.RollingFileAppender"
name="FILE_COST_MEASURE">
+ <file>${IOTDB_HOME}/logs/log_edge_measure.log</file>
+ <rollingPolicy
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+
<fileNamePattern>${IOTDB_HOME}/logs/log-edge-measure-%d{yyyyMMdd}.log.gz</fileNamePattern>
+ <maxHistory>30</maxHistory>
+ </rollingPolicy>
+ <append>true</append>
+ <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+ <pattern>%d [%t] %-5p %C{25}:%L - %m %n</pattern>
+ <charset>utf-8</charset>
+ </encoder>
+ <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+ <level>INFO</level>
+ </filter>
+ </appender>
+ <appender class="ch.qos.logback.core.rolling.RollingFileAppender"
name="QUERY_DEBUG">
+ <file>${IOTDB_HOME}/logs/log_edge_query_debug.log</file>
+ <rollingPolicy
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+
<fileNamePattern>${IOTDB_HOME}/logs/log-edge-query-debug-%d{yyyyMMdd}.log.gz</fileNamePattern>
+ <maxHistory>30</maxHistory>
+ </rollingPolicy>
+ <append>true</append>
+ <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+ <pattern>%d [%t] %C{25}:%L - %m %n</pattern>
+ <charset>utf-8</charset>
+ </encoder>
+ <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+ <level>INFO</level>
+ </filter>
+ </appender>
+ <appender class="ch.qos.logback.core.rolling.RollingFileAppender"
name="SLOW_SQL">
+ <file>${IOTDB_HOME}/logs/log_edge_slow_sql.log</file>
+ <rollingPolicy
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+
<fileNamePattern>${IOTDB_HOME}/logs/log-edge-slow-sql-%d{yyyyMMdd}.log.gz</fileNamePattern>
+ <maxHistory>30</maxHistory>
+ </rollingPolicy>
+ <append>true</append>
+ <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+ <pattern>%d [%t] %-5p %C{25}:%L - %m %n</pattern>
+ <charset>utf-8</charset>
+ </encoder>
+ <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+ <level>INFO</level>
+ </filter>
+ </appender>
+ <appender class="ch.qos.logback.core.rolling.RollingFileAppender"
name="SAMPLED_QUERIES">
+ <file>${IOTDB_HOME}/logs/log_edge_sampled_queries.log</file>
+ <rollingPolicy
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+
<fileNamePattern>${IOTDB_HOME}/logs/log-edge-sampled-queries-%d{yyyyMMdd}.log.gz</fileNamePattern>
+ <maxHistory>30</maxHistory>
+ </rollingPolicy>
+ <append>true</append>
+ <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+ <pattern>%d %m %n</pattern>
+ <charset>utf-8</charset>
+ </encoder>
+ <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+ <level>INFO</level>
+ </filter>
+ </appender>
+ <appender class="ch.qos.logback.core.rolling.RollingFileAppender"
name="COMPACTION">
+ <file>${IOTDB_HOME}/logs/log_edge_compaction.log</file>
+ <rollingPolicy
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+
<fileNamePattern>${IOTDB_HOME}/logs/log-edge-compaction-%d{yyyyMMdd}.log.gz</fileNamePattern>
+ <maxHistory>30</maxHistory>
+ </rollingPolicy>
+ <append>true</append>
+ <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+ <pattern>%d [%t] %-5p %C{25}:%L - %m %n</pattern>
+ <charset>utf-8</charset>
+ </encoder>
+ <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+ <level>INFO</level>
+ </filter>
+ </appender>
+ <appender class="ch.qos.logback.core.rolling.RollingFileAppender"
name="EXPLAIN_ANALYZE">
+ <file>${IOTDB_HOME}/logs/log_explain_analyze.log</file>
+ <rollingPolicy
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+
<fileNamePattern>${IOTDB_HOME}/logs/log-edge-explain-%d{yyyyMMdd}.log.gz</fileNamePattern>
+ <maxHistory>30</maxHistory>
+ </rollingPolicy>
+ <append>true</append>
+ <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+ <pattern>%d [%t] %-5p %C{25}:%L - %m %n</pattern>
+ <charset>utf-8</charset>
+ </encoder>
+ <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+ <level>INFO</level>
+ </filter>
+ </appender>
+ <root level="info">
+ <appender-ref ref="FILETRACE"/>
+ <appender-ref ref="FILEDEBUG"/>
+ <appender-ref ref="FILEWARN"/>
+ <appender-ref ref="FILEERROR"/>
+ <appender-ref ref="FILEALL"/>
+ <appender-ref ref="stdout"/>
+ </root>
+ <logger level="OFF" name="io.moquette.broker.metrics.MQTTMessageLogger"/>
+ <logger level="info" name="org.apache.iotdb.db.service"/>
+ <logger level="info" name="org.apache.iotdb.db.conf"/>
+ <logger level="info" name="org.apache.iotdb.db.cost.statistic">
+ <appender-ref ref="FILE_COST_MEASURE"/>
+ </logger>
+ <logger level="info" name="QUERY_DEBUG">
+ <appender-ref ref="QUERY_DEBUG"/>
+ </logger>
+ <logger level="info" name="SLOW_SQL" additivity="false">
+ <appender-ref ref="SLOW_SQL"/>
+ </logger>
+ <logger level="info" name="SAMPLED_QUERIES" additivity="false">
+ <appender-ref ref="SAMPLED_QUERIES"/>
+ </logger>
+ <logger level="info" name="QUERY_FREQUENCY"/>
+ <logger level="info" name="DETAILED_FAILURE_QUERY_TRACE"/>
+ <logger level="info" name="COMPACTION">
+ <appender-ref ref="COMPACTION"/>
+ </logger>
+ <logger level="info" name="org.apache.iotdb.pipe.api"/>
+ <logger level="info" name="org.apache.iotdb.db.pipe"/>
+ <logger level="info" name="org.apache.iotdb.commons.pipe"/>
+ <logger level="info" name="EXPLAIN_ANALYZE" additivity="false">
+ <appender-ref ref="EXPLAIN_ANALYZE"/>
+ </logger>
+</configuration>
diff --git a/integration-test/pom.xml b/integration-test/pom.xml
index 2152a5e1134..f6624fbd65d 100644
--- a/integration-test/pom.xml
+++ b/integration-test/pom.xml
@@ -30,6 +30,7 @@
<name>IoTDB: Integration-Test</name>
<properties>
<integrationTest.excludedGroups/>
+
<integrationTest.edgePackage>${maven.multiModuleProjectDirectory}/distribution/target/apache-iotdb-${project.version}-edge-bin.zip</integrationTest.edgePackage>
<integrationTest.forkCount>1</integrationTest.forkCount>
<integrationTest.includedGroups/>
<integrationTest.launchNodeInSameJVM>true</integrationTest.launchNodeInSameJVM>
@@ -333,6 +334,7 @@
<forkCount>${integrationTest.forkCount}</forkCount>
<reuseForks>false</reuseForks>
<systemPropertyVariables>
+
<EdgePackage>${integrationTest.edgePackage}</EdgePackage>
<TestEnv>${integrationTest.testEnv}</TestEnv>
<RandomSelectWriteNode>${integrationTest.randomSelectWriteNode}</RandomSelectWriteNode>
<ReadAndVerifyWithMultiNode>${integrationTest.readAndVerifyWithMultiNode}</ReadAndVerifyWithMultiNode>
@@ -447,6 +449,20 @@
</plugins>
</build>
<profiles>
+ <profile>
+ <id>EdgeIT</id>
+ <activation>
+ <activeByDefault>false</activeByDefault>
+ </activation>
+ <properties>
+
<integrationTest.excludedGroups>org.apache.iotdb.itbase.category.ManualIT</integrationTest.excludedGroups>
+
<integrationTest.includedGroups>org.apache.iotdb.itbase.category.EdgeIT</integrationTest.includedGroups>
+
<integrationTest.launchNodeInSameJVM>true</integrationTest.launchNodeInSameJVM>
+
<integrationTest.randomSelectWriteNode>false</integrationTest.randomSelectWriteNode>
+
<integrationTest.readAndVerifyWithMultiNode>false</integrationTest.readAndVerifyWithMultiNode>
+ <integrationTest.testEnv>Simple</integrationTest.testEnv>
+ </properties>
+ </profile>
<profile>
<id>SimpleIT</id>
<activation>
diff --git a/integration-test/src/assembly/mpp-test.xml
b/integration-test/src/assembly/mpp-test.xml
index 58bb8da0e15..e6fa76d31d8 100644
--- a/integration-test/src/assembly/mpp-test.xml
+++ b/integration-test/src/assembly/mpp-test.xml
@@ -37,6 +37,9 @@
<fileSet>
<outputDirectory>conf</outputDirectory>
<directory>${project.basedir}/../iotdb-core/node-commons/src/assembly/resources/conf</directory>
+ <excludes>
+ <exclude>edge/**</exclude>
+ </excludes>
</fileSet>
<fileSet>
<outputDirectory>conf</outputDirectory>
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 00000000000..f70516bd111
--- /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 00000000000..8bfaf3b0ebd
--- /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<String> 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<String, String> 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<String> replacedKeys = new HashSet<>();
+ final List<String> 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<String> 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<Path> paths = Files.list(extractionDir)) {
+ final List<Path> 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<String> 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<Path> 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 c4832cd886c..800d59bf727 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 @@ public final class 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 3029154ab3e..14713cbb801 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 @@ public final class 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 00000000000..04396a0be30
--- /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).
+ *
+ * <p>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.
+ *
+ * <p>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<Throwable> 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<Throwable> 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<Throwable> 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 00000000000..13c87b5295b
--- /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 00000000000..627623047d4
--- /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 00000000000..fccaebcef12
--- /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 00000000000..be1a70ea6f7
--- /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 00000000000..8f8f98ac64b
--- /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 00000000000..f6f9037bb54
--- /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 00000000000..af5f7cb5ca2
--- /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 00000000000..4eed010be62
--- /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 [...]
+pause