refactor: apply Option B, remove heartbeat and register tools once on startup
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 44s

This commit is contained in:
jade
2026-08-13 17:34:32 +09:00
parent 34e678ad63
commit 8f1c64ac58
177 changed files with 718 additions and 170 deletions

View File

@@ -18,8 +18,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
// Gateway가 /mcp Endpoint를 MCP Server로 노출하도록 지원합니다.
implementation 'org.springframework.ai:spring-ai-starter-mcp-server-webmvc'
// Gateway MCP Server/Client는 공통 모듈이 제공하는 MCP Java SDK 2.0.0을 사용합니다.
// ChatClient를 통해 OpenAI/OpenRouter 호환 모델을 호출합니다.
implementation 'org.springframework.ai:spring-ai-starter-model-openai'

View File

@@ -250,13 +250,5 @@ public class McpRouterController {
return ResponseEntity.ok("Deregistered");
}
@PostMapping("/registry/heartbeat")
public ResponseEntity<String> heartbeat(@RequestBody String uid) {
boolean success = registryService.refreshHeartbeat(uid);
if (success) {
return ResponseEntity.ok("Heartbeat updated");
} else {
return ResponseEntity.status(404).body("Tool not found");
}
}
}

View File

@@ -58,7 +58,7 @@ public class ScaffoldingController {
@PostMapping("/pod")
public String scaffoldPod(@RequestBody Map<String, String> req) {
try {
String moduleName = req.getOrDefault("moduleName", "dap-was-oth");
String moduleName = req.getOrDefault("moduleName", "dap-was-cus");
if (!moduleName.startsWith("dap-was-")) moduleName = "dap-was-" + moduleName;
String port = req.getOrDefault("port", "8085");
String shortName = moduleName.replace("dap-was-", "").replace("-", "");
@@ -83,7 +83,7 @@ public class ScaffoldingController {
String description = req.get("description");
String group = req.getOrDefault("categoryKey", req.getOrDefault("group", "COMMON"));
String routingType = req.getOrDefault("routingType", "HTTP");
String moduleName = req.getOrDefault("moduleName", "dap-was-oth");
String moduleName = req.getOrDefault("moduleName", "dap-was-cus");
String author = req.get("author");
if (author == null || author.trim().isEmpty()) author = System.getProperty("user.name");
String date = req.get("date");
@@ -124,7 +124,7 @@ public class ScaffoldingController {
throw new IllegalArgumentException("UseCase name must be PascalCase.");
}
String moduleName = request.moduleName() == null || request.moduleName().isBlank()
? "dap-was-oth" : request.moduleName().trim();
? "dap-was-cus" : request.moduleName().trim();
String author = request.author() == null || request.author().isBlank()
? System.getProperty("user.name") : request.author().trim();
String date = request.date() == null || request.date().isBlank()
@@ -248,12 +248,12 @@ public class ScaffoldingController {
File[] files = dir.listFiles(f -> f.isDirectory() && f.getName().startsWith("dap-was-") && !f.getName().equals("dap-was-lib"));
if (files == null || files.length == 0) {
return List.of("dap-was-oth", "dap-was-hr", "dap-was-sms");
return List.of("dap-was-cus", "dap-was-sal", "dap-was-pro", "dap-was-sys");
}
return Arrays.stream(files).map(File::getName).sorted().collect(Collectors.toList());
} catch (Exception e) {
return List.of("dap-was-oth", "dap-was-hr", "dap-was-sms");
return List.of("dap-was-cus", "dap-was-sal", "dap-was-pro", "dap-was-sys");
}
}

View File

@@ -30,26 +30,10 @@ public class InMemoryRegistryService {
* 툴 등록 및 갱신
*/
public void saveTool(ToolMetadata meta) {
meta.setLastHeartbeat(System.currentTimeMillis());
toolCache.put(meta.getUid(), meta);
log.info(" [InMemoryRegistry] 툴 등록 완료: {}", meta.getUid());
}
/**
* 하트비트 갱신 (TTL 초기화)
*/
public boolean refreshHeartbeat(String uid) {
ToolMetadata meta = toolCache.get(uid);
if (meta != null) {
meta.setLastHeartbeat(System.currentTimeMillis());
log.debug(" [InMemoryRegistry] 하트비트 갱신: {}", uid);
return true;
} else {
log.warn(" [InMemoryRegistry] 존재하지 않는 툴에 대한 하트비트 요청: {}", uid);
return false;
}
}
public List<String> getAvailablePods(String uid) {
ToolMetadata tool = getTool(uid);
if (tool != null && tool.getPodUrl() != null) {
@@ -91,21 +75,4 @@ public class InMemoryRegistryService {
toolCache.remove(uid);
log.info(" [InMemoryRegistry] 툴 삭제 완료: {}", uid);
}
/**
* 만료된 툴을 스케줄러로 삭제합니다. (10초마다 실행)
*/
@Scheduled(fixedRate = 10000)
public void evictExpiredTools() {
long now = System.currentTimeMillis();
List<String> expiredKeys = toolCache.entrySet().stream()
.filter(entry -> (now - entry.getValue().getLastHeartbeat()) > DEFAULT_TTL_MILLIS)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
for (String key : expiredKeys) {
toolCache.remove(key);
log.info(" [InMemoryRegistry] TTL 초과로 툴 자동 삭제: {}", key);
}
}
}

View File

@@ -51,9 +51,15 @@ public class DynamicMcpServerManager {
getOrCreateServer("common");
getOrCreateServer("hr");
getOrCreateServer("payment");
// Tool category names are not Pod names. Keep existing external category
// endpoints available while the SMS/OTH Pods transition to SAL/CUS.
getOrCreateServer("sms");
getOrCreateServer("email");
getOrCreateServer("oth");
getOrCreateServer("sal");
getOrCreateServer("email");
getOrCreateServer("cus");
getOrCreateServer("pro");
getOrCreateServer("sys");
getOrCreateServer("sample");
getOrCreateServer("smp"); // smp 카테고리도 기본적으로 항상 열어두도록 추가
}

View File

@@ -10,10 +10,15 @@
mcp:
gateway:
fallback:
default-url: http://was-oth:8084
default-url: http://was-cus:8084
routes:
sms: http://was-sms:8082
hr: http://was-oth:8084
cus: http://was-cus:8084
sal: http://was-sal:8082
sms: http://was-sal:8082
oth: http://was-cus:8084
hr: http://was-cus:8084
pro: http://was-pro:8085
sys: http://was-sys:8086
# --- 신한라이프 EAI/MCI 연계 IP 정보 (개발 환경) ---
shinhan:

View File

@@ -37,10 +37,15 @@ mcp:
default-url: http://localhost:8084
routes:
cmm_memo_retriever: http://localhost:8082
cus: http://localhost:8084
sal: http://localhost:8082
sms: http://localhost:8082
oth: http://localhost:8084
email: http://localhost:8083
payment: http://localhost:8085
hr: http://localhost:8084
pro: http://localhost:8085
sys: http://localhost:8086
tool:
host: localhost

View File

@@ -27,10 +27,16 @@ server:
mcp:
gateway:
fallback:
default-url: http://was-oth:8084
default-url: http://was-cus:8084
routes:
sms: http://was-sms:8082
hr: http://was-oth:8084
cus: http://was-cus:8084
sal: http://was-sal:8082
# Existing SMS category callers continue to resolve to the SAL Pod.
sms: http://was-sal:8082
oth: http://was-cus:8084
hr: http://was-cus:8084
pro: http://was-pro:8085
sys: http://was-sys:8086
agent-claims-required: false
trusted-claims-required: false
write-approval-required: false

View File

@@ -981,9 +981,11 @@
<div class="col-md-6">
<label class="form-label">Target Module</label>
<select class="form-select" name="moduleName" id="targetModuleSelect" required>
<option value="dap-was-oth">dap-was-oth</option>
<option value="dap-was-cus">dap-was-cus</option>
<option value="dap-was-hr">dap-was-hr</option>
<option value="dap-was-sms">dap-was-sms</option>
<option value="dap-was-sal">dap-was-sal</option>
<option value="dap-was-pro">dap-was-pro</option>
<option value="dap-was-sys">dap-was-sys</option>
</select>
<div class="input-hint">Destination Pod directory.</div>
</div>
@@ -1478,7 +1480,7 @@
.then(modules => {
const select = document.getElementById('targetModuleSelect');
if (modules && modules.length > 0) {
select.innerHTML = modules.map(m => `<option value="${m}" ${m === 'dap-was-oth' ? 'selected' : ''}>${m}</option>`).join('');
select.innerHTML = modules.map(m => `<option value="${m}" ${m === 'dap-was-cus' ? 'selected' : ''}>${m}</option>`).join('');
}
})
.catch(err => console.error('Failed to load modules:', err));