Skip to content

Add iotdb-thingsboard-table module for ThingsBoard on IoTDB Table Mode#110

Open
PDGGK wants to merge 2 commits into
apache:masterfrom
PDGGK:pr1
Open

Add iotdb-thingsboard-table module for ThingsBoard on IoTDB Table Mode#110
PDGGK wants to merge 2 commits into
apache:masterfrom
PDGGK:pr1

Conversation

@PDGGK

@PDGGK PDGGK commented Jun 14, 2026

Copy link
Copy Markdown

What this adds

A new Maven module iotdb-thingsboard-table that implements ThingsBoard's historical telemetry DAO SPI on top of Apache IoTDB 2.0.8 Table Mode (relational SQL via ITableSession / 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

  • IoTDBTableBaseDaoITableSessionPool wiring (Spring constructor injection), iotdb.* configuration binding, and getEntry() 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 per DataType), 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 to BasicTsKvEntry), the delete path, and the partition no-op, all on a bounded read thread pool. Per the ThingsBoard AbstractChunkedAggregationTimeseriesDao contract, a query is routed to raw when aggregation == NONE || interval < 1.
  • Aggregation, latest telemetry and attributes arrive in later PRs. The not-yet-implemented positive-interval aggregation path in IoTDBTableTimeseriesDao throws UnsupportedOperationException and is unreachable by default thanks to the explicit raw-only opt-in. IoTDBTableLatestDao / IoTDBTableAttributesDao ship 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)

  • Spring auto-configuration with classpath isolationIoTDBTableConfiguration is an @AutoConfiguration registered via META-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 a NoClassDefFoundError. All ThingsBoard-referencing beans live in a nested @Configuration; @ConditionalOnMissingBean(type=...) uses the string form.
  • Inert until explicitly opted in — because this PR delivers only the write + raw-read path (aggregation lands in a later PR), the live TimeseriesDao, session pool, writer and schema bootstrap activate only when BOTH database.ts.type=iotdb-table AND iotdb.ts.experimental-raw-only=true are 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-insensitive Condition, not SpEL.
  • Module-owned session pool — the module registers its session pool under a dedicated bean name and injects it everywhere by qualifier, so a host-provided ITableSessionPool can never silently substitute for it.
  • No silent half-activation — if the backend is enabled but the host already provides a conflicting non-IoTDB TimeseriesDao, a BeanFactoryPostProcessor fails 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 (@EnableConfigurationProperties sits on the conditional nested config), so an unrelated host with stray iotdb.* properties is unaffected.

ThingsBoard compile surface (Strategy F)

ThingsBoard's dao/common artifacts are not published to Maven Central, so the SPI/value types the module compiles against are provided as a compile-only source surface under src/provided/java and 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), and StrategyFContractTest pins the exact TimeseriesDao SPI signatures the DAO depends on so any accidental drift fails the build. Integration tests run against target/classes (which retains the compile surface).

Reactor / build impact

  • The module is added to the root reactor through a profile activated by <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.
  • The parent tsfile.version is left untouched at 2.1.1. The iotdb-session 2.0.8 Tablet write API needs tsfile 2.3.0, so the bump is a module-local property override in iotdb-thingsboard-table/pom.xml — only this module resolves the newer tsfile; every other connector keeps 2.1.1, so there is zero blast radius on the rest of the reactor.
  • The module's Testcontainers integration tests run only under an explicit -Piotdb-table-it profile, so a default mvn install runs 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-standalone container (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:check is 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.org list.

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>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, this is your first pull request in IoTDB project. Thanks for your contribution! IoTDB will be better because of you.

Comment thread pom.xml Outdated
<profile>
<id>iotdb-thingsboard-table-jdk17</id>
<activation>
<jdk>[17,)</jdk>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove activation

Comment thread pom.xml Outdated
while 17/21 still build and test it as part of the reactor.
-->
<profile>
<id>iotdb-thingsboard-table-jdk17</id>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with-thingsboard

