diff --git a/core/src/main/java/com/sap/ai/sdk/core/DeploymentResolver.java b/core/src/main/java/com/sap/ai/sdk/core/DeploymentResolver.java index 78c233b61..41c1c9640 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/DeploymentResolver.java +++ b/core/src/main/java/com/sap/ai/sdk/core/DeploymentResolver.java @@ -2,6 +2,7 @@ import com.sap.ai.sdk.core.client.DeploymentApi; import com.sap.ai.sdk.core.model.AiDeployment; +import com.sap.ai.sdk.core.model.AiDeploymentStatus; import java.util.HashSet; import java.util.Map; import java.util.Objects; @@ -9,6 +10,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Predicate; +import java.util.stream.Collectors; import javax.annotation.Nonnull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -27,7 +29,7 @@ class DeploymentResolver { @Nonnull private final AiCoreService service; - /** Cache for deployment ids. The key is the model name and the value is the deployment id. */ + /** Cache for deployments. The key is the resource group and the value a set of deployments. */ @Nonnull private final Map> cache; DeploymentResolver(@Nonnull final AiCoreService service) { @@ -35,7 +37,7 @@ class DeploymentResolver { } /** - * Remove all entries from the cache then load all deployments into the cache. + * Remove cached deployments for the resource group and reload running deployments into the cache. * *

Call this whenever a deployment is deleted. * @@ -46,8 +48,16 @@ void reloadDeployments(@Nonnull final String resourceGroup) { try { val apiClient = new DeploymentApi(service); val deployments = new HashSet<>(apiClient.query(resourceGroup).getResources()); - log.info("Found {} deployments in resource group '{}'.", deployments.size(), resourceGroup); - cache.put(resourceGroup, deployments); + val runningDeployments = + deployments.stream() + .filter(deployment -> AiDeploymentStatus.RUNNING.equals(deployment.getStatus())) + .collect(Collectors.toSet()); + log.info( + "Found {} of {} deployments running in resource group '{}'.", + runningDeployments.size(), + deployments.size(), + resourceGroup); + cache.put(resourceGroup, runningDeployments); } catch (final RuntimeException e) { throw new DeploymentResolutionException( "Failed to load deployments for resource group " + resourceGroup, e); diff --git a/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java b/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java index 7def53292..56b7457c1 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java @@ -81,7 +81,8 @@ void testGetInferenceDestination() { assertThat(destination.getHeaders()).containsExactly(new Header("AI-Resource-Group", "foo")); // scenario-based destination - val d = "{\"count\":1,\"resources\":[{\"id\":\"0123456789abcdef\",\"scenarioId\":\"foobar\"}]}"; + val d = + "{\"count\":1,\"resources\":[{\"id\":\"0123456789abcdef\",\"scenarioId\":\"foobar\", \"status\":\"RUNNING\"}]}"; val server = new WireMockServer(wireMockConfig().dynamicPort()); server.start(); server.stubFor(get(urlEqualTo("/v2/lm/deployments")).willReturn(okJson(d))); diff --git a/core/src/test/java/com/sap/ai/sdk/core/DeploymentResolverTest.java b/core/src/test/java/com/sap/ai/sdk/core/DeploymentResolverTest.java index e765c6ca3..abc9596f4 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/DeploymentResolverTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/DeploymentResolverTest.java @@ -10,6 +10,7 @@ import static com.sap.ai.sdk.core.AiCoreService.DEFAULT_RESOURCE_GROUP; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.tuple; import com.sap.ai.sdk.core.client.WireMockTestServer; import com.sap.ai.sdk.core.model.AiDeployment; @@ -169,6 +170,24 @@ void testIsDeploymentOfModel() { assertThat(DeploymentResolver.isDeploymentOfModel(gpt4VersionLatest, deployment)).isTrue(); } + @Test + void testCacheContainsRunningDeployments() { + wireMockServer.stubFor( + get(anyUrl()) + .willReturn( + aResponse() + .withBodyFile("hasStoppedDeployment.json") + .withHeader("content-type", "application/json"))); + + final var deployment = + resolver.getDeploymentIdByScenario(DEFAULT_RESOURCE_GROUP, "orchestration"); + + assertThat(deployment).isEqualTo("d4b1396b84c1944d"); + assertThat(cache.get(DEFAULT_RESOURCE_GROUP)) + .extracting(AiDeployment::getId, AiDeployment::getStatus) + .containsExactly(tuple("d4b1396b84c1944d", AiDeploymentStatus.RUNNING)); + } + private record TestModel(String name, String version) implements AiModel {} private static void stubResponse(String resourceGroup, String fileName) { diff --git a/core/src/test/resources/__files/hasStoppedDeployment.json b/core/src/test/resources/__files/hasStoppedDeployment.json new file mode 100644 index 000000000..1f66b47f4 --- /dev/null +++ b/core/src/test/resources/__files/hasStoppedDeployment.json @@ -0,0 +1,54 @@ +{ + "count": 8, + "resources": [ + { + "id": "d2a491b5010620b0", + "createdAt": "2026-06-08T14:56:25Z", + "modifiedAt": "2026-06-08T15:10:18Z", + "status": "STOPPED", + "details": { + "scaling": { + "backendDetails": {}, + "backend_details": {} + }, + "resources": { + "backendDetails": {}, + "backend_details": {} + } + }, + "scenarioId": "orchestration", + "configurationId": "0f0fb4c1-cf2c-441f-98d1-1f983d1b1756", + "targetStatus": "STOPPED", + "submissionTime": "2026-06-08T15:03:35Z", + "startTime": "2026-06-08T15:05:27Z", + "completionTime": "2026-06-08T15:27:23Z", + "configurationName": "orchestration-config", + "deploymentUrl": "https://api.ai.intprod-eu12.eu-central-1.aws.ml.hana.ondemand.com/v2/inference/deployments/d2a491b5010620b0" + }, + { + "id": "d4b1396b84c1944d", + "createdAt": "2026-04-16T10:03:39Z", + "modifiedAt": "2026-04-16T10:03:39Z", + "status": "RUNNING", + "details": { + "scaling": { + "backendDetails": {}, + "backend_details": {} + }, + "resources": { + "backendDetails": {}, + "backend_details": {} + } + }, + "scenarioId": "orchestration", + "configurationId": "e88f7dba-fd9c-4068-ab18-3306f2aa46bd", + "latestRunningConfigurationId": "e88f7dba-fd9c-4068-ab18-3306f2aa46bd", + "lastOperation": "CREATE", + "targetStatus": "RUNNING", + "submissionTime": "2026-04-17T08:32:01Z", + "startTime": "2026-04-17T08:32:59Z", + "configurationName": "orchestration-config", + "deploymentUrl": "https://api.ai.intprod-eu12.eu-central-1.aws.ml.hana.ondemand.com/v2/inference/deployments/d4b1396b84c1944d" + } + ] +} \ No newline at end of file