fix: enforce gb32960 kafka ack boundary

This commit is contained in:
lingniu
2026-06-23 17:27:12 +08:00
parent 633b3ea9c9
commit a80b38bd04
6 changed files with 474 additions and 30 deletions

View File

@@ -20,6 +20,7 @@ import org.slf4j.LoggerFactory;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.time.Instant;
import java.util.concurrent.CompletableFuture;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -55,6 +56,9 @@ public class Gb32960ChannelHandler extends SimpleChannelInboundHandler<byte[]> {
public static final AttributeKey<String> PLATFORM_ACCOUNT_ATTR =
Gb32960AccessService.PLATFORM_ACCOUNT_ATTR;
private static final AttributeKey<CompletableFuture<Void>> DURABLE_ACK_CHAIN_ATTR =
AttributeKey.valueOf("gb32960.durableAckChain");
private final Gb32960MessageDecoder decoder;
private final Dispatcher dispatcher;
private final Gb32960AccessService accessService;
@@ -123,7 +127,7 @@ public class Gb32960ChannelHandler extends SimpleChannelInboundHandler<byte[]> {
}
// 鉴权通过:先完成命令侧状态变更/日志,再统一 dispatch 到通用管线。
// 需要成功 ACK 的已接收帧,等待 dispatch 的持久化边界完成后再回 ACK。
// 需要成功 ACK 的已接收帧,等待配置的 Kafka dispatch 边界完成后再回 ACK。
// 平台登入鉴权失败会短路 return不进 dispatch。
switch (cmd) {
case VEHICLE_LOGIN -> logVehicleLogin(ctx, msg);
@@ -143,16 +147,62 @@ public class Gb32960ChannelHandler extends SimpleChannelInboundHandler<byte[]> {
return;
}
dispatcher.dispatchAndAwait(rf).whenComplete((ignored, ex) ->
ctx.executor().execute(() -> {
if (ex != null) {
log.warn("[gb32960] dispatch failed before ack peer={} vin={} cmd=0x{}",
addr(ctx), vin, Integer.toHexString(cmd.code()), ex);
ctx.close();
return;
}
writeAckForCommand(ctx, msg, rawVin);
}));
enqueueDurableAck(ctx, msg, rawVin, vin, cmd, dispatcher.dispatchAndAwait(rf));
}
private void enqueueDurableAck(ChannelHandlerContext ctx,
Gb32960Message msg,
byte[] rawVin,
String vin,
CommandType cmd,
CompletableFuture<Void> dispatchFuture) {
CompletableFuture<Void> previous = ctx.channel().attr(DURABLE_ACK_CHAIN_ATTR).get();
if (previous == null) {
previous = CompletableFuture.completedFuture(null);
}
CompletableFuture<Void> next = previous
.handle((ignored, previousFailure) -> null)
.thenCompose(ignored -> dispatchFuture.handle((ok, failure) -> failure))
.thenCompose(failure -> runOnEventLoop(ctx,
() -> handleDurableAckResult(ctx, msg, rawVin, vin, cmd, failure)));
ctx.channel().attr(DURABLE_ACK_CHAIN_ATTR).set(next);
}
private CompletableFuture<Void> runOnEventLoop(ChannelHandlerContext ctx, Runnable task) {
CompletableFuture<Void> done = new CompletableFuture<>();
Runnable wrapped = () -> {
try {
task.run();
done.complete(null);
} catch (Throwable t) {
done.completeExceptionally(t);
}
};
if (ctx.executor().inEventLoop()) {
wrapped.run();
} else {
ctx.executor().execute(wrapped);
}
return done;
}
private void handleDurableAckResult(ChannelHandlerContext ctx,
Gb32960Message msg,
byte[] rawVin,
String vin,
CommandType cmd,
Throwable failure) {
if (!ctx.channel().isActive()) {
return;
}
if (failure != null) {
log.warn("[gb32960] dispatch failed before ack peer={} vin={} cmd=0x{}",
addr(ctx), vin, Integer.toHexString(cmd.code()), failure);
ctx.close();
return;
}
writeAckForCommand(ctx, msg, rawVin);
}
private RawFrame rawFrame(ChannelHandlerContext ctx, Gb32960Message msg, byte[] frame) {
@@ -304,11 +354,11 @@ public class Gb32960ChannelHandler extends SimpleChannelInboundHandler<byte[]> {
}
/**
* 0x02/0x03/0x07 实时 / 补发 / 心跳:记录帧诊断;成功 ACK 在 dispatch 持久化边界后写回。
* 0x02/0x03/0x07 实时 / 补发 / 心跳:记录帧诊断;成功 ACK 在配置的 Kafka dispatch 边界后写回。
* 部分车端实现严格按规范没收到应答会停止后续上报或重连。ack-tag 按 cmd.code 区分便于日志筛选。
*
* <p>注意:应答成功不代表下游已完成持久化,只表示平台已接收并通过 Dispatcher 投递
* 生产排查存储问题时,应继续检查 raw archive sink、EventFileStoreSink 和 Kafka sink 的日志。
* <p>注意:应答成功表示当前配置的 Kafka sink 已完成该帧映射事件的 dispatch 边界
* 生产排查其他存储问题时,应继续检查 raw archive sink、EventFileStoreSink 等可选 sink 的日志。
*
* <p>日志策略(按 (channel, vin, rawTypeSignature) 去重,避免高频帧刷屏):
* <ul>

View File

@@ -2,6 +2,7 @@ package com.lingniu.ingest.protocol.gb32960.inbound;
import com.lingniu.ingest.api.event.VehicleEvent;
import com.lingniu.ingest.api.sink.EventSink;
import com.lingniu.ingest.codec.BccChecksum;
import com.lingniu.ingest.core.concurrency.AsyncBatchExecutor;
import com.lingniu.ingest.core.concurrency.DisruptorEventBus;
import com.lingniu.ingest.core.dispatcher.Dispatcher;
@@ -21,6 +22,9 @@ import io.netty.buffer.ByteBuf;
import io.netty.channel.embedded.EmbeddedChannel;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@@ -36,6 +40,7 @@ class Gb32960ChannelHandlerAckBoundaryTest {
assertThat(channel.writeInbound(Gb32960DecoderTest.buildRealtimeFrame("LTEST000000000001"))).isFalse();
assertThat((Object) channel.readOutbound()).isNull();
dispatch.awaitPublishCount(1);
dispatch.completeDurability();
channel.runPendingTasks();
@@ -55,6 +60,7 @@ class Gb32960ChannelHandlerAckBoundaryTest {
assertThat(channel.writeInbound(Gb32960DecoderTest.buildRealtimeFrame("LTEST000000000001"))).isFalse();
dispatch.awaitPublishCount(1);
dispatch.failDurability();
channel.runPendingTasks();
@@ -64,6 +70,35 @@ class Gb32960ChannelHandlerAckBoundaryTest {
}
}
@Test
void durableAcksAreWrittenInInboundOrderWhenLaterDispatchCompletesFirst() {
try (DispatchHarness dispatch = new DispatchHarness()) {
EmbeddedChannel channel = new EmbeddedChannel(handlerWith(dispatch.dispatcher()));
assertThat(channel.writeInbound(Gb32960DecoderTest.buildRealtimeFrame("LTEST000000000001"))).isFalse();
assertThat(channel.writeInbound(buildHeartbeatFrame("LTEST000000000001"))).isFalse();
assertThat((Object) channel.readOutbound()).isNull();
dispatch.awaitPublishCount(2);
dispatch.completeDurability(1);
channel.runPendingTasks();
assertThat((Object) channel.readOutbound()).isNull();
dispatch.completeDurability(0);
channel.runPendingTasks();
ByteBuf firstAck = channel.readOutbound();
ByteBuf secondAck = channel.readOutbound();
assertThat(firstAck).isNotNull();
assertThat(secondAck).isNotNull();
assertThat(firstAck.getUnsignedByte(2)).isEqualTo((short) 0x02);
assertThat(secondAck.getUnsignedByte(2)).isEqualTo((short) 0x07);
firstAck.release();
secondAck.release();
channel.finishAndReleaseAll();
}
}
private static Gb32960ChannelHandler handlerWith(Dispatcher dispatcher) {
return new Gb32960ChannelHandler(
decoder(),
@@ -87,6 +122,23 @@ class Gb32960ChannelHandlerAckBoundaryTest {
new Gb32960PlatformAuthorizer(auth.getPlatforms()));
}
private static byte[] buildHeartbeatFrame(String vin) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(0x23);
out.write(0x23);
out.write(0x07);
out.write(0xFE);
byte[] vinBytes = vin.getBytes(StandardCharsets.US_ASCII);
out.write(vinBytes, 0, 17);
out.write(0x01);
out.write(0x00);
out.write(0x00);
byte[] almost = out.toByteArray();
out.write(BccChecksum.compute(almost, 2, almost.length - 2) & 0xFF);
return out.toByteArray();
}
private static final class DispatchHarness implements AutoCloseable {
private final ControlledSink sink = new ControlledSink();
private final DisruptorEventBus eventBus = new DisruptorEventBus(1024, "blocking", List.of(sink));
@@ -103,11 +155,28 @@ class Gb32960ChannelHandlerAckBoundaryTest {
}
void completeDurability() {
sink.future.complete(null);
completeDurability(0);
}
void completeDurability(int index) {
sink.future(index).complete(null);
}
void failDurability() {
sink.future.completeExceptionally(new IllegalStateException("durability failed"));
sink.future(0).completeExceptionally(new IllegalStateException("durability failed"));
}
void awaitPublishCount(int count) {
long deadline = System.currentTimeMillis() + 3000;
while (System.currentTimeMillis() < deadline) {
synchronized (sink.futures) {
if (sink.futures.size() >= count) {
return;
}
}
Thread.onSpinWait();
}
throw new AssertionError("expected " + count + " sink publishes, actual=" + sink.futures.size());
}
@Override
@@ -118,16 +187,26 @@ class Gb32960ChannelHandlerAckBoundaryTest {
}
private static final class ControlledSink implements EventSink {
private final CompletableFuture<Void> future = new CompletableFuture<>();
private final List<CompletableFuture<Void>> futures = new ArrayList<>();
@Override
public String name() {
return "controlled";
return "kafka";
}
@Override
public CompletableFuture<Void> publish(VehicleEvent event) {
CompletableFuture<Void> future = new CompletableFuture<>();
synchronized (futures) {
futures.add(future);
}
return future;
}
private CompletableFuture<Void> future(int index) {
synchronized (futures) {
return futures.get(index);
}
}
}
}