fix(ft546): serialize equipment command sends
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user