feat(ingest-core): Dispatcher emits RawArchive event per inbound frame

Dispatcher.dispatch 顶部(interceptor 之前)在 RawFrame.rawBytes() 非空时通过
EventBus 发一条 VehicleEvent.RawArchive,由 ArchiveEventSink 消费落盘。

放在 interceptor 之前的理由:原始可回放的目标是"每一条成功解码的帧都要留痕",
dedup / rate-limit 不应过滤 archive。archive sink 的写失败只影响冷存,不影响
业务事件流。

RawArchive 的 VIN 从 RawFrame.sourceMeta["vin"] 取;缺失时留空字符串,ArchiveSink
落盘时会用 "unknown-vin" 占位(见 ArchiveEventSink.buildKey)。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
kkfluous
2026-04-20 15:54:59 +08:00
parent dd838b4f73
commit 812b8877d4

View File

@@ -9,7 +9,9 @@ import com.lingniu.ingest.core.pipeline.InterceptorChain;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.time.Instant;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
/** /**
@@ -46,6 +48,10 @@ public final class Dispatcher {
public void dispatch(RawFrame frame) { public void dispatch(RawFrame frame) {
IngestContext ctx = new IngestContext(UUID.randomUUID().toString()); IngestContext ctx = new IngestContext(UUID.randomUUID().toString());
try { try {
// 在 interceptor 之前发 RawArchive保证原始字节被无条件落盘dedup/rate-limit
// 不会过滤它),满足"原始可回放"目标。archive 的写盘靠下游 ArchiveEventSink 消费。
emitRawArchive(frame, ctx);
if (!interceptors.before(frame, ctx)) { if (!interceptors.before(frame, ctx)) {
log.debug("frame aborted: {}", ctx.abortReason()); log.debug("frame aborted: {}", ctx.abortReason());
return; return;
@@ -76,4 +82,33 @@ public final class Dispatcher {
interceptors.onError(t, ctx); interceptors.onError(t, ctx);
} }
} }
/**
* 从 {@link RawFrame} 构造一条 {@link VehicleEvent.RawArchive} 发到 EventBus。
* 仅在 {@code rawBytes} 非空时发——有些入站适配器(未来)可能只传解析后的对象。
*
* <p>VIN 取自 sourceMeta 里的 {@code vin} key由各入站适配器负责填充缺失时
* 留空字符串archive sink 会用 "unknown-vin" 占位保证 key 可解析。
*/
private void emitRawArchive(RawFrame frame, IngestContext ctx) {
byte[] bytes = frame.rawBytes();
if (bytes == null || bytes.length == 0) return;
Map<String, String> meta = frame.sourceMeta() == null ? Map.of() : frame.sourceMeta();
String vin = meta.getOrDefault("vin", "");
Instant ingestTime = frame.receivedAt() != null ? frame.receivedAt() : Instant.now();
VehicleEvent.RawArchive raw = new VehicleEvent.RawArchive(
UUID.randomUUID().toString(),
vin,
frame.protocolId(),
ingestTime,
ingestTime,
ctx.traceId(),
meta,
frame.command(),
frame.infoType(),
bytes);
eventBus.publish(raw);
}
} }