|
|
@@ -0,0 +1,320 @@
|
|
|
+package cn.com.yusys.manager.instanceManager.Impl;
|
|
|
+
|
|
|
+import cn.com.yusys.manager.common.ParseInstanceStatusRegistry;
|
|
|
+import cn.com.yusys.manager.instanceManager.InstanceManager;
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
+
|
|
|
+import javax.annotation.PostConstruct;
|
|
|
+import javax.annotation.PreDestroy;
|
|
|
+import javax.annotation.Resource;
|
|
|
+import java.io.*;
|
|
|
+import java.nio.file.Files;
|
|
|
+import java.nio.file.Path;
|
|
|
+import java.nio.file.Paths;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.concurrent.*;
|
|
|
+import java.util.concurrent.atomic.AtomicBoolean;
|
|
|
+
|
|
|
+public class ProcessInstanceManager implements InstanceManager {
|
|
|
+
|
|
|
+ private static final Logger log = LoggerFactory.getLogger(ProcessInstanceManager.class);
|
|
|
+
|
|
|
+ // 用于消费子进程输出流的线程池,避免阻塞主进程
|
|
|
+ private final ExecutorService streamGobblerExecutor = Executors.newCachedThreadPool(r -> {
|
|
|
+ Thread t = new Thread(r, "Process-Stream-Gobbler");
|
|
|
+ t.setDaemon(true);
|
|
|
+ return t;
|
|
|
+ });
|
|
|
+
|
|
|
+
|
|
|
+ // 存储实例ID与进程信息的映射
|
|
|
+ // 建议使用自定义对象包裹 Process,以便管理更多状态
|
|
|
+ private final Map<String, ProcessInfo> processMap = new ConcurrentHashMap<>();
|
|
|
+
|
|
|
+ @Value("${parser.instance.script-path:parse_service.py}")
|
|
|
+ private String scriptPath;
|
|
|
+
|
|
|
+ @Value("${parser.instance.python-path:python3}")
|
|
|
+ private String pythonPath;
|
|
|
+
|
|
|
+ @Value("${parser.instance.work-dir:.}")
|
|
|
+ private String workDir;
|
|
|
+
|
|
|
+ @PostConstruct
|
|
|
+ public void init() {
|
|
|
+ // 获取项目根目录
|
|
|
+ File projectRoot = getProjectRoot();
|
|
|
+
|
|
|
+ // 解析工作目录
|
|
|
+ File dir;
|
|
|
+ if (workDir.startsWith(".")) {
|
|
|
+ // 相对路径,基于项目根目录解析
|
|
|
+ dir = new File(projectRoot, workDir.substring(1));
|
|
|
+ } else if (workDir.startsWith("..")) {
|
|
|
+ // 相对路径,基于项目根目录解析
|
|
|
+ dir = new File(projectRoot, workDir);
|
|
|
+ } else {
|
|
|
+ // 绝对路径或相对路径,直接使用
|
|
|
+ dir = new File(workDir);
|
|
|
+ if (!dir.isAbsolute()) {
|
|
|
+ // 如果不是绝对路径,基于项目根目录解析
|
|
|
+ dir = new File(projectRoot, workDir);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!dir.exists() || !dir.isDirectory()) {
|
|
|
+ log.error("工作目录不存在或不是目录:{}", dir.getAbsolutePath());
|
|
|
+ // 根据策略决定是否抛出异常阻止启动
|
|
|
+ throw new IllegalStateException("Invalid work directory: " + dir.getAbsolutePath());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新 workDir 为绝对路径
|
|
|
+ this.workDir = dir.getAbsolutePath();
|
|
|
+ log.info("ProcessInstanceManager 初始化完成,工作目录:{}", workDir);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取项目根目录(包含 parser 目录的目录)
|
|
|
+ * @return 项目根目录
|
|
|
+ */
|
|
|
+ private File getProjectRoot() {
|
|
|
+ try {
|
|
|
+ // 获取当前类的路径
|
|
|
+ String path = getClass().getProtectionDomain().getCodeSource().getLocation().toURI().getPath();
|
|
|
+ File file = new File(path);
|
|
|
+
|
|
|
+ // 如果是文件(如 JAR 包),获取其父目录
|
|
|
+ if (file.isFile()) {
|
|
|
+ file = file.getParentFile();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 向上查找项目根目录(同时包含 pom.xml 和 parser 目录)
|
|
|
+ while (file != null) {
|
|
|
+ File pomFile = new File(file, "pom.xml");
|
|
|
+ File parserDir = new File(file, "parser");
|
|
|
+ if (pomFile.exists() && parserDir.exists() && parserDir.isDirectory()) {
|
|
|
+ return file;
|
|
|
+ }
|
|
|
+ file = file.getParentFile();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果找不到,尝试使用当前工作目录
|
|
|
+ File currentDir = new File(System.getProperty("user.dir"));
|
|
|
+ File parserDir = new File(currentDir, "parser");
|
|
|
+ if (parserDir.exists() && parserDir.isDirectory()) {
|
|
|
+ return currentDir;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果当前工作目录也没有 parser 目录,尝试向上查找
|
|
|
+ File parentDir = currentDir.getParentFile();
|
|
|
+ if (parentDir != null) {
|
|
|
+ parserDir = new File(parentDir, "parser");
|
|
|
+ if (parserDir.exists() && parserDir.isDirectory()) {
|
|
|
+ return parentDir;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果都找不到,返回当前工作目录
|
|
|
+ log.warn("无法找到包含 parser 目录的项目根目录,使用当前工作目录");
|
|
|
+ return currentDir;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("无法获取项目根目录,使用当前工作目录", e);
|
|
|
+ return new File(System.getProperty("user.dir"));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String startParseInstance(int port) {
|
|
|
+ String instanceId = "python-parser-" + port;
|
|
|
+
|
|
|
+ // 双重检查锁或直接覆盖,这里选择先清理
|
|
|
+ if (processMap.containsKey(instanceId)) {
|
|
|
+ log.warn("实例 {} 已存在,正在强制清理...", instanceId);
|
|
|
+ terminateInstance(instanceId);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 解析绝对路径
|
|
|
+ Path scriptFullPath = Paths.get(workDir, scriptPath).toAbsolutePath();
|
|
|
+ if (!Files.exists(scriptFullPath)) {
|
|
|
+ throw new RuntimeException("解析脚本不存在:" + scriptFullPath);
|
|
|
+ }
|
|
|
+
|
|
|
+ ProcessBuilder processBuilder = new ProcessBuilder(
|
|
|
+ pythonPath,
|
|
|
+ scriptFullPath.toString(),
|
|
|
+ "--host", "0.0.0.0",
|
|
|
+ "--port", String.valueOf(port)
|
|
|
+ );
|
|
|
+
|
|
|
+ processBuilder.directory(new File(workDir));
|
|
|
+ // 不自动合并流,我们需要分别处理或统一由 Gobbler 处理,这里保持合并方便处理
|
|
|
+ processBuilder.redirectErrorStream(true);
|
|
|
+
|
|
|
+ try {
|
|
|
+ Process process = processBuilder.start();
|
|
|
+
|
|
|
+ // 【关键修复】启动后台线程消费输出流,防止阻塞
|
|
|
+ streamGobblerExecutor.submit(() -> gobbleStream(process, instanceId));
|
|
|
+
|
|
|
+ // 简单延迟检查,确认进程没有立即退出 (可选,更严谨的做法是异步监听 exitCode)
|
|
|
+ // 这里暂不 block 等待,假设启动即成功,依靠后续健康检查或日志发现崩溃
|
|
|
+
|
|
|
+ long pid = getPid(process);
|
|
|
+ ProcessInfo info = new ProcessInfo(process, instanceId, System.currentTimeMillis(), pid);
|
|
|
+ processMap.put(instanceId, info);
|
|
|
+
|
|
|
+ log.info("Python 实例启动成功,ID: {}, PID: {}, 端口:{}", instanceId, getPid(process), port);
|
|
|
+ return instanceId;
|
|
|
+
|
|
|
+ } catch (IOException e) {
|
|
|
+ log.error("启动 Python 实例失败:{}", e.getMessage(), e);
|
|
|
+ throw new RuntimeException("拉起 Python 实例失败:" + e.getMessage(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void terminateInstance(String instanceId) {
|
|
|
+ ProcessInfo info = processMap.remove(instanceId);
|
|
|
+ if (info == null) {
|
|
|
+ log.warn("未找到实例:{}", instanceId);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ Process process = info.process;
|
|
|
+ if (!process.isAlive()) {
|
|
|
+ log.info("实例 {} 已经退出,无需终止", instanceId);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ log.info("正在终止实例:{} (PID: {})", instanceId, getPid(process));
|
|
|
+
|
|
|
+ // 1. 尝试优雅终止
|
|
|
+ process.destroy();
|
|
|
+ try {
|
|
|
+ if (!process.waitFor(10, TimeUnit.SECONDS)) {
|
|
|
+ // 2. 强制终止
|
|
|
+ log.warn("实例 {} 未在 10s 内退出,执行 kill -9", instanceId);
|
|
|
+ process.destroyForcibly();
|
|
|
+ process.waitFor(5, TimeUnit.SECONDS);
|
|
|
+ }
|
|
|
+ log.info("实例 {} 已正常终止", instanceId);
|
|
|
+ } catch (InterruptedException e) {
|
|
|
+ Thread.currentThread().interrupt();
|
|
|
+ log.error("等待实例 {} 终止时被中断", instanceId, e);
|
|
|
+ process.destroyForcibly();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 后台消费子进程的输出流,防止缓冲区满导致子进程阻塞
|
|
|
+ */
|
|
|
+ private void gobbleStream(Process process, String instanceId) {
|
|
|
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
|
|
|
+ String line;
|
|
|
+ while ((line = reader.readLine()) != null) {
|
|
|
+ // 将 Python 的日志转发到 Java 的日志系统,带上实例ID便于排查
|
|
|
+ log.info("[Instance-{}] {}", instanceId, line);
|
|
|
+ }
|
|
|
+ } catch (IOException e) {
|
|
|
+ // 进程结束时流会关闭,这是正常的
|
|
|
+ if (process.isAlive()) {
|
|
|
+ log.error("读取实例 {} 输出流时发生异常", instanceId, e);
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ // 进程结束后,可以从 map 中清理(如果需要自动清理已退出的进程)
|
|
|
+ // 这里可以选择移除,或者保留状态供查询
|
|
|
+ log.debug("实例 {} 的输出流读取线程结束", instanceId);
|
|
|
+ // 可选:如果是意外退出,可以在这里回调通知注册中心更新状态
|
|
|
+ checkAndRemoveIfExited(instanceId, process);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void checkAndRemoveIfExited(String instanceId, Process process) {
|
|
|
+ // 简单的延迟检查,实际生产中可能需要更复杂的回调机制
|
|
|
+ try {
|
|
|
+ Thread.sleep(100);
|
|
|
+ if (!process.isAlive()) {
|
|
|
+ int exitCode = process.exitValue();
|
|
|
+ log.error("实例 {} 意外退出,退出码:{}", instanceId, exitCode);
|
|
|
+ processMap.remove(instanceId); // 移除死进程
|
|
|
+ // TODO: 通知 instanceStateRegistry 更新状态为 FAILED
|
|
|
+ }
|
|
|
+ } catch (InterruptedException e) {
|
|
|
+ Thread.currentThread().interrupt();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public Map<String, Process> getAllProcesses() {
|
|
|
+ // 返回只读视图或转换后的 Map,暴露内部 Process 对象需谨慎
|
|
|
+ Map<String, Process> result = new HashMap<>();
|
|
|
+ processMap.forEach((k, v) -> {
|
|
|
+ if (v.process.isAlive()) {
|
|
|
+ result.put(k, v.process);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 应用关闭时,清理所有子进程
|
|
|
+ */
|
|
|
+ @PreDestroy
|
|
|
+ public void destroyAll() {
|
|
|
+ log.info("正在关闭 ProcessInstanceManager,终止所有活跃实例...");
|
|
|
+ for (String id : new HashMap<>(processMap).keySet()) {
|
|
|
+ terminateInstance(id);
|
|
|
+ }
|
|
|
+ streamGobblerExecutor.shutdownNow();
|
|
|
+ log.info("所有解析实例已清理完毕");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 内部类,用于封装进程信息
|
|
|
+ private static class ProcessInfo {
|
|
|
+ final Process process;
|
|
|
+ final String instanceId;
|
|
|
+ final long startTime;
|
|
|
+ final Long pid; // 存储进程PID
|
|
|
+
|
|
|
+ public ProcessInfo(Process process, String instanceId, long startTime, Long pid) {
|
|
|
+ this.process = process;
|
|
|
+ this.instanceId = instanceId;
|
|
|
+ this.startTime = startTime;
|
|
|
+ this.pid = pid;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Long getPid(String instanceId) {
|
|
|
+ ProcessInfo info = processMap.get(instanceId);
|
|
|
+ if (info == null) {
|
|
|
+ log.warn("未找到实例:{}", instanceId);
|
|
|
+ return -1L;
|
|
|
+ }
|
|
|
+ return info.pid;
|
|
|
+ }
|
|
|
+
|
|
|
+ private long getPid(Process process) {
|
|
|
+ if (process == null) return -1;
|
|
|
+
|
|
|
+ // 尝试通过反射获取 (主要针对 Linux/Mac 的 UNIXProcess)
|
|
|
+ if (process.getClass().getName().equals("java.lang.UNIXProcess")) {
|
|
|
+ try {
|
|
|
+ java.lang.reflect.Field field = process.getClass().getDeclaredField("pid");
|
|
|
+ field.setAccessible(true);
|
|
|
+ return field.getLong(process);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("反射获取 PID 失败", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果是 Windows 或其他情况,Java 8 很难直接获取 PID
|
|
|
+ // 如果项目确定只跑在 Linux/Mac 上,上面的代码就够了。
|
|
|
+ // 如果需要支持 Windows Java 8,建议忽略 PID 日志或升级 JDK。
|
|
|
+
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+}
|