docs: add detailed 32960 pipeline comments
This commit is contained in:
@@ -8,15 +8,23 @@ import java.lang.annotation.Target;
|
||||
/**
|
||||
* 批量异步处理:框架把多次同类调用聚合成 {@code List} 交给方法。
|
||||
*
|
||||
* <p>处理方法签名需为 {@code void foo(List<T> list)} 或返回 {@code List<VehicleEvent>}。
|
||||
* <p>处理方法签名需接收 {@code List<T>},返回 {@code List<VehicleEvent>} 或单个
|
||||
* {@code VehicleEvent}。Dispatcher 不直接调用目标方法,而是交给
|
||||
* {@code AsyncBatchExecutor} 按 size/waitMs 聚合后调用。
|
||||
*
|
||||
* <p>注意:当调用方需要给每条事件补不同的 rawArchiveUri 等逐条元数据时,执行器会按单条 flush,
|
||||
* 以保证事件与原始帧一一对应。
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
public @interface AsyncBatch {
|
||||
|
||||
/** 达到该批量大小后立即 flush。 */
|
||||
int size() default 1000;
|
||||
|
||||
/** 第一条消息入队后最多等待毫秒数,避免低流量车辆长期不出批。 */
|
||||
long waitMs() default 500;
|
||||
|
||||
/** 每个 Handler 方法对应的批处理 worker 数。 */
|
||||
int poolSize() default 2;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import java.lang.annotation.Target;
|
||||
* 声明幂等键,由 Dedup 拦截器使用。支持 SpEL 表达式,上下文根对象为 Handler 参数。
|
||||
*
|
||||
* <p>示例:{@code @IdempotentKey("#msg.vin + ':' + #msg.seq + ':' + #msg.eventTime")}
|
||||
*
|
||||
* <p>当前内置去重主要基于 RawFrame sourceMeta/raw bytes;该注解保留给更细粒度 Handler 级去重。
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
|
||||
@@ -14,14 +14,20 @@ import java.lang.annotation.Target;
|
||||
* <li>JT/T 808:{@code command} = 消息 ID(0x0100 / 0x0200 / ...);{@code infoType} 留空
|
||||
* <li>MQTT:{@code command} 可为 topic hash 或忽略
|
||||
* </ul>
|
||||
*
|
||||
* <p>同一个方法可以声明多个 command/infoType。Dispatcher 会按协议、command、infoType 精确路由;
|
||||
* 32960 实时上报解析后通常由 0x02/0x03 主命令加信息体 ID 决定落到哪个 Handler。
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
public @interface MessageMapping {
|
||||
|
||||
/** 协议主命令或消息 ID,空数组表示该维度不限制。 */
|
||||
int[] command() default {};
|
||||
|
||||
/** 协议子类型,GB32960 中对应信息体 ID;空数组表示该维度不限制。 */
|
||||
int[] infoType() default {};
|
||||
|
||||
/** 仅用于日志/Swagger/排障展示,不参与路由。 */
|
||||
String desc() default "";
|
||||
}
|
||||
|
||||
@@ -6,7 +6,10 @@ import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 单 VIN 速率限制。超限消息直接进 DLQ 并打点。
|
||||
* 单 VIN 速率限制声明。
|
||||
*
|
||||
* <p>当前内置限流器在 Pipeline before 阶段按 VIN 中止处理,超限帧不会进入 Handler 和下游存储。
|
||||
* 如果需要把超限帧写入 DLQ,应在拦截器或入口层显式增加对应 sink。
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
|
||||
@@ -33,6 +33,8 @@ public final class EnvelopeConsumerProcessor {
|
||||
throw new IllegalArgumentException("record must not be null");
|
||||
}
|
||||
EnvelopeIngestResult result = ingestor.tryIngest(record.payload());
|
||||
// 派生消费者不在这里抛出业务异常给 Kafka worker;坏消息统一进入 DLQ,
|
||||
// worker 看到 process 正常返回后才能提交 offset,避免同一坏消息无限阻塞消费组。
|
||||
if (DEAD_LETTER_STATUSES.contains(result.status())) {
|
||||
deadLetterSink.publish(toDeadLetter(record, result));
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@ import java.util.Map;
|
||||
|
||||
/**
|
||||
* Shared raw-archive key conventions used by Dispatcher, archive sink, and query services.
|
||||
*
|
||||
* <p>32960 生产链路里,RAW .bin 文件本体按 {@code 日期/协议/VIN/eventId.bin} 落在 archive 根目录,
|
||||
* DuckDB 只保存 {@code archive://...} 引用和查询索引。这个类集中维护 key 规则,避免接收端、落盘端、
|
||||
* snapshot 查询端对目录分区的理解不一致。</p>
|
||||
*/
|
||||
public final class RawArchiveKeys {
|
||||
|
||||
@@ -23,6 +27,7 @@ public final class RawArchiveKeys {
|
||||
}
|
||||
|
||||
public static String key(Instant ingestTime, ProtocolId source, String vin, String eventId) {
|
||||
// 分区日期使用 ingestTime 的东八区自然日,和磁盘目录保持一致;eventTime 只代表车端上报时间。
|
||||
String dateKey = DATE_KEY.format(ingestTime == null ? Instant.EPOCH : ingestTime);
|
||||
String safeVin = vin == null || vin.isBlank() ? "unknown-vin" : vin;
|
||||
String safeEventId = eventId == null || eventId.isBlank()
|
||||
|
||||
@@ -5,6 +5,10 @@ package com.lingniu.ingest.api.event;
|
||||
*
|
||||
* <p>Protocol mappers use stable internal field keys so Kafka, Parquet, Redis,
|
||||
* and statistics do not depend on protocol-specific names.
|
||||
*
|
||||
* <p>{@code value} 统一用字符串承载,真实类型由 {@code valueType} 表达。这样 Kafka
|
||||
* protobuf、CSV 导出、DuckDB JSON 字段和前端展示可以共用同一份字段结构;数值精度/格式化
|
||||
* 由查询层或前端按 {@code valueType + unit} 决定。
|
||||
*/
|
||||
public record TelemetryFieldValue(
|
||||
String key,
|
||||
@@ -24,6 +28,7 @@ public record TelemetryFieldValue(
|
||||
}
|
||||
value = value == null ? "" : value;
|
||||
unit = unit == null ? "" : unit;
|
||||
// 默认 GOOD,只有解析器明确知道异常/无效/缺失时才降级,避免调用方到处补质量字段。
|
||||
quality = quality == null ? Quality.GOOD : quality;
|
||||
sourcePath = sourcePath == null ? "" : sourcePath;
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ public record TelemetrySnapshot(
|
||||
rawArchiveUri = rawArchiveUri == null ? "" : rawArchiveUri;
|
||||
metadata = Map.copyOf(metadata == null ? Map.of() : metadata);
|
||||
fields = List.copyOf(fields == null ? List.of() : fields);
|
||||
// snapshot/fields 查询按 key 投影和导出,重复 key 会导致前端列和 CSV 表头不稳定,构造期直接拒绝。
|
||||
validateUniqueKeys(fields);
|
||||
}
|
||||
|
||||
@@ -57,6 +58,7 @@ public record TelemetrySnapshot(
|
||||
if (key == null || key.isBlank()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
// fields 保持插入顺序,单字段查询走线性查找;批量查询应使用 fieldsByKey 建索引。
|
||||
for (TelemetryFieldValue field : fields) {
|
||||
if (field.key().equals(key)) {
|
||||
return Optional.of(field);
|
||||
@@ -70,6 +72,7 @@ public record TelemetrySnapshot(
|
||||
for (TelemetryFieldValue field : fields) {
|
||||
index.put(field.key(), field);
|
||||
}
|
||||
// LinkedHashMap 保留原始字段顺序,CSV 导出和 Swagger 示例能稳定复现解析器顺序。
|
||||
return Map.copyOf(index);
|
||||
}
|
||||
|
||||
|
||||
@@ -141,11 +141,11 @@ public sealed interface VehicleEvent
|
||||
) implements VehicleEvent {}
|
||||
|
||||
/**
|
||||
* 原始报文冷存事件:每条成功解码的入站帧由 Dispatcher 产出一条,携带原始字节交
|
||||
* {@code ArchiveEventSink} 写入 ArchiveStore。Kafka sink 默认不处理本类型
|
||||
* (见 {@code KafkaEventSink.accepts})。
|
||||
* 原始报文归档事件:每条成功解码的入站帧由 Dispatcher 产出一条,携带原始字节和归档索引信息。
|
||||
* 32960 历史全字段查询依赖它对应的 {@code archive://...} 文件能够被回读。
|
||||
*
|
||||
* <p>key 组装建议:{@code yyyy/MM/dd/<source>/<vin>/<eventId>.bin},具体由 sink 实现决定。
|
||||
* <p>当前 {@code KafkaEventSink} 默认不发送本类型,{@code EventFileStoreSink} 只保存索引不保存
|
||||
* bytes;因此启用新 raw 落盘/转发实现时,必须同时保证 {@link RawArchiveKeys} 的 key 规则一致。
|
||||
*
|
||||
* @param command 协议主命令码(如 32960 0x02/0x03 等),冗余在事件里便于按命令分片归档
|
||||
* @param infoType 协议子类型(可为 0),同上
|
||||
|
||||
@@ -5,6 +5,9 @@ import java.util.Map;
|
||||
|
||||
/**
|
||||
* 一次消息处理的上下文,贯穿整个拦截链与 Handler。线程不共享,不需要同步。
|
||||
*
|
||||
* <p>attributes 用于在入口、拦截器、Dispatcher 之间传递轻量元数据,例如 traceId、归档 URI、
|
||||
* 鉴权结果等;不要放大对象或长期缓存,避免高频上报时放大内存占用。
|
||||
*/
|
||||
public final class IngestContext {
|
||||
|
||||
@@ -32,6 +35,7 @@ public final class IngestContext {
|
||||
}
|
||||
|
||||
public void abort(String reason) {
|
||||
// reason 会进入日志/诊断信息,保持短小、机器可读,方便定位是去重、限流还是鉴权失败。
|
||||
this.aborted = true;
|
||||
this.abortReason = reason;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ import java.util.Map;
|
||||
* @param rawBytes 原始字节,用于冷存与排障(可能为 null,按配置开关)
|
||||
* @param sourceMeta 来源元数据:peer ip、端口、session id、topic 等
|
||||
* @param receivedAt 服务器接收时刻
|
||||
*
|
||||
* <p>RawFrame 是所有协议进入统一 Pipeline 的边界对象。32960 TCP 入口会把 VIN、seq、
|
||||
* rawArchiveUri 等关键索引放进 sourceMeta,后续去重、快照和历史查询都依赖这些字段保持稳定。
|
||||
*/
|
||||
public record RawFrame(
|
||||
ProtocolId protocolId,
|
||||
|
||||
Reference in New Issue
Block a user