docs: add detailed 32960 pipeline comments
This commit is contained in:
@@ -24,6 +24,13 @@ import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 基于 Parquet 文件 + DuckDB sidecar 索引的历史事件库。
|
||||
*
|
||||
* <p>写入时按 {@code protocol/date/vehicle/header} 分区保存 Parquet,便于按单车、按天直接读取。
|
||||
* 同时维护一个轻量 DuckDB 索引文件 {@code events.duckdb},用于无 VIN 查询、rawArchiveUri
|
||||
* 精确定位和索引重建。设计目标是把 32960 查询的热路径限制在“单车若干天的文件”内。
|
||||
*/
|
||||
public final class DuckDbParquetEventFileStore implements EventFileStore {
|
||||
|
||||
private static final TypeReference<Map<String, String>> STRING_MAP =
|
||||
@@ -68,9 +75,11 @@ public final class DuckDbParquetEventFileStore implements EventFileStore {
|
||||
|
||||
@Override
|
||||
public List<EventFileRecord> query(EventFileQuery query) throws IOException {
|
||||
// 有 VIN 时直接扫该车分区 parquet,避免先查总索引再和车辆文件 join。
|
||||
if (query.vin() != null) {
|
||||
return queryParquet(query);
|
||||
}
|
||||
// 无 VIN 的管理类查询走 DuckDB 索引,代价更高但不影响高频单车查询路径。
|
||||
ensureIndexInitialized();
|
||||
return queryIndex(query);
|
||||
}
|
||||
@@ -121,6 +130,7 @@ public final class DuckDbParquetEventFileStore implements EventFileStore {
|
||||
if (query.eventTimeTo() != null) {
|
||||
predicates.add("event_time_ms <= " + query.eventTimeTo().toEpochMilli());
|
||||
}
|
||||
// 这里的 SQL 只拼接经过 escape 的内部值;外部 rawArchiveUri 查询使用 PreparedStatement。
|
||||
String where = predicates.isEmpty() ? "" : "WHERE " + String.join(" AND ", predicates) + "\n";
|
||||
String sql = """
|
||||
SELECT event_id, protocol, event_type, vin,
|
||||
@@ -255,6 +265,7 @@ public final class DuckDbParquetEventFileStore implements EventFileStore {
|
||||
ps.executeBatch();
|
||||
}
|
||||
if (Files.exists(partFile)) {
|
||||
// DuckDB 不能原地追加 parquet;先 UNION 旧文件和本批数据,再原子替换分区文件。
|
||||
statement.execute("""
|
||||
CREATE TEMPORARY TABLE combined_records AS
|
||||
SELECT event_id, protocol, event_type, vin,
|
||||
@@ -301,6 +312,7 @@ public final class DuckDbParquetEventFileStore implements EventFileStore {
|
||||
Statement statement = connection.createStatement()) {
|
||||
createIndexSchema(statement);
|
||||
if (isIndexEmpty(statement)) {
|
||||
// 新部署或删除 index 后,可从现有 parquet 自动回填索引,避免历史数据不可查。
|
||||
backfillIndexFromParquet(statement);
|
||||
}
|
||||
indexInitialized = true;
|
||||
@@ -381,6 +393,7 @@ public final class DuckDbParquetEventFileStore implements EventFileStore {
|
||||
Path dir = query.vin() == null
|
||||
? partitionDir(query.protocol(), date)
|
||||
: partitionDir(query.protocol(), date).resolve("vehicle=" + storageName(query.vin()));
|
||||
// 目录结构把 VIN 放在 date 下,单车多日查询只需要遍历目标日期内的目标车辆目录。
|
||||
if (Files.isDirectory(dir)) {
|
||||
try (Stream<Path> stream = Files.walk(dir)) {
|
||||
stream.filter(path -> path.getFileName().toString().endsWith(".parquet"))
|
||||
@@ -471,6 +484,7 @@ public final class DuckDbParquetEventFileStore implements EventFileStore {
|
||||
if (vin == null || vin.isBlank()) {
|
||||
return "_unknown";
|
||||
}
|
||||
// VIN 会进入文件路径,保守替换特殊字符,避免路径穿越和跨平台文件名问题。
|
||||
return vin.replaceAll("[^A-Za-z0-9._-]", "_");
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,12 @@ import com.lingniu.ingest.api.ProtocolId;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 文件历史库查询条件。
|
||||
*
|
||||
* <p>{@code dateFrom/dateTo} 用于定位分区目录,{@code eventTimeFrom/eventTimeTo}
|
||||
* 用于在分区内做精确时间过滤。这样既支持按天快速裁剪,也支持接口精确到秒的查询。
|
||||
*/
|
||||
public record EventFileQuery(
|
||||
ProtocolId protocol,
|
||||
LocalDate dateFrom,
|
||||
@@ -34,6 +40,7 @@ public record EventFileQuery(
|
||||
if (eventTimeFrom != null && eventTimeTo != null && eventTimeTo.isBefore(eventTimeFrom)) {
|
||||
throw new IllegalArgumentException("eventTimeTo must not be before eventTimeFrom");
|
||||
}
|
||||
// 归一化可选字段,避免空字符串进入 SQL 谓词或文件分区路径。
|
||||
order = order == null ? Order.ASC : order;
|
||||
limit = limit <= 0 ? 100 : limit;
|
||||
vin = vin == null || vin.isBlank() ? null : vin.trim();
|
||||
|
||||
@@ -10,6 +10,9 @@ import java.util.Map;
|
||||
*
|
||||
* <p>{@code payloadJson} 保存统一 telemetry snapshot JSON;公共列只保留排序、分区、
|
||||
* 追溯和通用展示需要的最小字段。
|
||||
*
|
||||
* <p>对 GB32960 来说,{@code rawArchiveUri} 是最关键的追溯列:通用查询可以直接展示
|
||||
* payloadJson,专用全字段查询则通过 rawArchiveUri 回读原始包重新解码。
|
||||
*/
|
||||
public record EventFileRecord(
|
||||
String eventId,
|
||||
@@ -35,6 +38,7 @@ public record EventFileRecord(
|
||||
if (ingestTime == null) {
|
||||
throw new IllegalArgumentException("ingestTime must not be null");
|
||||
}
|
||||
// 允许 eventType/vin/rawArchiveUri 为空字符串,便于保存平台登录、RAW 索引等非车辆事件。
|
||||
eventType = eventType == null ? "" : eventType;
|
||||
vin = vin == null ? "" : vin;
|
||||
rawArchiveUri = rawArchiveUri == null ? "" : rawArchiveUri;
|
||||
|
||||
@@ -3,16 +3,29 @@ package com.lingniu.ingest.eventfilestore;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 历史明细文件库抽象。
|
||||
*
|
||||
* <p>写入侧只接受已经标准化的 {@link EventFileRecord};具体实现可以落 Parquet、维护 DuckDB
|
||||
* sidecar 索引,或在测试中用内存实现。32960 专用 snapshot 查询会先用这里按 VIN/日期找到
|
||||
* rawArchiveUri,再回读原始 .bin 解码完整字段。
|
||||
*/
|
||||
public interface EventFileStore {
|
||||
|
||||
/** 单条追加的便捷方法,最终仍走批量写入路径,保证实现只维护一种落盘语义。 */
|
||||
default void append(EventFileRecord record) throws IOException {
|
||||
appendAll(List.of(record));
|
||||
}
|
||||
|
||||
void appendAll(List<EventFileRecord> records) throws IOException;
|
||||
|
||||
/** 按协议、日期、VIN、事件类型等条件查询标准化历史记录。 */
|
||||
List<EventFileRecord> query(EventFileQuery query) throws IOException;
|
||||
|
||||
/**
|
||||
* 按 rawArchiveUri 回查索引记录。默认实现返回 null,允许轻量测试实现不维护该索引。
|
||||
* 生产 DuckDB/Parquet 实现必须覆盖,用于 snapshots 的 sourceFrames 反查。
|
||||
*/
|
||||
default EventFileRecord findByRawArchiveUri(String rawArchiveUri) throws IOException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -22,8 +22,12 @@ import org.slf4j.LoggerFactory;
|
||||
/**
|
||||
* EventBus 出口:把解析后的 {@link VehicleEvent} 明细写入 Parquet 文件库。
|
||||
*
|
||||
* <p>{@link VehicleEvent.RawArchive} 只写可查询索引,不写原始 bytes;原始 bytes 本体仍由
|
||||
* sink-archive 独立冷存。
|
||||
* <p>{@link VehicleEvent.RawArchive} 在这里仅写可查询索引和 {@code archive://...} 引用,不写
|
||||
* 原始 bytes。本体必须已经由上游归档链路落成 .bin;否则 snapshot/frame 查询只能找到索引,
|
||||
* 但回读原始包时会报缺失。
|
||||
*
|
||||
* <p>32960 生产链路当前只把 RAW_ARCHIVE 作为历史查询事实源;REALTIME 和 LOCATION
|
||||
* 是从 RAW 派生出来的轻量事件,不再重复写入 event-history,避免历史库里出现两套含义相近的数据。
|
||||
*/
|
||||
public final class EventFileStoreSink implements EventSink, AutoCloseable {
|
||||
|
||||
@@ -73,6 +77,7 @@ public final class EventFileStoreSink implements EventSink, AutoCloseable {
|
||||
|
||||
@Override
|
||||
public boolean accepts(VehicleEvent event) {
|
||||
// REALTIME/LOCATION 可以由 RAW 重放得到;历史库只保留 RAW_ARCHIVE 和少量非遥测事件索引。
|
||||
return event != null
|
||||
&& !(event instanceof VehicleEvent.Realtime)
|
||||
&& !(event instanceof VehicleEvent.Location);
|
||||
@@ -134,6 +139,7 @@ public final class EventFileStoreSink implements EventSink, AutoCloseable {
|
||||
synchronized (buffer) {
|
||||
buffer.add(record);
|
||||
if (buffer.size() >= batchSize) {
|
||||
// 拷贝后释放锁,避免慢 I/O 阻塞 EventBus 后续写入线程。
|
||||
toFlush = new ArrayList<>(buffer);
|
||||
buffer.clear();
|
||||
}
|
||||
@@ -189,6 +195,7 @@ public final class EventFileStoreSink implements EventSink, AutoCloseable {
|
||||
if (raw.metadata() != null) {
|
||||
metadata.putAll(raw.metadata());
|
||||
}
|
||||
// metadata 是 HTTP 查询和 raw 文件回查共用的关联字段,缺失时在这里补齐。
|
||||
metadata.putIfAbsent(RawArchiveKeys.META_EVENT_ID, raw.eventId());
|
||||
metadata.putIfAbsent(RawArchiveKeys.META_KEY, rawArchiveKey);
|
||||
metadata.putIfAbsent(RawArchiveKeys.META_URI, rawArchiveUri);
|
||||
|
||||
@@ -15,6 +15,18 @@ import org.springframework.context.annotation.Bean;
|
||||
import java.nio.file.Path;
|
||||
import java.time.ZoneId;
|
||||
|
||||
/**
|
||||
* EventFileStore 自动装配。
|
||||
*
|
||||
* <p>开启 {@code lingniu.ingest.event-file-store.enabled=true} 后会创建两类 Bean:
|
||||
* <ul>
|
||||
* <li>{@link EventFileStore}:DuckDB sidecar index + Parquet 文件库,用于历史查询
|
||||
* <li>{@link EventFileStoreSink}:EventBus sink,把可接收事件转换为 {@code EventFileRecord}
|
||||
* </ul>
|
||||
*
|
||||
* <p>32960 当前生产路径里,event-file-store 主要保存 RAW_ARCHIVE 的可查询索引;
|
||||
* 原始 bytes 本体由 sink-archive 写入 archive 根目录。
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@EnableConfigurationProperties(EventFileStoreProperties.class)
|
||||
@ConditionalOnProperty(
|
||||
@@ -28,6 +40,7 @@ public class EventFileStoreAutoConfiguration {
|
||||
public EventFileStore eventFileStore(EventFileStoreProperties properties,
|
||||
ObjectProvider<ObjectMapper> objectMapper) {
|
||||
ObjectMapper mapper = mapper(objectMapper);
|
||||
// zoneId 只影响文件分区日期;payload 中 eventTime/ingestTime 仍保持 Instant/UTC 表示。
|
||||
return new DuckDbParquetEventFileStore(
|
||||
Path.of(properties.getPath()),
|
||||
ZoneId.of(properties.getZoneId()),
|
||||
@@ -48,6 +61,7 @@ public class EventFileStoreAutoConfiguration {
|
||||
|
||||
private static ObjectMapper mapper(ObjectProvider<ObjectMapper> provider) {
|
||||
ObjectMapper base = provider.getIfAvailable(ObjectMapper::new);
|
||||
// 使用 copy,避免全局 ObjectMapper 被本模块注册 JavaTimeModule 时产生隐式副作用。
|
||||
return base.copy().registerModule(new JavaTimeModule());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,21 +12,30 @@ public class EventFileStoreProperties {
|
||||
|
||||
/**
|
||||
* Parquet 文件库根路径。
|
||||
*
|
||||
* <p>实际结构为 {@code protocol=GB32960/date=yyyy-MM-dd/vehicle=VIN/header=event-records-v1/events.parquet},
|
||||
* 另有 {@code events.duckdb} sidecar index 放在根路径下。
|
||||
*/
|
||||
private String path = "./event-store";
|
||||
|
||||
/**
|
||||
* 按事件时间分区时使用的业务时区。
|
||||
*
|
||||
* <p>该配置只决定“某条事件落入哪一天目录”,不改变接口响应里的 UTC 时间字符串。
|
||||
*/
|
||||
private String zoneId = "Asia/Shanghai";
|
||||
|
||||
/**
|
||||
* Sink 缓冲多少条事件后批量写一个 Parquet part 文件。
|
||||
*
|
||||
* <p>值越大写入吞吐越好,但异常退出时内存缓冲里尚未 flush 的事件越多。
|
||||
*/
|
||||
private int batchSize = 500;
|
||||
|
||||
/**
|
||||
* 未达到 batchSize 时的最长刷盘间隔。
|
||||
*
|
||||
* <p>用于低流量车辆:即使没有攒满 batch,也会定期把缓冲写入文件,降低查询延迟。
|
||||
*/
|
||||
private long flushIntervalMillis = 1000;
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ public final class EnvelopeMapper {
|
||||
.setIngestTimeMs(event.ingestTime().toEpochMilli())
|
||||
.setIngestNodeId(nodeId);
|
||||
if (event.metadata() != null) b.putAllMetadata(event.metadata());
|
||||
// telemetrySnapshot 是跨事件类型的统一字段视图,消费者可先读它,必要时再读具体 payload。
|
||||
VehicleEventTelemetrySnapshotMapper.toSnapshot(event)
|
||||
.map(EnvelopeMapper::buildTelemetrySnapshot)
|
||||
.ifPresent(b::setTelemetrySnapshot);
|
||||
@@ -70,6 +71,8 @@ public final class EnvelopeMapper {
|
||||
int size = ra.rawBytes() == null ? 0 : ra.rawBytes().length;
|
||||
String key = rawArchiveKey(ra);
|
||||
String uri = rawArchiveUri(ra, key);
|
||||
// RAW envelope 只携带引用信息,不把完整原始字节塞进 Kafka,避免 topic 膨胀。
|
||||
// 注意 KafkaEventSink 当前仍过滤 RawArchive;这段用于未来放开 raw 引用转发或消费者测试。
|
||||
b.putMetadata(RawArchiveKeys.META_KEY, key);
|
||||
b.putMetadata(RawArchiveKeys.META_URI, uri);
|
||||
b.putMetadata(RawArchiveKeys.META_EVENT_ID, ra.eventId());
|
||||
|
||||
@@ -33,6 +33,8 @@ public final class KafkaEnvelopeConsumerFactory {
|
||||
List<KafkaEnvelopeConsumerWorker> workers = new ArrayList<>();
|
||||
for (Map.Entry<String, SinkMqProperties.Binding> entry : bindings.entrySet()) {
|
||||
String processorBeanName = entry.getKey();
|
||||
// binding 的 key 必须和 Spring Bean 名一致;这样配置只声明 topic/group,
|
||||
// 实际处理逻辑仍由各业务模块自己的 EnvelopeConsumerProcessor 承接。
|
||||
EnvelopeConsumerProcessor processor = processors.get(processorBeanName);
|
||||
SinkMqProperties.Binding binding = entry.getValue();
|
||||
if (processor == null || binding == null || !binding.isEnabled()) {
|
||||
@@ -56,6 +58,8 @@ public final class KafkaEnvelopeConsumerFactory {
|
||||
if (configured != null && !configured.isEmpty()) {
|
||||
return configured;
|
||||
}
|
||||
// 默认绑定保留老派生模块的消费能力;32960 主链路不依赖这些派生表,
|
||||
// 历史查询以 event-file-store 的 RAW 索引和 archive .bin 为准。
|
||||
SinkMqProperties.Topics topics = props.getTopics();
|
||||
Map<String, SinkMqProperties.Binding> defaults = new LinkedHashMap<>();
|
||||
defaults.put("eventHistoryEnvelopeConsumerProcessor", binding(
|
||||
@@ -108,6 +112,8 @@ public final class KafkaEnvelopeConsumerFactory {
|
||||
p.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ByteArrayDeserializer.class.getName());
|
||||
p.put(ConsumerConfig.GROUP_ID_CONFIG, groupId(binding, processorBeanName));
|
||||
p.put(ConsumerConfig.CLIENT_ID_CONFIG, consumer.getClientIdPrefix() + "-" + processorBeanName);
|
||||
// 手动提交 offset:只有本批次至少有一条记录被处理器接收后才 commit,
|
||||
// 避免轮询到空批次或未绑定 topic 时推进消费位点。
|
||||
p.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
|
||||
p.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, consumer.getAutoOffsetReset());
|
||||
p.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, consumer.getMaxPollRecords());
|
||||
|
||||
@@ -45,6 +45,7 @@ public final class KafkaEnvelopeConsumerRunner implements SmartLifecycle, AutoCl
|
||||
if (!running.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
// 每个 worker 一条后台线程,避免某个处理器阻塞时拖慢其他消费组。
|
||||
executor = Executors.newFixedThreadPool(workers.size(), r -> {
|
||||
Thread thread = new Thread(r, "kafka-envelope-consumer");
|
||||
thread.setDaemon(true);
|
||||
@@ -60,6 +61,8 @@ public final class KafkaEnvelopeConsumerRunner implements SmartLifecycle, AutoCl
|
||||
try {
|
||||
worker.pollOnce(pollTimeout);
|
||||
} catch (RuntimeException ex) {
|
||||
// Kafka/处理器异常不让 Spring 生命周期退出;退避后继续消费,
|
||||
// 具体坏消息由 EnvelopeConsumerProcessor 写入 DLQ。
|
||||
log.warn("Kafka envelope consumer poll failed; the worker will retry after backoff", ex);
|
||||
sleepBackoff();
|
||||
}
|
||||
|
||||
@@ -37,8 +37,11 @@ public final class KafkaEnvelopeConsumerWorker implements AutoCloseable {
|
||||
for (ConsumerRecord<String, byte[]> record : records) {
|
||||
EnvelopeConsumerProcessor processor = processorsByTopic.get(record.topic());
|
||||
if (processor == null) {
|
||||
// worker 可能订阅多个 topic;没有显式绑定处理器的 topic 不参与提交语义。
|
||||
continue;
|
||||
}
|
||||
// EnvelopeConsumerProcessor 内部会把解析或业务错误转成 DLQ 记录,
|
||||
// 这里保持 Kafka worker 的职责单一:轮询、分发、成功后提交 offset。
|
||||
processor.process(new EnvelopeConsumerRecord(
|
||||
record.topic(),
|
||||
record.partition(),
|
||||
@@ -48,6 +51,7 @@ public final class KafkaEnvelopeConsumerWorker implements AutoCloseable {
|
||||
processed++;
|
||||
}
|
||||
if (processed > 0) {
|
||||
// commitSync 放在批次末尾,保证同一个 poll 批次内的消息按 Kafka offset 一起确认。
|
||||
consumer.commitSync();
|
||||
}
|
||||
return processed;
|
||||
|
||||
@@ -34,6 +34,7 @@ public final class KafkaEnvelopeDeadLetterSink implements EnvelopeDeadLetterSink
|
||||
throw new IllegalArgumentException("record must not be null");
|
||||
}
|
||||
ProducerRecord<String, byte[]> out = new ProducerRecord<>(topic, record.key(), record.payload());
|
||||
// DLQ payload 保持原始 Kafka value;定位信息全部放 header,便于后续重放或人工排查。
|
||||
header(out, "dlq-service", record.service());
|
||||
header(out, "dlq-source-topic", record.topic());
|
||||
header(out, "dlq-source-partition", Integer.toString(record.partition()));
|
||||
|
||||
@@ -43,9 +43,9 @@ public final class KafkaEventSink implements EventSink, AutoCloseable {
|
||||
}
|
||||
|
||||
/**
|
||||
* 拒绝 {@link VehicleEvent.RawArchive} —— 原始报文体积大、属于冷存域,本期由
|
||||
* {@code ArchiveEventSink} 独占处理。未来若需要将 raw archive URI 回填到 Kafka 的
|
||||
* {@code vehicle.raw.archive} topic,再放开此过滤并在 Envelope 里填 uri/checksum。
|
||||
* 拒绝 {@link VehicleEvent.RawArchive} —— 当前生产 Kafka sink 只投递实时、会话、告警等轻量事件。
|
||||
* {@link EnvelopeMapper} 和 {@link TopicRouter} 已具备 RAW envelope/topic 的映射能力,但这里仍
|
||||
* 显式过滤;如需把 32960 raw 引用转发到 {@code vehicle.raw.archive},应先移除此过滤并补充测试。
|
||||
*/
|
||||
@Override
|
||||
public boolean accepts(VehicleEvent event) {
|
||||
|
||||
@@ -30,6 +30,10 @@ import java.util.Properties;
|
||||
* <li>{@code lingniu.ingest.sink.mq.type=kafka}(默认 kafka)—— 后端选择,预留
|
||||
* rocketmq/pulsar 等扩展。
|
||||
* </ol>
|
||||
*
|
||||
* <p>Producer 和 Consumer 是两个独立开关:{@code sink.mq.enabled=true} 只表示可以创建
|
||||
* Kafka producer/sink;是否从 Kafka 拉 envelope 还要单独开启
|
||||
* {@code lingniu.ingest.sink.mq.consumer.enabled=true} 并配置 bindings。
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@EnableConfigurationProperties(SinkMqProperties.class)
|
||||
@@ -84,6 +88,7 @@ public class SinkMqAutoConfiguration {
|
||||
TopicRouter router,
|
||||
SinkMqProperties props,
|
||||
CircuitBreaker breaker) {
|
||||
// KafkaEventSink 是 EventBus 的生产端出口;它不会启动任何 Kafka 消费线程。
|
||||
return new KafkaEventSink(producer, mapper, router, props.getTopics().getDlq(), breaker);
|
||||
}
|
||||
|
||||
@@ -101,6 +106,7 @@ public class SinkMqAutoConfiguration {
|
||||
@ConditionalOnProperty(prefix = "lingniu.ingest.sink.mq.consumer", name = "enabled", havingValue = "true")
|
||||
public KafkaEnvelopeConsumerRunner kafkaEnvelopeConsumerRunner(Map<String, EnvelopeConsumerProcessor> processors,
|
||||
SinkMqProperties props) {
|
||||
// Consumer runner 根据 processor bean 名和配置 binding 生成 worker;没有 binding 时直接失败,避免静默不消费。
|
||||
List<KafkaEnvelopeConsumerWorker> workers = new KafkaEnvelopeConsumerFactory().createWorkers(processors, props);
|
||||
if (workers.isEmpty()) {
|
||||
throw new IllegalStateException("no kafka envelope consumer workers created; check consumer bindings");
|
||||
|
||||
@@ -20,12 +20,14 @@ public class SinkMqProperties {
|
||||
|
||||
/** MQ 后端类型。目前仅支持 kafka,预留 rocketmq/pulsar 等。 */
|
||||
private String type = "kafka";
|
||||
/** Kafka bootstrap servers;生产环境应通过环境变量覆盖,不建议使用默认开发地址。 */
|
||||
private String bootstrapServers = "114.55.58.251:9092";
|
||||
private String compressionType = "zstd";
|
||||
private int lingerMs = 20;
|
||||
private int batchSize = 65536;
|
||||
private String acks = "all";
|
||||
private boolean enableIdempotence = true;
|
||||
/** 写入 envelope 的节点标识,进入 Protobuf 字段 ingest_node_id,便于追踪多实例来源。 */
|
||||
private String nodeId = "ingest-local";
|
||||
private Topics topics = new Topics();
|
||||
private Consumer consumer = new Consumer();
|
||||
@@ -54,12 +56,16 @@ public class SinkMqProperties {
|
||||
public void setConsumer(Consumer consumer) { this.consumer = consumer; }
|
||||
|
||||
public static class Topics {
|
||||
/** 实时遥测事件 topic;GB32960 RAW-only 架构下可逐步弱化该 topic。 */
|
||||
private String realtime = "vehicle.realtime";
|
||||
/** 位置事件 topic;通常可由 realtime/RAW 派生。 */
|
||||
private String location = "vehicle.location";
|
||||
private String alarm = "vehicle.alarm";
|
||||
private String session = "vehicle.session";
|
||||
private String mediaMeta = "vehicle.media.meta";
|
||||
/** RAW 归档引用 topic,payload 应携带 archive URI/size,不建议携带完整 raw bytes。 */
|
||||
private String rawArchive = "vehicle.raw.archive";
|
||||
/** producer 熔断或 consumer 处理失败时的死信 topic。 */
|
||||
private String dlq = "vehicle.dlq";
|
||||
|
||||
public String getRealtime() { return realtime; }
|
||||
@@ -79,6 +85,7 @@ public class SinkMqProperties {
|
||||
}
|
||||
|
||||
public static class Consumer {
|
||||
/** Kafka 消费总开关。与 producer 总开关分离,默认 false,避免单体服务意外自消费。 */
|
||||
private boolean enabled = false;
|
||||
private boolean autoStartup = true;
|
||||
private String clientIdPrefix = "lingniu-envelope-consumer";
|
||||
@@ -86,6 +93,12 @@ public class SinkMqProperties {
|
||||
private int loopBackoffMillis = 1000;
|
||||
private String autoOffsetReset = "earliest";
|
||||
private int maxPollRecords = 500;
|
||||
/**
|
||||
* Processor bean name -> Kafka binding。
|
||||
*
|
||||
* <p>示例 key:{@code eventHistoryEnvelopeConsumerProcessor}、
|
||||
* {@code vehicleStateEnvelopeConsumerProcessor}、{@code vehicleStatEnvelopeConsumerProcessor}。
|
||||
*/
|
||||
private Map<String, Binding> bindings = new LinkedHashMap<>();
|
||||
|
||||
public boolean isEnabled() { return enabled; }
|
||||
@@ -107,8 +120,11 @@ public class SinkMqProperties {
|
||||
}
|
||||
|
||||
public static class Binding {
|
||||
/** 单个 processor binding 开关,用于临时停某个下游消费者而不关整个 consumer runner。 */
|
||||
private boolean enabled = true;
|
||||
/** Kafka consumer group id。不同服务要独立消费同一 topic 时必须使用不同 group。 */
|
||||
private String groupId;
|
||||
/** 该 processor 订阅的 topic 列表。 */
|
||||
private List<String> topics = new ArrayList<>();
|
||||
|
||||
public boolean isEnabled() { return enabled; }
|
||||
|
||||
Reference in New Issue
Block a user