Isolate route manifest failures during tool preload
All checks were successful
Deploy Gateway / deploy (push) Successful in 2m29s
All checks were successful
Deploy Gateway / deploy (push) Successful in 2m29s
This commit is contained in:
@@ -103,7 +103,7 @@ public class PortalToolRegistryClient implements ToolRegistryClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* 포털 전체 registry snapshot API를 한 번 호출해 route별 Tool catalog를 구성합니다. 응답의 {@code routes[]}에 있는 각 route마다 Tool Service manifest를 조회해 route별 in-memory snapshot 후보를 만듭니다.
|
||||
* 포털 전체 registry snapshot API를 한 번 호출해 route별 Tool catalog를 구성합니다. 응답의 {@code routes[]}에 있는 각 route마다 Tool Service manifest를 조회하되, 한 route의 실패가 다른 정상 route의 manifest 조회를 막지 않도록 route 단위로 실패를 격리합니다.
|
||||
*
|
||||
* @return 조회 또는 변환된 목록 정보를 반환합니다.
|
||||
*/
|
||||
@@ -111,7 +111,8 @@ public class PortalToolRegistryClient implements ToolRegistryClient {
|
||||
public Map<String, List<ToolMetadata>> fetchAllTools() {
|
||||
ensurePortalRegistryLoaded();
|
||||
Map<String, List<ToolMetadata>> snapshots = new LinkedHashMap<>();
|
||||
bundlesByRoute.forEach((routeKey, bundles) -> snapshots.put(routeKey, fetchRouteTools(routeKey, bundles)));
|
||||
bundlesByRoute.forEach((routeKey, bundles) ->
|
||||
fetchRouteToolsSafely(routeKey, bundles).ifPresent(tools -> snapshots.put(routeKey, tools)));
|
||||
return Map.copyOf(snapshots);
|
||||
}
|
||||
|
||||
@@ -313,6 +314,26 @@ public class PortalToolRegistryClient implements ToolRegistryClient {
|
||||
return merge(results);
|
||||
}
|
||||
|
||||
/**
|
||||
* 전체 route preload 중 한 route의 Tool Service manifest 조회 실패를 해당 route로만 제한합니다. 물리 MCP 한 대가 여러 논리 route를 담는 구성에서는 하나의 route 장애 때문에 다른 route의 정상 Tool 목록까지 누락되면 안 되므로, 실패 route는 로그만 남기고 성공 route snapshot 수집을 계속합니다.
|
||||
*
|
||||
* @param routeKey 처리 대상 route key입니다.
|
||||
* @param bundles 처리 대상 목록입니다.
|
||||
* @return 조회된 선택값을 반환합니다.
|
||||
*/
|
||||
private Optional<List<ToolMetadata>> fetchRouteToolsSafely(String routeKey, List<Bundle> bundles) {
|
||||
try {
|
||||
return Optional.of(fetchRouteTools(routeKey, bundles));
|
||||
} catch (RuntimeException exception) {
|
||||
log.warn(
|
||||
"Portal route manifest refresh ignored: routeKey={}, reason={}, message={}",
|
||||
routeKey,
|
||||
exception.getClass().getSimpleName(),
|
||||
exception.getMessage());
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool Server 한 대의 routing manifest API를 호출합니다. 응답 JSON shape는 Tool Server와 Agent Builder의 계약이므로 MCP가 필드명을 변환하지 않고 원문 JSON tree를 그대로 반환합니다.
|
||||
*
|
||||
|
||||
@@ -177,6 +177,31 @@ class PortalToolRegistryClientTest {
|
||||
assertThat(toolServer.getRequestCount()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsUsableRoutesWhenAnotherRouteManifestIsUnavailable() throws Exception {
|
||||
MockWebServer unreachableToolServer = new MockWebServer();
|
||||
unreachableToolServer.start();
|
||||
try {
|
||||
portal.enqueue(portalRegistryWithOneBrokenAndThreeUsableRoutes("portal-1", unreachableToolServer));
|
||||
unreachableToolServer.enqueue(new MockResponse().setResponseCode(503));
|
||||
toolServer.enqueue(manifestForBundle("was-cus", "cus.customer"));
|
||||
toolServer.enqueue(manifestForBundle("was-pro", "pro.product"));
|
||||
toolServer.enqueue(manifestForBundle("was-sys", "sys.health"));
|
||||
PortalToolRegistryClient client = client();
|
||||
|
||||
Map<String, List<ToolMetadata>> snapshots = client.fetchAllTools();
|
||||
|
||||
assertThat(snapshots).containsOnlyKeys("cus", "pro", "sys");
|
||||
assertThat(snapshots.get("cus")).extracting(ToolMetadata::name).containsExactly("cus.customer");
|
||||
assertThat(snapshots.get("pro")).extracting(ToolMetadata::name).containsExactly("pro.product");
|
||||
assertThat(snapshots.get("sys")).extracting(ToolMetadata::name).containsExactly("sys.health");
|
||||
assertThat(unreachableToolServer.getRequestCount()).isEqualTo(1);
|
||||
assertThat(toolServer.getRequestCount()).isEqualTo(3);
|
||||
} finally {
|
||||
unreachableToolServer.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void fetchesRoutingManifestForTheRequestedPortalRoute() throws Exception {
|
||||
portal.enqueue(portalRegistry("portal-1"));
|
||||
@@ -393,6 +418,62 @@ class PortalToolRegistryClientTest {
|
||||
"""
|
||||
.formatted(revision, toolServer.url("").toString().replaceAll("/+$", "")));
|
||||
}
|
||||
|
||||
private MockResponse portalRegistryWithOneBrokenAndThreeUsableRoutes(
|
||||
String revision, MockWebServer unreachableToolServer) {
|
||||
String usableDomain = toolServer.url("").toString().replaceAll("/+$", "");
|
||||
return jsonResponse(
|
||||
"""
|
||||
{
|
||||
"registryRevision": "%s",
|
||||
"routes": [
|
||||
{
|
||||
"routeKey": "sal",
|
||||
"toolServices": [ {
|
||||
"serviceKey": "was-sal",
|
||||
"serviceDomain": "%s",
|
||||
"manifestPath": "/tool-manifest",
|
||||
"status": "ACTIVE"
|
||||
} ]
|
||||
},
|
||||
{
|
||||
"routeKey": "cus",
|
||||
"toolServices": [ {
|
||||
"serviceKey": "was-cus",
|
||||
"serviceDomain": "%s",
|
||||
"manifestPath": "/tool-manifest",
|
||||
"status": "ACTIVE"
|
||||
} ]
|
||||
},
|
||||
{
|
||||
"routeKey": "pro",
|
||||
"toolServices": [ {
|
||||
"serviceKey": "was-pro",
|
||||
"serviceDomain": "%s",
|
||||
"manifestPath": "/tool-manifest",
|
||||
"status": "ACTIVE"
|
||||
} ]
|
||||
},
|
||||
{
|
||||
"routeKey": "sys",
|
||||
"toolServices": [ {
|
||||
"serviceKey": "was-sys",
|
||||
"serviceDomain": "%s",
|
||||
"manifestPath": "/tool-manifest",
|
||||
"status": "ACTIVE"
|
||||
} ]
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
.formatted(
|
||||
revision,
|
||||
unreachableToolServer.url("").toString().replaceAll("/+$", ""),
|
||||
usableDomain,
|
||||
usableDomain,
|
||||
usableDomain));
|
||||
}
|
||||
|
||||
private MockResponse manifest(String revision, String toolName) {
|
||||
return manifestForBundle("external-tool-server", revision, toolName);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user