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

@@ -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);
}
}
}
}