ZH Writes and Errors - AlanDevise/TsGate GitHub Wiki
2.1.3 已通过 GitHub 源码/标签和 Maven Central 发布,同 SHA CI 与公开制品核验通过。 当前坐标为 2.1.3;详见 2.1.3 发布说明.
2.1.3 收紧结构化过滤参数校验,在整批预检前归并 IoTDB 物理表名的大小写变体,在测试与 CI 中核对后端登记,拒绝 IoTDB/InfluxDB 3 的重复结果键,并在复制输入前检查直接 adapter 批写的记录数。公开 API、Template → SPI → adapter 主干、Java 17/客户端/服务端基线与配置默认值均保持。 2.1.3 发布说明.
直接调用 IoTDB、InfluxDB 1.x、InfluxDB 3 或 openGemini 的 batchWriteDetailed(database, records),会在 List.copyOf(records) 前将 records.size() 与已配置的 max-batch-records 比较,再在复制后复核快照大小,随后才逐条校验、编码或 I/O。继承的 batchWrite 入口也会进入此路径。报告大小已经超限时,适配器不会调用 iterator() 或 toArray() 来复制、检查集合中的记录。
失败抛出 TSDBBatchWriteException,错误为 ARGUMENT_ERROR、NOT_COMMITTED,已校验/已提交记录数及物理批次数均为零,不发 HTTP 写入请求、不借用 IoTDB session。既有适配器状态检查仍优先:未初始化或已关闭时保留状态错误;ready 状态下 null/空批次仍成功。TGTemplate 原本已在复制业务对象前后检查数量,其行为不变。
本修订只是把既有记录数保护提前,公开 API、SPI、配置键/默认值及请求拆分不变。不新增字节/列数预算,也不对 size()、isEmpty() 或复制行为失真/代价高昂的自定义 Collection 承诺堆内存硬上限;复制后复核也不能让调用方并发修改成为原子操作。范围与验证状态见 2.1.3 发布说明。
IoTDB 校验表名后,按 Locale.ROOT 小写物理表身份归并应用批次。例如,measurement 为 metrics、METRICS、Metrics 的记录共用一个表批次,构造的 Tablet 表名统一为 metrics;调用方原始记录及 tag/field map 不变。同一物理表内的原始行序、既有 Tablet capacity 保持。兼容变体共同按原有规则拆分,totalBatches、committedBatches 统计归并后实际 Tablet 数,记录数仍统计输入记录;其他物理表的列 schema 仍相互独立。
在获取任何 session 或写入其他表之前,先对同一物理表的所有拼写共同检查列角色及推断出的非 null 类型。例如,metrics 中的 device tag 与 METRICS 中的 device field 冲突,或两种拼写下的字段类型冲突,均以 METADATA_ERROR、NOT_COMMITTED 拒绝完整应用批次。确认记录/Tablet 数为零且无数据库 I/O,即使前面的有效记录已足够组成一个完整 Tablet。已发布的 2.1.2 按原始表名拼写分组;扩展到统一物理表的检查纳入 2.1.3。
写入失败已定位到尝试写入的表时,BatchWriteResult.failedMeasurement 指向其小写物理表名;写前预检或获取连接失败尚未选择尝试写入的 Tablet 时仍可为 null。I/O 后的既有提交状态处理、不提供跨 Tablet 事务、预先创建服务端 schema 的要求均保持。InfluxDB 1.x、InfluxDB 3、openGemini 沿用各自现有的 measurement 大小写行为。
IoTDB 精确重复输出列,或 InfluxDB 3 成功查询响应中同一 JSON 对象内的精确重复键,均抛出错误码为 QUERY_ERROR(100308)的 TSDBException。整次 query、count 或适配器 executeQuery 失败:调用方拿不到部分行,也不会收到按覆盖后值算出的计数或空结果 fallback。IoTDB 在迭代前检查元数据;InfluxDB 3 后续行出现重复,也会使该响应先前读到的全部行失效。沿用既有资源清理路径。
此项校验针对查询响应,不改变写入 payload、BatchWriteResult、提交状态分类或原生 SQL 文本。解码时合法的大小写不同输出名仍作为不同键保留。直接调用借出的原生客户端仍遵循 SDK 自身行为。范围与验证状态见 2.1.3 发布说明。
InfluxDB 1.x、InfluxDB 3 和 OpenGemini 对整个应用批次预先生成的 UTF-8 行协议 payload 应用 tsdb.<backend>.max-batch-bytes,默认 67108864 字节(64 MiB),必须为正数。预算包含 payload 内换行分隔符,合计全部物理请求;原有单请求行数/字节上限、max-batch-records、查询响应限制及 IoTDB Tablet 限制保持。配置在构造时生成快照。
超预算批次在第一个请求前返回 ARGUMENT_ERROR、NOT_COMMITTED,已提交记录/批次数为零,无数据库 I/O;不会为了满足预算先提交一部分。调大配置意味着允许更多预生成 payload 内存,应由部署方明确决定。此限制约束编码后 payload 字节,不代表 JVM 总堆内存限制,也不增加异步写入队列。
通过统一适配器 API 发出的 HTTP 请求不自动跟随重定向,同源重定向也不例外。应配置数据库最终 URL,不依赖 HTTP 3xx 路由。重定向响应保留后端既有失败与提交分类;收到重定向不等于能确认零提交。适配器不会向重定向目标重放提交不明的写入。借用原生客户端仍是独立 API 边界。
import com.alandevise.tsgate.annotation.TGField;
import com.alandevise.tsgate.annotation.TGMeasurement;
import com.alandevise.tsgate.annotation.TGTag;
import com.alandevise.tsgate.annotation.TGTime;
@TGMeasurement("ACCRUE")
public class AccrueRecord {
@TGTime
private Long timestamp;
@TGTag("device_code")
private String deviceCode;
@TGTag("point_type")
private String pointType;
@TGTag("source")
private String source;
@TGField("value")
private Double value;
@TGField("status")
private Long status;
}注解说明:
-
@TGMeasurement:声明目标 measurement/table。 -
@TGTime:声明时间字段;字段类型只能是Long/long,值为 Unix Epoch 毫秒且不得为空,不做隐式时区转换。 -
@TGTag:声明 tag 字段;写入时转换为字符串。 -
@TGField:声明普通 field 字段;至少需要一个非空 field 值。
写入 POJO 必须声明一个 @TGTime 和至少一个 @TGField。查询承接对象可以不是写入 POJO,也可以不声明 TsGate 注解。
IoTDB 表模型非引号列名的大小写变体对应同一个物理列。批次预检和 Tablet 逐行取值现在统一使用 Locale.ROOT 小写身份,不修改调用方原始 TSDBRecord、tag map 或 field map。
- 同一条记录同时含
device和DEVICE属于歧义输入,即使都是 tag,或都是相同 Java 类型的 field,也会被拒绝。tag 与 field 共用该身份同样拒绝,包括 null 字段。 - 不同记录的
value、VALUE、VaLuE在角色及推断出的非 null 类型一致时合并成一个物理列。null/缺失值仍为 null,后续非 null 行可提供字段类型。 - 任一角色/类型冲突都会在借连接前拒绝完整业务批次,即使前面的 measurement 或完整 Tablet 原本有效,也不先写入。结果为
METADATA_ERROR、NOT_COMMITTED、零确认记录/批次,且无数据库 I/O。
此修复补全本地校验,不使多个 Tablet 成为事务,也不提前查询服务端 schema。写请求发起后的错误仍须按既有提交状态处理。
注入 com.alandevise.tsgate.core.TGTemplate。启用后端时,对应 starter 注册的统一模板默认 Bean 名为 tgTemplate:
import com.alandevise.tsgate.core.TGTemplate;
import org.springframework.beans.factory.annotation.Autowired;
@Autowired
private TGTemplate tgTemplate;不传 database 时使用 adapter 配置中的默认 database。
boolean success = tgTemplate.write(record);指定 database:
boolean success = tgTemplate.write("tsdb", record);批量写入:
boolean success = tgTemplate.batchWrite("tsdb", records);需要判断提交边界时使用详细结果;失败会抛出携带同一结果对象的 TSDBBatchWriteException:
BatchWriteResult result = tgTemplate.batchWriteDetailed("tsdb", records);commitState 可能为 SUCCESS、NOT_COMMITTED、PARTIALLY_COMMITTED 或 UNKNOWN。网络异常、HTTP 408、5xx 等响应
不能证明当前物理批次没有落库,按 UNKNOWN 返回,错误码为 BATCH_COMMIT_UNKNOWN;此前已确认提交的条数和批次数仍保留。
InfluxDB 3 在 accept_partial=false 下将 HTTP 400/401/403/404/405/413/415/422/429 视为当前批次的明确拒绝,根据此前提交数量报告零提交或部分提交。
InfluxDB 1.x 仅将 HTTP 401/403/404/405/413/415/429 视为明确拒绝;HTTP 400 可能已经写入部分点,按 UNKNOWN 处理。
retryable 与提交确定性独立:429、408 和暂时性 5xx 可由调用方按幂等策略重试;501/505 不提示重试。适配层不会据此自动重放。
accept_partial=false
的校验拒绝语义参见官方写入说明。
单条写入内部会包装成单元素集合,直接作为一个批次下发;当前 starter 不做异步攒批、无界缓冲或背压队列。
OpenGemini 适配在 I/O 前检查整个批次:measurement 名不能含逗号、分号、正斜杠、反斜杠,不能等于 . 或 ..,也不能含不可打印字符。字母、组合标记、数字、标点、符号及 ASCII 空格遵循服务端可打印名称规则,= 保留原样。非法名称报 ARGUMENT_ERROR,结果为 NOT_COMMITTED、零物理批次,不会重命名;继承的 line protocol 校验仍生效。请求发出后,HTTP 400 的部分写入响应仍沿用 InfluxDB 1.x 的 UNKNOWN 语义。
适配层统一抛出 TSDBException 或其子类。错误码固定为 1003xx 六位整数;业务侧可以通过
exception.getErrorCode() 获取枚举,也可以通过 exception.getCode() 直接获取整数编码。错误码是业务错误分类,不等同于 HTTP
状态码。
| 错误码 | 枚举 | 含义 |
|---|---|---|
| 100300 | CONFIGURATION_ERROR |
必填配置缺失、批量上限或连接池配置非法 |
| 100301 | ARGUMENT_ERROR |
SQL、分页、过滤条件、标识符或写入值非法 |
| 100302 | METADATA_ERROR |
注解模型、物理列、Java 类型或结果映射异常 |
| 100303 | ADAPTER_STATE_ERROR |
adapter 或底层 client 尚未初始化或不可用 |
| 100304 | CONNECTION_ERROR |
网络连接、超时、服务不可用或限流 |
| 100305 | PERMISSION_ERROR |
登录认证失败或权限不足 |
| 100306 | RESOURCE_NOT_FOUND |
database、table、column 等资源不存在 |
| 100307 | WRITE_ERROR |
服务端明确拒绝写入且提交边界可判定 |
| 100308 | QUERY_ERROR |
查询语法、执行或响应解析失败 |
| 100309 | BATCH_COMMIT_UNKNOWN |
写入后断连或响应不足以确认当前批次是否提交 |
| 100310 | UNSUPPORTED_OPERATION |
统一 API 不支持的后端能力或调用组合 |
| 100399 | INTERNAL_ERROR |
无法进一步分类的内部错误及旧构造器兼容兜底 |
try{
tgTemplate.write(record);
}catch(
TSDBException e){
int code = e.getCode();
String detail = e.getMessage();
Throwable rootCause = e.getCause();
}业务 Web 层负责把该业务错误码映射为统一响应和适当的 HTTP 状态;starter 不注册全局异常处理器。
-
write、batchWrite和batchWriteDetailed都是同步 API:方法成功返回表示底层 TSDB 已确认本次写入结果,而不是仅进入适配层内存队列。 - OpenGemini 适配的新 measurement 或 tag series 在异步索引合并后才对查询可见。业务需要时,应在读侧显式轮询期望 timestamp、tags 与值并设置截止时间;每次 adapter 查询只发一次请求,不做透明可见性重试。
- 同步不等于全局串行。业务侧可以并发调用,底层通过 IoTDB SessionPool 或 InfluxDB HTTP 连接池复用连接;大批数据应优先使用批量 API 降低网络往返。
- TSDB 或连接池变慢时,延迟、超时或异常会直接传递给调用方,形成明确的压力反馈。adapter 不在内部暂存数据,因此不存在应用重启时尚未 flush 的内存数据。
- 需要异步削峰、持久化重试或可靠投递时,应由业务线程池、消息队列或专用数据通道实现;这些组件才能根据业务语义决定队列容量、拒绝策略、 持久化、重试和去重方式。
page() 和 strictCursorPage() 只用于明细查询;聚合查询必须使用 page(pageNum, pageSize) 的 offset 分页。聚合结果会按
window_start + groupByTags(无时间窗口时按 groupByTags)稳定排序。offset 分页会额外执行一次总数查询:明细查询直接统计满足
条件的原始行数,聚合/分组查询统计聚合结果行数。
- IoTDB:强制使用官方
ITableSessionPool;tsdb.iotdb.pool.enabled=false时应用启动失败。 - IoTDB:必须配置并预先创建默认 database;该库设置到
TableSessionPoolBuilder,普通读写不再发送冗余USE,跨库操作关闭 Session 时由官方客户端恢复默认库。 - 此前验证的 IoTDB SDK 2.0.10 在关闭 Session 恢复默认库失败时不会可靠补充对应池槽位,因此默认库必须在应用运行期间保持存在,业务账号也必须保留 对默认库的访问权限。
- IoTDB SDK 2.0.11 仍在借用的 Session 关闭时恢复默认数据库,升级客户端不免除保持该库可用的要求。
- IoTDB:池满时等待归还,获取超时直接返回,不更换物理池;获取阶段其他连接异常也不自动换池。查询执行阶段仍保留已知 SDK
空响应缺陷和连接异常的按版本恢复,只读最多重试一次。此修复不提供获取阶段槽位泄漏的自动恢复,也未引入跨池信号量;故障换池时
旧池排空与新池使用可能短暂重叠,
max-size仍是单个物理池的上限。 - IoTDB:批量写入会按 measurement 组装 Tablet,单个 Tablet 最大行数由
tsdb.iotdb.table.tablet-max-row-size控制。 - IoTDB:
tsdb.iotdb.table.rpc-compression-enabled默认true,省略即保留默认;2.0.2 等不兼容旧服务端显式设为false。它控制 Tablet RPC 紧凑编码/压缩,不是 Thrift 传输或磁盘压缩。验证时需要一个实际 Tablet 至少 10 行并在写入后回查,不能只看业务 batch 总量;初始池和故障替换池使用同一配置,不增加写入自动重试,也不改变提交状态报告。 - IoTDB:字段值为 null 时从同批次非空值推断类型;始终为 null 的列省略,完全没有可推断 field 的表批次在 I/O 前拒绝。
- IoTDB:字段精确映射为
INT32/INT64/FLOAT/DOUBLE/BOOLEAN/STRING/BLOB/DATE;未支持的 Java 类型在首个 Tablet 写入前拒绝。 - IoTDB/InfluxDB:单次业务批量写入默认最多
10000条,分别由各自的max-batch-records配置控制。 - InfluxDB 3:全部记录会先编码校验,再按最多
5000行或1 MiBUTF-8 payload 拆分为一个或多个 HTTP 请求;写接口使用accept_partial=false。 - InfluxDB 1.x:同样先做全批校验和分块,但服务端 HTTP 400 可能已写入部分合法点。此时提交状态为
UNKNOWN,已确认数量只包含此前成功请求;不自动重试可能已经部分提交的块。 - 两个 InfluxDB adapter 均拒绝以
#开头的 measurement,防止被 line protocol 当注释后静默丢点;measurement 的=与后端反斜杠规则各自正确编码。 - InfluxDB 整数字段支持 Byte/Short/Integer/Long 和 signed-int64 范围内的 BigInteger,后者使用整数协议后缀避免 float64 舍入;不接受含义不明的 Number 子类。
- InfluxDB Float/Double 必须有限。1.x 的 BigDecimal 仅接受 float64 可精确表示值;3.x 的 BigDecimal 使用正常 double 舍入,但拒绝溢出以及非零值下溢为 0。
- InfluxDB 1.x 额外拒绝
_field/_measurement/time作为 tag 或 field 键,避免其协议保留名称造成丢点或拒绝。 - 批量写入不提供跨多个 Tablet 或 HTTP 请求的数据库事务。前置转换或校验失败可确认整批零写入;开始 I/O 后应根据
BatchWriteResult.commitState判断成功、零提交、部分提交或状态未知。 - 查询和分页的业务
limit/pageSize必须在1~10000范围内,offset 计算溢出时直接失败。 - 当前 adapter 是同步写入模型,不维护异步缓冲队列;如果业务侧采集流量可能超过 TSDB 或网络写入能力,应在业务侧或专门的数据通道中 设计限流、削峰、重试和背压。
实测 1.4.1/1.5.2 的 Influx 兼容 HTTP 写入路径会先把整数字段转换为 float64,再存储为整数。整数协议后缀也不能避免这一转换:9007199254740993i 读回为 9007199254740992。因此 OpenGemini 适配器在 I/O 前检查整个批次,只接受 signed-int64 范围内且可由 IEEE 754 float64 精确表示的整数字段。-2^53 至 2^53 之间的整数全部精确;2^53+2、Long.MIN_VALUE 等部分更大绝对值的整数也精确。2^53+1、Long.MAX_VALUE 和 Long.MIN_VALUE+1 在写前返回 ARGUMENT_ERROR、NOT_COMMITTED、零物理请求,不强制转换,也不编码成字符串。公共结构化 query/count 同样预检整数过滤字面量,包括 IN/BETWEEN 的每个值,避免服务端数值比较舍入后错误匹配。 极大整数型 BigDecimal 过滤值先与 signed-int64 边界比较再转换为整数,避免超大指数绕过检查或触发无界展开。Float/Double 及非整数查询字面量仍遵循普通浮点语义;已有 BigDecimal 字段校验继续生效。Epoch 毫秒时间戳走独立的时间路径。原生 SQL 与借用原生客户端写入绕过这些检查,保留服务端精度边界;该校验也不保证聚合累加无溢出或浮点运算精确。此限制针对本次 HTTP 适配,不代表 openGemini 的所有引擎或其他写入协议。
TsGate 2.1.0 包含独立的 OpenGemini adapter 与 starter。配置、默认引擎边界及准确版本测试见 OpenGemini 接入指南,发行说明见 2.1.0 发布说明。