@CritasWang CritasWang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.xmliotdb.version 2.0.5→2.0.8tsfile.version 2.1.1→2.3.0CLAUDE.md 明确写着"不要孤立 bump iotdb.version / tsfile.version"。PR 辩称这是模块本地覆盖,但由于该模块在 JDK 17+ 上自动激活(见 #3),它会与其它连接器(tsfile 2.1.1)同处一个 reactor。一旦有人执行 mvn -P enforcedependencyConvergence 会因同一 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-starterwith-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 / 小问题

  • IoTDBTableTimeseriesDao dataPointDays uses Math.toIntExact, which can throw ArithmeticException on an accounting overflow and fail a telemetry write — prefer saturating. / dataPointDaysMath.toIntExact,会计数字溢出时抛 ArithmeticException 而使写入失败,建议饱和处理。
  • IoTDBTableConfig.flushThreads uses @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 / mockito are 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.md does not mention the version-conflict side effect of participating as a reactor submodule. / CI-NOTES.md 未提及作为 reactor 子模块参与构建时触发的版本冲突副作用。

✅ What's done well / 做得好的地方

  • All CompletableFuture / SettableFuture paths 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 inserted flag avoiding replay when close() throws after a successful insert, and the acceptedSaves registry 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/provided stubs. / 所有新增源文件(含 src/provided stub)许可证头完整。

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>
@PDGGK

PDGGK commented Jun 24, 2026

Copy link
Copy Markdown
Author

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 apache/iotdb:2.0.8-standalone), plus apache-rat, checkstyle and spotless all clean.

🔴 1 — version overrides / reactor convergence

I removed all of the 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 — exactly like the sibling spark/flink connectors — so there is a single tsfile version across the reactor and the convergence risk you flagged is gone (confirmed via dependency:tree).

The only code change this required was switching the builder call back to TableSessionPoolBuilder.enableCompression(...) (the 2.0.5 method name). I verified the full write path still works at runtime: the 9 container ITs (including a 500-point mixed-type single-flush) pass against the apache/iotdb:2.0.8-standalone server, so the 2.0.5 client / 2.0.8 server RPC pairing is fine.

I deliberately did not keep iotdb-session 2.0.8 and only drop tsfile: I tested that combination and iotdb-session 2.0.8 was compiled against tsfile 2.2.1 and calls TSFileConfig.getValueEncoder(...) at runtime, which does not exist in tsfile 2.1.1 — a large flush fails with NoSuchMethodError. Keeping 2.0.8 would therefore force a tsfile override again, i.e. reintroduce exactly the divergence you raised. Inheriting 2.0.5/2.1.1 is the clean resolution.

One honest note on -P enforce: after this change the tsfile conflict is gone, but dependencyConvergence still reports one pre-existing finding on org.apache.httpcomponents:httpcore (4.4.12 vs 4.4.16). It comes transitively through iotdb-session 2.0.5 → libthrift 0.14.1 and is shared by every iotdb-session consumer in the reactor (spark, flink, spring-boot-starter, collector, examples) — the parent pins httpclient but not httpcore. It is not introduced by this module and is best resolved at the parent level; I'm happy to file a separate small PR adding httpcore to the parent dependencyManagement if you'd like.

🔴 2 — jakarta.validation-api 3.0.2

I 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 jakarta.* namespace. The module is compiled to deploy into that host, so jakarta.validation.* (3.0.2) is the correct namespace; downgrading to javax.validation 2.0.2 would break validation at the real runtime. The annotations are provided-scope and the Hibernate Validator provider is supplied by the ThingsBoard runtime, not bundled here. I made the override explicit at the dependency and documented the rationale in the pom.

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 (Binder + ValidationBindHandler) and asserts a bad bound value is rejected with a BindValidationException, with a paired positive case so the rejection is attributable to the constraint, not the wiring.

🔴 3 — explicit named profile

