Add iotdb-thingsboard-table module for ThingsBoard on IoTDB Table Mode#110
Add iotdb-thingsboard-table module for ThingsBoard on IoTDB Table Mode#110PDGGK wants to merge 2 commits into
Conversation
Implements ThingsBoard's historical telemetry TimeseriesDao SPI on Apache IoTDB 2.0.8 Table Mode (ITableSession SQL + Tablet writes). This first PR delivers the write + raw-read foundation: IoTDBTableBaseDao, an async bounded-queue batch writer, and the raw findAllAsync/remove read-delete path. Aggregation, latest telemetry and attributes land in later PRs. The module is inert by default: the live DAO/pool/writer/schema-bootstrap activate only on an explicit database.ts.type=iotdb-table plus iotdb.ts.experimental-raw-only=true opt-in, the auto-configuration is classpath-isolated from a non-ThingsBoard runtime, and ThingsBoard SPI types are a compile-only source surface excluded from the built jar (Strategy F). It is added to the reactor through a JDK-17 profile and overrides tsfile only within its own module pom, so the rest of the reactor is unaffected. Signed-off-by: Zihan Dai <99155080+PDGGK@users.noreply.github.com>
There was a problem hiding this comment.
Thanks for this well-structured first PR of the GSoC series. The concurrency design (writer drain/back-pressure, future completion on all paths), the Spring activation isolation, and the DDL injection defense are all solid. Below are the findings — there are a few build-layer issues that should be resolved before merge, two of which conflict with the repo's CLAUDE.md conventions.
感谢这个结构清晰的 GSoC 系列首期 PR。并发设计(writer 的 drain/背压、所有路径上的 future 完成)、Spring 激活隔离、以及 DDL 注入防御都做得很扎实。以下是审查发现——其中有几个构建层问题建议在合并前解决,有两个与仓库 CLAUDE.md 约定直接冲突。
🔴 Blocking / 需在合并前解决
1. Module-local override of tsfile.version / iotdb.version violates CLAUDE.md and risks a reactor-wide convergence failure
iotdb-thingsboard-table/pom.xml overrides iotdb.version 2.0.5 → 2.0.8 and tsfile.version 2.1.1 → 2.3.0. CLAUDE.md explicitly states "do not bump iotdb.version / tsfile.version in isolation". The PR argues the override is module-local, but because the module is auto-activated on JDK 17+ (see #3), it shares the reactor with the other connectors (tsfile 2.1.1). The moment anyone runs mvn -P enforce, dependencyConvergence will fail on two coexisting tsfile versions in one reactor. CI does not enable enforcer today (enforcer.skip=true by default), so it is green now — but the release flow and local -P enforce validation will break.
iotdb-thingsboard-table/pom.xml 把 iotdb.version 2.0.5→2.0.8、tsfile.version 2.1.1→2.3.0。CLAUDE.md 明确写着"不要孤立 bump iotdb.version / tsfile.version"。PR 辩称这是模块本地覆盖,但由于该模块在 JDK 17+ 上自动激活(见 #3),它会与其它连接器(tsfile 2.1.1)同处一个 reactor。一旦有人执行 mvn -P enforce,dependencyConvergence 会因同一 reactor 中 tsfile 存在两个版本而失败。目前 CI 未启用 enforcer(默认 enforcer.skip=true),所以是绿的——但发布流程和本地 -P enforce 验证都会失败。
2. jakarta.validation-api jumps 2.0.2 → 3.0.2 — a cross-namespace (javax.* → jakarta.*) break
The parent is Spring Boot 2.7.18 / Spring 5.3.39 / jakarta.validation-api 2.0.2 (javax.validation namespace). The module overrides it to 3.0.2 (jakarta.validation namespace) while still inheriting Spring 5.3 / Boot 2.7. This is the javax.* → jakarta.* generational rename and is incompatible with the javax.validation the inherited Spring 5.x / Boot 2.7 expects. This is the most subtle and most serious version issue in the PR — please align with the parent's javax namespace, or justify why the module can safely diverge.
父 pom 是 Spring Boot 2.7.18 / Spring 5.3.39 / jakarta.validation-api 2.0.2(javax.validation 命名空间)。模块却将其覆盖到 3.0.2(jakarta.validation 命名空间),同时仍继承 Spring 5.3 / Boot 2.7。这是 javax.*→jakarta.* 的跨代重命名,与所继承的 Spring 5.x / Boot 2.7 期望的 javax.validation 不兼容。这是本 PR 中最隐蔽也最严重的版本问题——请说明模块为何能安全地分叉。
3. The module is wired into the root reactor via a <jdk>[17,)</jdk> auto-activated profile, diverging from the connector convention
The repo convention is a two-layer explicit opt-in (a connector goes into a named profile in connectors/pom.xml, and the root pom pulls in connectors). This PR attaches the module directly to the root pom with JDK auto-activation, so any mvn clean verify on a JDK 17+ machine silently pulls in this version-overriding module with no -P flag. That is the exact channel by which the #1/#2 version conflicts propagate. Please switch to an explicit named profile (cf. how iotdb-spring-boot-starter uses with-springboot). Behavior on Jenkins JDK 11 is fine ([17,) does not match, module skipped).
仓库惯例是两层显式 opt-in(连接器进 connectors/pom.xml 的具名 profile,再由根 pom 引入 connectors)。本 PR 把模块直接挂在根 pom 上并用 JDK 自动激活,因此任何 JDK 17+ 机器上的 mvn clean verify 都会无声地把这个携带版本覆盖的模块拉进构建,且无需任何 -P 参数。这正是 #1/#2 版本冲突得以扩散的传导路径。请改用显式具名 profile(参考 iotdb-spring-boot-starter 的 with-springboot 写法)。Jenkins JDK 11 上行为正常([17,) 不匹配,模块被跳过)。
🟠 Recommended / 建议修复
4. Writer retry classification is too narrow — a transient IoTDB outage drops a whole batch. IoTDBTableTimeseriesWriter.insertWithRetry only retries IoTDBConnectionException; every StatementExecutionException is treated as permanent and fails the entire batch (potentially hundreds of points) without retry. The class name and the retryMaxAttempts config imply broader coverage than is delivered. Please distinguish transient vs. permanent failures, or state the limitation explicitly in the README.
写入器重试分类过窄——IoTDB 瞬时不可用会丢整批。insertWithRetry 只对 IoTDBConnectionException 重试,StatementExecutionException 一律当作永久失败、直接 fail 掉整个 batch(可能含数百个点)。类名与 retryMaxAttempts 配置暗示的覆盖面比实际更广。请区分瞬时/永久失败,或在 README 显式声明此限制。
5. Schema bootstrap detects "already exists" via error-message string matching. IoTDBTableSchemaBootstrap.isAlreadyExists relies on message.contains("already exist" / "has already been"). Any change to the server wording, version, or i18n leads to either a false positive (swallowing a real DDL failure — a silent-failure anti-pattern) or a false negative (breaking idempotency). Prefer a structured IoTDB error/status code, or CREATE TABLE IF NOT EXISTS if Table Mode supports it.
schema bootstrap 用错误信息字符串匹配判断"已存在"。isAlreadyExists 依赖 message.contains("already exist" / "has already been")。服务端文案、版本或 i18n 的任何变化都会导致误判(吞掉真实的 DDL 失败——silent-failure 反模式)或漏判(破坏幂等性)。建议改用 IoTDB 结构化错误/状态码,或在 Table Mode 支持时使用 CREATE TABLE IF NOT EXISTS。
6. Missing an integration test that boots without ThingsBoard on the classpath. The core selling point is "completely inert / no NoClassDefFoundError on a non-ThingsBoard classpath". The @ConditionalOnClass(name=...) string form is correct, but whole-chain isolation depends on whether IoTDBTableTimeseriesDao et al. reference org.thingsboard.* on a non-conditional path. This can currently only be inferred statically — a context-startup test on a ThingsBoard-free classpath would be the authoritative evidence.
缺少"无 ThingsBoard 类路径"下启动的集成测试。核心卖点是"非 ThingsBoard 类路径上完全 inert、不抛 NoClassDefFoundError"。@ConditionalOnClass(name=...) 字符串写法本身正确,但整链隔离取决于 IoTDBTableTimeseriesDao 等是否在非条件路径上引用了 org.thingsboard.*。目前只能静态推断——一个在无 ThingsBoard jar 的类路径上启动最小上下文的测试,才是权威证据。
7. Clarify the target Spring Boot version for the spring.factories + AutoConfiguration.imports dual registration. With the Boot 3.x @AutoConfiguration annotation in play, the Boot 2.x EnableAutoConfiguration key in spring.factories is effectively dead on Boot 3.x and only adds confusion. Please state ThingsBoard's actual Boot version so the redundant registration can be removed or justified.
请澄清 spring.factories + AutoConfiguration.imports 双注册所针对的 Spring Boot 版本。既然用了 Boot 3.x 的 @AutoConfiguration 注解,spring.factories 里 Boot 2.x 的 EnableAutoConfiguration 键在 Boot 3.x 上实际失效、只会增加困惑。请说明 ThingsBoard 实际的 Boot 版本,以便删除冗余注册或给出理由。
🟡 Minor / 小问题
IoTDBTableTimeseriesDaodataPointDaysusesMath.toIntExact, which can throwArithmeticExceptionon an accounting overflow and fail a telemetry write — prefer saturating. /dataPointDays用Math.toIntExact,会计数字溢出时抛ArithmeticException而使写入失败,建议饱和处理。IoTDBTableConfig.flushThreadsuses@Min(1) @Max(1)to mean "must be 1" — an anti-pattern with no custom validation message; either drop the config knob or give it a clear message. /flushThreads用@Min(1) @Max(1)表达"只能为 1"是反模式且无自定义校验消息,建议移除该配置项或加明确消息。guava/mockitoare also overridden locally — please justify or align with the parent. /guava/mockito也做了本地覆盖,请说明必要性或对齐父版本。defaultTtlMs: the log key and field name diverge and may suggest it controls IoTDB physical retention, which it does not. /defaultTtlMs的日志键名与字段名不一致,易让人误以为能控制 IoTDB 物理保留期。CI-NOTES.mddoes not mention the version-conflict side effect of participating as a reactor submodule. /CI-NOTES.md未提及作为 reactor 子模块参与构建时触发的版本冲突副作用。
✅ What's done well / 做得好的地方
- All
CompletableFuture/SettableFuturepaths complete correctly (success / failure / reject / shutdown) — no hung futures. / 所有 future 路径都正确完成,无悬挂。 - DDL injection defense is robust: the database name is validated at two layers (
@Pattern+ bootstrap regex), and Java's^...$(no MULTILINE) correctly rejects newline/semicolon/space injection attempts. / DDL 注入防御扎实:数据库名双层校验,Java^...$(无 MULTILINE)正确拒绝换行/分号/空格等注入。 - Activation condition is tight (dual switch, case/whitespace-insensitive), inert by default. / 激活条件严密,默认完全 inert。
- Not interrupting the worker on shutdown, the
insertedflag avoiding replay whenclose()throws after a successful insert, and theacceptedSavesregistry guaranteeing each future completes exactly once on shutdown — all handled correctly. / shutdown 不打断 worker、inserted标志避免重放、acceptedSaves保证 shutdown 时每个 future 恰好完成一次——都处理对了。 - Apache license headers are complete across all new sources including the
src/providedstubs. / 所有新增源文件(含src/providedstub)许可证头完整。
Resolves the review on apache#110. - Version strategy: drop the module-local iotdb / tsfile / guava overrides. The module now inherits the parent reactor's iotdb-session 2.0.5, tsfile 2.1.1 and guava 32.1.2-jre (a single tsfile across the reactor) and uses the 2.0.5 enableCompression builder API. jakarta.validation-api stays at 3.0.2 (the ThingsBoard 4.3.x Spring Boot 3 / jakarta runtime namespace), now made explicit at the dependency and documented. - Reactor wiring: replace the <jdk>[17,)</jdk> auto-activated profile with a named with-thingsboard opt-in profile; build and test it on the JDK 17+ CI jobs via -P with-thingsboard. - Writer: retry only transient StatementExecutionException status codes; fail fast on permanent ones. - Schema bootstrap: use CREATE TABLE IF NOT EXISTS and detect already-exists via the structured IoTDB status code (message-substring match as fallback). - Auto-configuration: state ThingsBoard's Spring Boot 3.5.x version in the docs; keep the spring.factories entry only for Boot 2.7 portability. - Tests: add a no-ThingsBoard-classpath startup test, retry classification tests, a status-code idempotency test, and a @Validated-through-Spring test. - Minor: saturate dataPointDays; clear flushThreads validation message; align the defaultTtlMs log key; remove a useless null check. Signed-off-by: Zihan Dai <99155080+PDGGK@users.noreply.github.com>
|
Thanks for the very detailed review — the version-layer findings in particular were spot on. Here is what I changed, point by point. Verified locally with a cache-off full build: 79 unit tests + 9 Testcontainers ITs (against 🔴 1 — version overrides / reactor convergenceI removed all of the iotdb / tsfile / guava overrides. The module now inherits the parent reactor's The only code change this required was switching the builder call back to I deliberately did not keep One honest note on 🔴 2 — jakarta.validation-api 3.0.2I kept 3.0.2 and took the "justify the divergence" path you offered. The deployment host is ThingsBoard 4.3.1.2, which runs on Spring Boot 3.5.14 / Spring 6 / JDK 17 (from its own pom) — i.e. the You implicitly surfaced a real gap here, which I also fixed: the previous tests only exercised a hand-built validator. I added a test that drives Spring's real binding/validation path ( 🔴 3 — explicit named profileDone exactly as your inline comments: the root-pom profile is now One consequence worth flagging: now that it is no longer JDK-auto-activated, the module is built only by workflows that pass 🟠 4 — retry classification
🟠 5 — schema idempotencyThe DDL now uses 🟠 6 — no-ThingsBoard startup testAdded a context-startup test that boots the whole 🟠 7 — dual registration / Boot versionThingsBoard's actual Spring Boot version is 3.5.14 (from TB 4.3.1.2's pom), so 🟡 minor
Thanks again — happy to iterate on any of these. |
What this adds
A new Maven module
iotdb-thingsboard-tablethat implements ThingsBoard's historical telemetry DAO SPI on top of Apache IoTDB 2.0.8 Table Mode (relational SQL viaITableSession/ Tablet writes). This is the first PR of a staged series; it delivers the write + raw-read foundation, packaged so it activates only on an explicit opt-in and otherwise stays completely inert.DAO implementation
IoTDBTableBaseDao—ITableSessionPoolwiring (Spring constructor injection),iotdb.*configuration binding, andgetEntry()mapping of the five typed columns (bool_v/long_v/double_v/str_v/json_v) with fail-fast on the single-typed-column schema invariant.IoTDBTableTimeseriesDao.save()— an async bounded-queue batch writer: rows are mapped to a sparse IoTDB Tablet (TAG columns declared low-cardinality-first —entity_type, tenant_id, key, entity_id— plus one populated typed FIELD perDataType), flushed by size/linger, with retry+backoff on connection errors, reject-on-full back-pressure (fails the future rather than blocking the caller, with reject counters and a rate-limited WARN — at most one per 10s with cumulative counts — so dropped points are never silent), and a graceful shutdown drain.findAllAsync(raw) +remove+savePartition— the non-aggregated read path (half-open ranges, escaped key/order, rows mapped back toBasicTsKvEntry), the delete path, and the partition no-op, all on a bounded read thread pool. Per the ThingsBoardAbstractChunkedAggregationTimeseriesDaocontract, a query is routed to raw whenaggregation == NONE || interval < 1.IoTDBTableTimeseriesDaothrowsUnsupportedOperationExceptionand is unreachable by default thanks to the explicit raw-only opt-in.IoTDBTableLatestDao/IoTDBTableAttributesDaoship only as unregistered inert skeletons (not Spring beans, no ThingsBoard interface binding), so no configuration selector can route traffic to a non-working DAO.Activation & runtime (inert by default, opt-in foundation)
IoTDBTableConfigurationis an@AutoConfigurationregistered viaMETA-INF/spring/...AutoConfiguration.imports+META-INF/spring.factories. The outer class carries a string-based@ConditionalOnClass(name="org.thingsboard.server.dao.timeseries.TimeseriesDao")and contains no bean method or annotation that force-loads a ThingsBoard type, so on a non-ThingsBoard classpath Spring evaluates the condition from ASM metadata and skips the module without aNoClassDefFoundError. All ThingsBoard-referencing beans live in a nested@Configuration;@ConditionalOnMissingBean(type=...)uses the string form.TimeseriesDao, session pool, writer and schema bootstrap activate only when BOTHdatabase.ts.type=iotdb-tableANDiotdb.ts.experimental-raw-only=trueare set. A normal deployment (selector alone, or neither) gets nothing, so the not-yet-implemented aggregation path is never reachable through the public SPI. Activation uses a small case-insensitiveCondition, not SpEL.ITableSessionPoolcan never silently substitute for it.TimeseriesDao, aBeanFactoryPostProcessorfails startup with a clear message (trusting only resolvable bean types, fail-closed) rather than leaving the pool/bootstrap running while the DAO is shadowed.IoTDBTableSchemaBootstrap— creates the IoTDB database/table on first activation (same opt-in guard +@ConditionalOnProperty(iotdb.schema.bootstrap, matchIfMissing=true)), so the first write does not fail on a missing table. The configured database name is validated before it is spliced into DDL.iotdb.*config is bound/validated only when the backend is selected (@EnableConfigurationPropertiessits on the conditional nested config), so an unrelated host with strayiotdb.*properties is unaffected.ThingsBoard compile surface (Strategy F)
ThingsBoard's
dao/commonartifacts are not published to Maven Central, so the SPI/value types the module compiles against are provided as a compile-only source surface undersrc/provided/javaand excluded from the built jar (org/thingsboard/**,org/apache/commons/**); at runtime the real ThingsBoard classpath provides them. The surface is kept in sync with ThingsBoard v4.3.1.2 (each stub file carries a provenance header with the verified version + date), andStrategyFContractTestpins the exactTimeseriesDaoSPI signatures the DAO depends on so any accidental drift fails the build. Integration tests run againsttarget/classes(which retains the compile surface).Reactor / build impact
<jdk>[17,)</jdk>(it uses Java-17 language features), so existing JDK 8/11 root builds skip it and only 17/21 jobs build it — no change to current CI on older JDKs.tsfile.versionis left untouched at2.1.1. The iotdb-session 2.0.8 Tablet write API needs tsfile2.3.0, so the bump is a module-local property override iniotdb-thingsboard-table/pom.xml— only this module resolves the newer tsfile; every other connector keeps2.1.1, so there is zero blast radius on the rest of the reactor.-Piotdb-table-itprofile, so a defaultmvn installruns unit tests only and does not require Docker.Tests
73 unit tests (type mapping, batch flush/linger, back-pressure, retry, shutdown drain + forced-stop settle guarantees, reject-WARN rate limiting, raw read SQL/mapping, half-open delete, blank-key fail-fast, read-pool reject/drain, auto-config discovery + classpath isolation, named-pool ownership, conflict-guard fail-fast, schema bootstrap + DDL column-order pins, Strategy-F contract) plus 9 Testcontainers integration tests against a real
apache/iotdb:2.0.8-standalonecontainer (round-trip all five types, order/limit, half-open end, scoped delete, key escaping, fresh-database bootstrap through a database-bound pool).mvn apache-rat:checkis clean.Known limitation (documented, in the active path)
This affects the write + raw-read surface delivered here, not a deferred placeholder: if the same
(tenant, entity, key, ts)point has its data type changed across two separate flushes, the two typed columns coexist on that row, and the raw read fails fast on the single-typed-column invariant (IoTDBTableBaseDao.getEntry) rather than silently returning a wrong value. Within a single flush the writer already de-duplicates such a type change. Full delete-then-insert reconciliation across flushes is deferred to a later PR in the series. The behaviour is pinned by a unit test and an integration test and documented in the module README.Context
This module is the first deliverable of a Google Summer of Code 2026 project to add an Apache IoTDB 2.x Table Mode storage backend to ThingsBoard. The design (schema, current-state analysis, Spring activation, and the staged-PR plan) was shared and discussed earlier on the
dev@iotdb.apache.orglist.