fix(ft546): serialize equipment command sends

This commit is contained in:
zgw
2026-08-11 14:39:26 +08:00
parent 14a5a6174e
commit e5f91de1a0
4 changed files with 213 additions and 65 deletions

View File

@@ -24,10 +24,17 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class Ft546EquipmentAdaptersTest {
private static final Instant NOW = Instant.parse("2026-08-11T05:00:00Z");
@@ -153,6 +160,35 @@ class Ft546EquipmentAdaptersTest {
assertEquals(1, runtime.payloads.size());
}
@Test
void rejectsConcurrentEquipmentSendWhileFirstWriteIsInProgress()
throws Exception {
FakeCdzRead runtime = new FakeCdzRead("1", chargerDevice());
runtime.blockFirstWrite();
Ft546ChargerAdapter adapter = new Ft546ChargerAdapter(runtime,
profile("1", "CHARGER", "CHARGE", "DB701.0"),
(deviceId, commandId) -> false, contextProvider());
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
Future<SendReceipt> first = executor.submit(() -> adapter.send(
envelope("CHARGE-1", "1", DeviceCommandType.CHARGE,
"enabled=1")));
assertTrue(runtime.firstWriteEntered.await(2, TimeUnit.SECONDS));
SendReceipt second = adapter.send(envelope("CHARGE-2", "1",
DeviceCommandType.CHARGE, "enabled=1"));
runtime.releaseFirstWrite.countDown();
assertEquals(SendReceipt.Status.ACCEPTED,
first.get(2, TimeUnit.SECONDS).getStatus());
assertEquals(SendReceipt.Status.REJECTED, second.getStatus());
assertEquals(1, runtime.writeCalls.get());
} finally {
runtime.releaseFirstWrite.countDown();
executor.shutdownNow();
}
}
@Test
void factReadersPublishAConnectionDropAfterTheyWereConnected() {
FakeTsjRead lift = new FakeTsjRead("1", liftDevice());
@@ -266,9 +302,17 @@ class Ft546EquipmentAdaptersTest {
}
private static final class FakeCdzRead extends PlcCdzRead {
private final List<String> addresses = new ArrayList<String>();
private final List<byte[]> payloads = new ArrayList<byte[]>();
private final List<String> addresses = Collections.synchronizedList(
new ArrayList<String>());
private final List<byte[]> payloads = Collections.synchronizedList(
new ArrayList<byte[]>());
private final CountDownLatch firstWriteEntered =
new CountDownLatch(1);
private final CountDownLatch releaseFirstWrite =
new CountDownLatch(1);
private int writeResult = 1;
private volatile boolean blockFirstWrite;
private final AtomicInteger writeCalls = new AtomicInteger();
private FakeCdzRead(String id, DeviceCdz device) {
super(map(id, device), new PlcConnectInfo("127.0.0.1", 102));
@@ -277,10 +321,28 @@ class Ft546EquipmentAdaptersTest {
@Override
public int write(String address, byte[] payload) {
int call = writeCalls.incrementAndGet();
if (blockFirstWrite && call == 1) {
firstWriteEntered.countDown();
await(releaseFirstWrite);
}
addresses.add(address);
payloads.add(payload.clone());
return writeResult;
}
private void blockFirstWrite() {
blockFirstWrite = true;
}
private static void await(CountDownLatch latch) {
try {
latch.await();
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
throw new IllegalStateException(interrupted);
}
}
}
private static ConcurrentHashMap<String, DeviceTsj> map(

View File

@@ -22,6 +22,12 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -149,6 +155,34 @@ class Ft546LegacySrgvAdapterTest {
assertTrue(runtime.writes.isEmpty());
}
@Test
void rejectsConcurrentShuttleSendWithoutInterleavingRegisterSequences()
throws Exception {
FakePlcSrgvRead runtime = connectedRuntime("1");
runtime.blockFirstWrite();
Ft546LegacySrgvAdapter adapter = adapter(runtime, pointProfile(),
null, (deviceId, commandId) -> false);
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
Future<SendReceipt> first = executor.submit(() -> adapter.send(
envelope("CMD-1",
"taskNo=30001|taskType=1|route=070501>070502")));
assertTrue(runtime.firstWriteEntered.await(2, TimeUnit.SECONDS));
SendReceipt second = adapter.send(envelope("CMD-2",
"taskNo=30002|taskType=1|route=070503>070504"));
runtime.releaseFirstWrite.countDown();
assertEquals(SendReceipt.Status.ACCEPTED,
first.get(2, TimeUnit.SECONDS).getStatus());
assertEquals(SendReceipt.Status.REJECTED, second.getStatus());
assertEquals(4, runtime.writes.size());
} finally {
runtime.releaseFirstWrite.countDown();
executor.shutdownNow();
}
}
@Test
void twoStageProfileIsRejectedUntilExactEvidencePasses() {
FakePlcSrgvRead runtime = connectedRuntime("1");
@@ -340,23 +374,32 @@ class Ft546LegacySrgvAdapterTest {
private static final class FakePlcSrgvRead extends PlcSrgvRead {
private final List<RecordedWrite> writes =
new ArrayList<RecordedWrite>();
Collections.synchronizedList(
new ArrayList<RecordedWrite>());
private final CountDownLatch firstWriteEntered =
new CountDownLatch(1);
private final CountDownLatch releaseFirstWrite =
new CountDownLatch(1);
private String throwAfterAddress;
private String returnFalseAfterAddress;
private volatile boolean blockFirstWrite;
private final AtomicInteger writeCalls = new AtomicInteger();
private FakePlcSrgvRead(DeviceSrgv device) {
super(device, new PlcConnectInfo("127.0.0.1", 502));
}
@Override
public synchronized boolean Write(String address, int[] value) {
public boolean Write(String address, int[] value) {
beforeWrite();
record(address, "INT32", value);
maybeThrow(address);
return !address.equals(returnFalseAfterAddress);
}
@Override
public synchronized boolean Write(String address, short[] value) {
public boolean Write(String address, short[] value) {
beforeWrite();
List<Integer> values = new ArrayList<Integer>();
for (short item : value) {
values.add((int) item);
@@ -366,6 +409,27 @@ class Ft546LegacySrgvAdapterTest {
return !address.equals(returnFalseAfterAddress);
}
private void blockFirstWrite() {
blockFirstWrite = true;
}
private void beforeWrite() {
int call = writeCalls.incrementAndGet();
if (blockFirstWrite && call == 1) {
firstWriteEntered.countDown();
await(releaseFirstWrite);
}
}
private static void await(CountDownLatch latch) {
try {
latch.await();
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
throw new IllegalStateException(interrupted);
}
}
private void record(String address, String kind, int[] values) {
List<Integer> converted = new ArrayList<Integer>();
Arrays.stream(values).forEach(converted::add);