From edafe7af940864e353245c4bfff87c0fc1956b87 Mon Sep 17 00:00:00 2001 From: zgw <13623675548@163.com> Date: Tue, 11 Aug 2026 12:10:24 +0800 Subject: [PATCH] fix(ft546): serialize shuttle reader lifecycle --- .../shuttle/Ft546SrgvRuntimeFactory.java | 3 + .../com/ruoyi/schedule/ScheduleThread.java | 136 +++++++++++++++--- .../Ft546DeviceRuntimeRegistryTest.java | 30 ++++ 3 files changed, 151 insertions(+), 18 deletions(-) diff --git a/ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/shuttle/Ft546SrgvRuntimeFactory.java b/ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/shuttle/Ft546SrgvRuntimeFactory.java index 8ba036f..cfb010c 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/shuttle/Ft546SrgvRuntimeFactory.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/shuttle/Ft546SrgvRuntimeFactory.java @@ -38,6 +38,9 @@ public class Ft546SrgvRuntimeFactory { } public PlcSrgvRead create(DeviceSrgv device) { + if (device == null) { + throw new IllegalArgumentException("device identity is required"); + } Ft546DeviceProfile profile = new Ft546DeviceProfile(); profile.setDeviceId(String.valueOf(device.getDeviceId())); profile.setDeviceType("SHUTTLE"); diff --git a/ruoyi-admin/src/main/java/com/ruoyi/schedule/ScheduleThread.java b/ruoyi-admin/src/main/java/com/ruoyi/schedule/ScheduleThread.java index 6bd22aa..96f9ab6 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/schedule/ScheduleThread.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/schedule/ScheduleThread.java @@ -30,6 +30,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -66,8 +67,10 @@ public class ScheduleThread { @Autowired private Ft546SrgvRuntimeFactory srgvRuntimeFactory; - private boolean isRun = false; - private boolean isRun2 = false; + private volatile boolean isRun = false; + private volatile boolean isRun2 = false; + + private static final long THREAD_STOP_TIMEOUT_MS = 5000L; /** * 项目初始化启动 @@ -171,16 +174,21 @@ public class ScheduleThread { /** * 开始线程 */ - public void startAllThread() { - plcSsjReadCustomThread.getThread().start(); - for (CustomThread reader - : plcSrgvReadCustomThreads.values()) { - reader.getThread().start(); + public synchronized void startAllThread() { + if (isRun) { + return; + } + List readerThreads = requireFactReaderThreads(); + for (Thread readerThread : readerThreads) { + if (readerThread.getState() != Thread.State.NEW) { + throw new IllegalStateException( + "PLC fact reader cannot be restarted: " + + readerThread.getName()); + } + } + for (Thread readerThread : readerThreads) { + readerThread.start(); } - plcTsjReadCustomThread.getThread().start(); - plcCdzReadCustomThread.getThread().start(); - - isRun = true; } @@ -200,14 +208,18 @@ public class ScheduleThread { /** * 停止线程 */ - public void stopAllThread() { - plcSsjReadCustomThread.runnable.isRun = false; - for (CustomThread reader - : plcSrgvReadCustomThreads.values()) { - reader.runnable.isRun = false; + public synchronized void stopAllThread() { + if (!isRun) { + return; } + List readerThreads = currentFactReaderThreads(); + signalFactReadersToStop(); + isRun = false; + awaitFactReadersStopped(readerThreads); plcSrgvReadCustomThreads.clear(); - deviceRuntimeRegistry.clear(); + if (deviceRuntimeRegistry != null) { + deviceRuntimeRegistry.clear(); + } // plcDdj1ReadCustomThread.runnable.isRun = false; /* plcDdj2ReadCustomThread.runnable.isRun = false; plcDdj3ReadCustomThread.runnable.isRun = false; @@ -215,7 +227,95 @@ public class ScheduleThread { plcDdj5ReadCustomThread.runnable.isRun = false; plcDdj6ReadCustomThread.runnable.isRun = false;*/ plcReadInit(); - isRun = false; + } + + private List requireFactReaderThreads() { + List threads = new ArrayList(); + threads.add(requireThread(plcSsjReadCustomThread, + "conveyor fact reader")); + for (Map.Entry> entry + : plcSrgvReadCustomThreads.entrySet()) { + threads.add(requireThread(entry.getValue(), + "shuttle fact reader " + entry.getKey())); + } + threads.add(requireThread(plcTsjReadCustomThread, + "lift fact reader")); + threads.add(requireThread(plcCdzReadCustomThread, + "charger fact reader")); + return threads; + } + + private List currentFactReaderThreads() { + List threads = new ArrayList(); + addThreadIfPresent(threads, plcSsjReadCustomThread); + for (CustomThread reader + : plcSrgvReadCustomThreads.values()) { + addThreadIfPresent(threads, reader); + } + addThreadIfPresent(threads, plcTsjReadCustomThread); + addThreadIfPresent(threads, plcCdzReadCustomThread); + return threads; + } + + private void signalFactReadersToStop() { + if (plcSsjReadCustomThread != null + && plcSsjReadCustomThread.runnable != null) { + plcSsjReadCustomThread.runnable.isRun = false; + } + for (CustomThread reader + : plcSrgvReadCustomThreads.values()) { + if (reader != null && reader.runnable != null) { + reader.runnable.isRun = false; + } + } + if (plcTsjReadCustomThread != null + && plcTsjReadCustomThread.runnable != null) { + plcTsjReadCustomThread.runnable.isRun = false; + } + if (plcCdzReadCustomThread != null + && plcCdzReadCustomThread.runnable != null) { + plcCdzReadCustomThread.runnable.isRun = false; + } + } + + private void awaitFactReadersStopped(List readerThreads) { + long deadline = System.currentTimeMillis() + THREAD_STOP_TIMEOUT_MS; + for (Thread readerThread : readerThreads) { + long remaining = deadline - System.currentTimeMillis(); + if (remaining <= 0L) { + break; + } + try { + readerThread.join(remaining); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw new IllegalStateException( + "interrupted while stopping PLC fact readers", + interrupted); + } + } + for (Thread readerThread : readerThreads) { + if (readerThread.isAlive()) { + throw new IllegalStateException( + "PLC fact reader did not stop: " + + readerThread.getName()); + } + } + } + + private static Thread requireThread(CustomThread customThread, + String name) { + if (customThread == null || customThread.getThread() == null) { + throw new IllegalStateException(name + " is not initialized"); + } + return customThread.getThread(); + } + + private static void addThreadIfPresent(List threads, + CustomThread customThread) { + if (customThread != null && customThread.getThread() != null) { + threads.add(customThread.getThread()); + } } /** diff --git a/ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device/Ft546DeviceRuntimeRegistryTest.java b/ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device/Ft546DeviceRuntimeRegistryTest.java index 603e041..285761d 100644 --- a/ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device/Ft546DeviceRuntimeRegistryTest.java +++ b/ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device/Ft546DeviceRuntimeRegistryTest.java @@ -1,9 +1,11 @@ package com.ruoyi.project.ft546.device; import com.ruoyi.common.domain.PlcConnectInfo; +import com.ruoyi.common.domain.CustomThread; import com.ruoyi.device.srgv.domain.DeviceSrgv; import com.ruoyi.project.ft546.config.Ft546DeviceProfile; import com.ruoyi.project.ft546.device.shuttle.Ft546SrgvRuntimeFactory; +import com.ruoyi.schedule.ScheduleThread; import com.ruoyi.schedule.plcRead.srgv.PlcSrgvRead; import org.junit.jupiter.api.Test; @@ -14,6 +16,7 @@ import java.nio.file.Paths; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -55,6 +58,29 @@ class Ft546DeviceRuntimeRegistryTest { () -> new Ft546SrgvRuntimeFactory().create(device, profile)); } + @Test + void factoryRejectsMissingDeviceWithTheDeclaredValidationError() { + assertThrows(IllegalArgumentException.class, + () -> new Ft546SrgvRuntimeFactory().create(null)); + } + + @Test + void startingFactReadersTwiceIsIdempotent() { + ScheduleThread schedule = new ScheduleThread(); + schedule.plcSsjReadCustomThread = noOpThread(); + schedule.plcTsjReadCustomThread = noOpThread(); + schedule.plcCdzReadCustomThread = noOpThread(); + + schedule.startAllThread(); + + assertDoesNotThrow(schedule::startAllThread); + } + + @Test + void stoppingBeforeInitializationIsANoOp() { + assertDoesNotThrow(() -> new ScheduleThread().stopAllThread()); + } + @Test void schedulingAndLegacySendPathsDoNotSelectThreeFixedReaders() throws Exception { @@ -75,6 +101,10 @@ class Ft546DeviceRuntimeRegistryTest { new PlcConnectInfo(device.getIp(), device.getPort())); } + private static CustomThread noOpThread() { + return new CustomThread(new Thread(() -> { }), null); + } + private static DeviceSrgv device(String id) { DeviceSrgv device = new DeviceSrgv(); device.setDeviceId(Integer.valueOf(id));