Done exactly as your inline comments: the root-pom profile is now with-thingsboard, the <jdk>[17,)</jdk> activation is removed, and CI activates it on the JDK 17+ jobs via -P with-thingsboard in compile-check.yml (the 8/11 jobs omit the flag and skip it). This mirrors with-springboot / iotdb-spring-boot-starter.

One consequence worth flagging: now that it is no longer JDK-auto-activated, the module is built only by workflows that pass -P with-thingsboard. I added that to compile-check.yml (build + tests on 17/21), but code-analysis.yml (CodeQL autobuild) and dependency-check.yml (SBOM) don't pass connector profiles, so this module is no longer included there — which matches the other opt-in modules (with-springboot, with-examples). Happy to add -P with-thingsboard to those workflows if you'd prefer to keep this module under CodeQL/SBOM coverage; I left them as-is to stay consistent with the existing convention.

🟠 4 — retry classification

insertWithRetry now also catches StatementExecutionException and retries only transient server-side conditions, keyed on TSStatusCode (WRITE_PROCESS_REJECT, INTERNAL_REQUEST_TIME_OUT, INTERNAL_REQUEST_RETRY_ERROR, TOO_MANY_CONCURRENT_QUERIES_ERROR, DISPATCH_ERROR). Permanent failures (parse / type / schema) fail fast. Added two tests asserting the exact insert invocation counts for a transient vs a permanent code.

🟠 5 — schema idempotency

The DDL now uses CREATE TABLE IF NOT EXISTS (valid on 2.0.8 Table Mode — exercised by the IT), so the normal re-run returns SUCCESS without throwing. As defense-in-depth, isAlreadyExists now checks the structured status code (TABLE_ALREADY_EXISTS / DATABASE_ALREADY_EXISTS / COLUMN_ALREADY_EXISTS) over the cause chain, with the message-substring match kept only as a last-resort fallback. Added a test proving the code-driven path (an already-exists code with a non-matching message is still tolerated).

🟠 6 — no-ThingsBoard startup test

Added a context-startup test that boots the whole @Import(IoTDBTableConfiguration.class) chain under a FilteredClassLoader("org.thingsboard") with the selector enabled, and asserts the context starts cleanly (hasNotFailed()), creates no module beans, and that the ThingsBoard class is genuinely absent (ClassNotFoundException). I also strengthened the existing discovery test with a positive hasNotFailed() assertion. (It's a context-slice test rather than a full @SpringBootTest, but it drives the real activation path with the class actually hidden.)

🟠 7 — dual registration / Boot version

ThingsBoard's actual Spring Boot version is 3.5.14 (from TB 4.3.1.2's pom), so @AutoConfiguration + AutoConfiguration.imports is the active and correct mechanism. I kept the spring.factories EnableAutoConfiguration entry only for Boot-2.7 portability (it is ignored on Boot 3.x) and corrected the javadoc/README to state this clearly rather than calling it a vague "fallback".

🟡 minor

  • dataPointDays now saturates at Integer.MAX_VALUE instead of Math.toIntExact throwing.
  • flushThreads keeps the exact-1 constraint (so an unsupported value fails fast) but now carries a clear custom validation message + javadoc, instead of a bare @Min(1)@Max(1).
  • guava override dropped (inherits the parent). mockito override kept (this module is JUnit 5 / JDK 17 only and needs mockito 5.x) with a comment explaining the divergence from the parent's PowerMock-pinned 2.x.
  • defaultTtlMs log key aligned to the field name.
  • CI-NOTES.md updated (version policy + the pre-existing httpcore note).
  • CodeQL: removed the useless null check. The "start of thread in constructor" note is a deliberate, safe design — all worker-visible fields are final and assigned before the thread starts, and the package-private startWorker flag keeps construction and start separable (the unit tests construct the writer without starting it); the class is not intended for subclassing, so I kept it as-is. The "useless parameter" alerts are on the src/provided SPI stubs, whose signatures must mirror the upstream ThingsBoard interfaces, so those stay too.

Thanks again — happy to iterate on any of these.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants