package com.xcong.excoin.modules.station.registry; import com.xcong.excoin.modules.station.model.HeartbeatMsg; import com.xcong.excoin.modules.station.model.InstanceInfo; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.util.Collection; import java.util.concurrent.ConcurrentHashMap; /** * JAR 实例注册表 — 心跳更新,超时自动清理。 */ @Slf4j @Component public class InstanceRegistry { /** 30s 心跳 × 2 = 60s 超时下线 */ private static final long TIMEOUT_MS = 60_000; private final ConcurrentHashMap registry = new ConcurrentHashMap<>(); /** * 心跳更新(不存在则新增) */ public void update(String apiKeyMd5, HeartbeatMsg msg) { InstanceInfo info = InstanceInfo.builder() .apiKeyMd5(apiKeyMd5) .contract(msg.getContract()) .state(msg.getState()) .leverage(msg.getLeverage()) .currentRound(msg.getCurrentRound()) .cumulativePnl(msg.getCumulativePnl()) .principal(msg.getPrincipal()) .hostPort(msg.getHostPort()) .lastSeen(System.currentTimeMillis()) .build(); InstanceInfo old = registry.put(apiKeyMd5, info); if (old == null) { log.info("[Station] 实例上线, apiKeyMd5={}, contract={}", apiKeyMd5, msg.getContract()); } } /** * 更新状态(用于 ACK) */ public void updateState(String apiKeyMd5, String newState) { InstanceInfo info = registry.get(apiKeyMd5); if (info != null) { info.setState(newState); info.setLastSeen(System.currentTimeMillis()); } } /** * 获取单个实例 */ public InstanceInfo get(String apiKeyMd5) { return registry.get(apiKeyMd5); } /** * 所有在线实例 */ public Collection list() { return registry.values(); } /** * 每 30s 清理超时实例 */ @Scheduled(fixedRate = 30_000) public void cleanDead() { long now = System.currentTimeMillis(); registry.entrySet().removeIf(entry -> { boolean dead = now - entry.getValue().getLastSeen() > TIMEOUT_MS; if (dead) { log.info("[Station] 实例离线, apiKeyMd5={}, 最后心跳:{}ms前", entry.getKey(), now - entry.getValue().getLastSeen()); } return dead; }); } }