diff options
Diffstat (limited to 'configserver')
5 files changed, 382 insertions, 171 deletions
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java index 02ca4ce14c4..80194337daa 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java @@ -111,6 +111,8 @@ import java.util.stream.Collectors; import static com.yahoo.config.model.api.container.ContainerServiceType.CONTAINER; import static com.yahoo.config.model.api.container.ContainerServiceType.LOGSERVER_CONTAINER; +import static com.yahoo.vespa.config.server.application.ConfigConvergenceChecker.ServiceResponse; +import static com.yahoo.vespa.config.server.application.ConfigConvergenceChecker.ServiceListResponse; import static com.yahoo.vespa.config.server.filedistribution.FileDistributionUtil.fileReferenceExistsOnDisk; import static com.yahoo.vespa.config.server.filedistribution.FileDistributionUtil.getFileReferencesOnDisk; import static com.yahoo.vespa.config.server.tenant.TenantRepository.HOSTED_VESPA_TENANT; @@ -737,16 +739,22 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye // ---------------- Convergence ---------------------------------------------------------------- - public HttpResponse checkServiceForConfigConvergence(ApplicationId applicationId, String hostAndPort, URI uri, - Duration timeout, Optional<Version> vespaVersion) { - return convergeChecker.getServiceConfigGenerationResponse(getApplication(applicationId, vespaVersion), hostAndPort, uri, timeout); + public ServiceResponse checkServiceForConfigConvergence(ApplicationId applicationId, + String hostAndPort, + Duration timeout, + Optional<Version> vespaVersion) { + return convergeChecker.getServiceConfigGeneration(getApplication(applicationId, vespaVersion), hostAndPort, timeout); } - public HttpResponse servicesToCheckForConfigConvergence(ApplicationId applicationId, URI uri, - Duration timeoutPerService, Optional<Version> vespaVersion) { - return convergeChecker.getServiceConfigGenerationsResponse(getApplication(applicationId, vespaVersion), uri, timeoutPerService); + public ServiceListResponse servicesToCheckForConfigConvergence(ApplicationId applicationId, + URI uri, + Duration timeoutPerService, + Optional<Version> vespaVersion) { + return convergeChecker.getServiceConfigGenerations(getApplication(applicationId, vespaVersion), uri, timeoutPerService); } + public ConfigConvergenceChecker configConvergenceChecker() { return convergeChecker; } + // ---------------- Logs ---------------------------------------------------------------- public HttpResponse getLogs(ApplicationId applicationId, Optional<String> hostname, String apiParams) { diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/application/ConfigConvergenceChecker.java b/configserver/src/main/java/com/yahoo/vespa/config/server/application/ConfigConvergenceChecker.java index 24744a1b3b2..ad14cf4aab6 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/application/ConfigConvergenceChecker.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/application/ConfigConvergenceChecker.java @@ -10,8 +10,6 @@ import com.yahoo.concurrent.DaemonThreadFactory; import com.yahoo.config.model.api.HostInfo; import com.yahoo.config.model.api.PortInfo; import com.yahoo.config.model.api.ServiceInfo; -import com.yahoo.slime.Cursor; -import com.yahoo.vespa.config.server.http.JSONResponse; import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder; @@ -92,27 +90,26 @@ public class ConfigConvergenceChecker extends AbstractComponent { } /** Check all services in given application. Returns the minimum current generation of all services */ - public JSONResponse getServiceConfigGenerationsResponse(Application application, URI requestUrl, Duration timeoutPerService) { + public ServiceListResponse getServiceConfigGenerations(Application application, URI uri, Duration timeoutPerService) { Map<ServiceInfo, Long> currentGenerations = getServiceConfigGenerations(application, timeoutPerService); long currentGeneration = currentGenerations.values().stream().mapToLong(Long::longValue).min().orElse(-1); - return new ServiceListResponse(200, currentGenerations, requestUrl, application.getApplicationGeneration(), - currentGeneration); + return new ServiceListResponse(currentGenerations, uri, application.getApplicationGeneration(), currentGeneration); } /** Check service identified by host and port in given application */ - public JSONResponse getServiceConfigGenerationResponse(Application application, String hostAndPortToCheck, URI requestUrl, Duration timeout) { + public ServiceResponse getServiceConfigGeneration(Application application, String hostAndPortToCheck, Duration timeout) { Long wantedGeneration = application.getApplicationGeneration(); try (CloseableHttpAsyncClient client = createHttpClient()) { client.start(); if ( ! hostInApplication(application, hostAndPortToCheck)) - return ServiceResponse.createHostNotFoundInAppResponse(requestUrl, hostAndPortToCheck, wantedGeneration); + return new ServiceResponse(ServiceResponse.Status.hostNotFound, wantedGeneration); long currentGeneration = getServiceGeneration(client, URI.create("http://" + hostAndPortToCheck), timeout).get(); boolean converged = currentGeneration >= wantedGeneration; - return ServiceResponse.createOkResponse(requestUrl, hostAndPortToCheck, wantedGeneration, currentGeneration, converged); + return new ServiceResponse(ServiceResponse.Status.ok, wantedGeneration, currentGeneration, converged); } catch (InterruptedException | ExecutionException | CancellationException e) { // e.g. if we cannot connect to the service to find generation - return ServiceResponse.createNotFoundResponse(requestUrl, hostAndPortToCheck, wantedGeneration, e.getMessage()); + return new ServiceResponse(ServiceResponse.Status.notFound, wantedGeneration, e.getMessage()); } catch (Exception e) { - return ServiceResponse.createErrorResponse(requestUrl, hostAndPortToCheck, wantedGeneration, e.getMessage()); + return new ServiceResponse(ServiceResponse.Status.error, wantedGeneration, e.getMessage()); } } @@ -192,7 +189,7 @@ public class ConfigConvergenceChecker extends AbstractComponent { return false; } - private static Optional<Integer> getStatePort(ServiceInfo service) { + public static Optional<Integer> getStatePort(ServiceInfo service) { return service.getPorts().stream() .filter(port -> port.getTags().contains("state")) .map(PortInfo::getPort) @@ -249,63 +246,70 @@ public class ConfigConvergenceChecker extends AbstractComponent { .build(); } - private static class ServiceListResponse extends JSONResponse { - - // Pre-condition: servicesToCheck has a state port - private ServiceListResponse(int status, Map<ServiceInfo, Long> servicesToCheck, URI uri, long wantedGeneration, - long currentGeneration) { - super(status); - Cursor serviceArray = object.setArray("services"); - servicesToCheck.forEach((service, generation) -> { - Cursor serviceObject = serviceArray.addObject(); - String hostName = service.getHostName(); - int statePort = getStatePort(service).get(); - serviceObject.setString("host", hostName); - serviceObject.setLong("port", statePort); - serviceObject.setString("type", service.getServiceType()); - serviceObject.setString("url", uri.toString() + "/" + hostName + ":" + statePort); - serviceObject.setLong("currentGeneration", generation); - }); - object.setString("url", uri.toString()); - object.setLong("currentGeneration", currentGeneration); - object.setLong("wantedGeneration", wantedGeneration); - object.setBool("converged", currentGeneration >= wantedGeneration); + public static class ServiceResponse { + + public enum Status { ok, notFound, hostNotFound, error } + + public final Status status; + public final Long wantedGeneration; + public final Long currentGeneration; + public final boolean converged; + public final Optional<String> errorMessage; + + public ServiceResponse(Status status, Long wantedGeneration) { + this(status, wantedGeneration, 0L); } - } - private static class ServiceResponse extends JSONResponse { + public ServiceResponse(Status status, Long wantedGeneration, Long currentGeneration) { + this(status, wantedGeneration, currentGeneration, false); + } - private ServiceResponse(int status, URI uri, String hostname, Long wantedGeneration) { - super(status); - object.setString("url", uri.toString()); - object.setString("host", hostname); - object.setLong("wantedGeneration", wantedGeneration); + public ServiceResponse(Status status, Long wantedGeneration, Long currentGeneration, boolean converged) { + this(status, wantedGeneration, currentGeneration, converged, Optional.empty()); } - static ServiceResponse createOkResponse(URI uri, String hostname, Long wantedGeneration, Long currentGeneration, boolean converged) { - ServiceResponse serviceResponse = new ServiceResponse(200, uri, hostname, wantedGeneration); - serviceResponse.object.setBool("converged", converged); - serviceResponse.object.setLong("currentGeneration", currentGeneration); - return serviceResponse; + public ServiceResponse(Status status, Long wantedGeneration, String errorMessage) { + this(status, wantedGeneration, 0L, false, Optional.ofNullable(errorMessage)); } - static ServiceResponse createHostNotFoundInAppResponse(URI uri, String hostname, Long wantedGeneration) { - ServiceResponse serviceResponse = new ServiceResponse(410, uri, hostname, wantedGeneration); - serviceResponse.object.setString("problem", "Host:port (service) no longer part of application, refetch list of services."); - return serviceResponse; + private ServiceResponse(Status status, Long wantedGeneration, Long currentGeneration, boolean converged, Optional<String> errorMessage) { + this.status = status; + this.wantedGeneration = wantedGeneration; + this.currentGeneration = currentGeneration; + this.converged = converged; + this.errorMessage = errorMessage; } - static ServiceResponse createErrorResponse(URI uri, String hostname, Long wantedGeneration, String error) { - ServiceResponse serviceResponse = new ServiceResponse(500, uri, hostname, wantedGeneration); - serviceResponse.object.setString("error", error); - return serviceResponse; + } + + public static class ServiceListResponse { + + public final List<Service> services = new ArrayList<>(); + public final URI uri; + public final long wantedGeneration; + public final long currentGeneration; + + public ServiceListResponse(Map<ServiceInfo, Long> services, URI uri, long wantedGeneration, long currentGeneration) { + services.forEach((key, value) -> this.services.add(new Service(key, value))); + this.uri = uri; + this.wantedGeneration = wantedGeneration; + this.currentGeneration = currentGeneration; } - static ServiceResponse createNotFoundResponse(URI uri, String hostname, Long wantedGeneration, String error) { - ServiceResponse serviceResponse = new ServiceResponse(404, uri, hostname, wantedGeneration); - serviceResponse.object.setString("error", error); - return serviceResponse; + public List<Service> services() { return services; } + + public static class Service { + + public final ServiceInfo serviceInfo; + public final Long currentGeneration; + + public Service(ServiceInfo serviceInfo, Long currentGeneration) { + this.serviceInfo = serviceInfo; + this.currentGeneration = currentGeneration; + } + } + } } diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandler.java index 4dda141491c..0131517818d 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandler.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandler.java @@ -5,6 +5,7 @@ import com.google.inject.Inject; import com.yahoo.component.Version; import com.yahoo.config.application.api.ApplicationFile; import com.yahoo.config.model.api.Model; +import com.yahoo.config.model.api.ServiceInfo; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.HostFilter; import com.yahoo.config.provision.InstanceName; @@ -16,13 +17,15 @@ import com.yahoo.jdisc.Response; import com.yahoo.restapi.ErrorResponse; import com.yahoo.restapi.MessageResponse; import com.yahoo.restapi.Path; +import com.yahoo.slime.Cursor; import com.yahoo.slime.SlimeUtils; import com.yahoo.text.StringUtilities; import com.yahoo.vespa.config.server.ApplicationRepository; -import com.yahoo.vespa.config.server.application.ApplicationReindexing; +import com.yahoo.vespa.config.server.application.ConfigConvergenceChecker; import com.yahoo.vespa.config.server.http.ContentHandler; import com.yahoo.vespa.config.server.http.ContentRequest; import com.yahoo.vespa.config.server.http.HttpHandler; +import com.yahoo.vespa.config.server.http.JSONResponse; import com.yahoo.vespa.config.server.http.NotFoundException; import com.yahoo.vespa.config.server.http.v2.request.ApplicationContentRequest; import com.yahoo.vespa.config.server.http.v2.response.ApplicationSuspendedResponse; @@ -33,6 +36,7 @@ import com.yahoo.vespa.config.server.http.v2.response.ReindexingResponse; import com.yahoo.vespa.config.server.tenant.Tenant; import java.io.IOException; +import java.net.URI; import java.time.Duration; import java.time.Instant; import java.util.Map; @@ -43,8 +47,11 @@ import java.util.StringJoiner; import java.util.TreeMap; import java.util.TreeSet; +import static com.yahoo.vespa.config.server.application.ConfigConvergenceChecker.ServiceListResponse; +import static com.yahoo.vespa.config.server.application.ConfigConvergenceChecker.ServiceResponse; import static com.yahoo.yolean.Exceptions.uncheck; + /** * Operations on applications (delete, wait for config convergence, restart, application content etc.) * @@ -108,13 +115,21 @@ public class ApplicationHandler extends HttpHandler { } private HttpResponse listServiceConverge(ApplicationId applicationId, HttpRequest request) { - return applicationRepository.servicesToCheckForConfigConvergence(applicationId, request.getUri(), - getTimeoutFromRequest(request), getVespaVersionFromRequest(request)); + ServiceListResponse response = + applicationRepository.servicesToCheckForConfigConvergence(applicationId, + request.getUri(), + getTimeoutFromRequest(request), + getVespaVersionFromRequest(request)); + return new HttpServiceListResponse(response); } private HttpResponse checkServiceConverge(ApplicationId applicationId, String hostAndPort, HttpRequest request) { - return applicationRepository.checkServiceForConfigConvergence(applicationId, hostAndPort, request.getUri(), - getTimeoutFromRequest(request), getVespaVersionFromRequest(request)); + ServiceResponse response = + applicationRepository.checkServiceForConfigConvergence(applicationId, + hostAndPort, + getTimeoutFromRequest(request), + getVespaVersionFromRequest(request)); + return HttpServiceResponse.createResponse(response, hostAndPort, request.getUri()); } private HttpResponse serviceStatusPage(ApplicationId applicationId, String service, String hostname, String pathSuffix) { @@ -301,4 +316,79 @@ public class ApplicationHandler extends HttpHandler { .map(Version::fromString); } + static class HttpServiceResponse extends JSONResponse { + + public static HttpServiceResponse createResponse(ConfigConvergenceChecker.ServiceResponse serviceResponse, String hostAndPort, URI uri) { + switch (serviceResponse.status) { + case ok: + return createOkResponse(uri, hostAndPort, serviceResponse.wantedGeneration, serviceResponse.currentGeneration, serviceResponse.converged); + case hostNotFound: + return createHostNotFoundInAppResponse(uri, hostAndPort, serviceResponse.wantedGeneration); + case notFound: + return createNotFoundResponse(uri, hostAndPort, serviceResponse.wantedGeneration, serviceResponse.errorMessage.orElse("")); + case error: + return createErrorResponse(uri, hostAndPort, serviceResponse.wantedGeneration, serviceResponse.errorMessage.orElse("")); + default: + throw new IllegalArgumentException("Unknown status " + serviceResponse.status); + } + } + + private HttpServiceResponse(int status, URI uri, String hostname, Long wantedGeneration) { + super(status); + object.setString("url", uri.toString()); + object.setString("host", hostname); + object.setLong("wantedGeneration", wantedGeneration); + } + + private static HttpServiceResponse createOkResponse(URI uri, String hostname, Long wantedGeneration, Long currentGeneration, boolean converged) { + HttpServiceResponse serviceResponse = new HttpServiceResponse(200, uri, hostname, wantedGeneration); + serviceResponse.object.setBool("converged", converged); + serviceResponse.object.setLong("currentGeneration", currentGeneration); + return serviceResponse; + } + + private static HttpServiceResponse createHostNotFoundInAppResponse(URI uri, String hostname, Long wantedGeneration) { + HttpServiceResponse serviceResponse = new HttpServiceResponse(410, uri, hostname, wantedGeneration); + serviceResponse.object.setString("problem", "Host:port (service) no longer part of application, refetch list of services."); + return serviceResponse; + } + + private static HttpServiceResponse createErrorResponse(URI uri, String hostname, Long wantedGeneration, String error) { + HttpServiceResponse serviceResponse = new HttpServiceResponse(500, uri, hostname, wantedGeneration); + serviceResponse.object.setString("error", error); + return serviceResponse; + } + + private static HttpServiceResponse createNotFoundResponse(URI uri, String hostname, Long wantedGeneration, String error) { + HttpServiceResponse serviceResponse = new HttpServiceResponse(404, uri, hostname, wantedGeneration); + serviceResponse.object.setString("error", error); + return serviceResponse; + } + + } + + static class HttpServiceListResponse extends JSONResponse { + + // Pre-condition: servicesToCheck has a state port + public HttpServiceListResponse(ConfigConvergenceChecker.ServiceListResponse response) { + super(200); + Cursor serviceArray = object.setArray("services"); + response.services().forEach((service) -> { + ServiceInfo serviceInfo = service.serviceInfo; + Cursor serviceObject = serviceArray.addObject(); + String hostName = serviceInfo.getHostName(); + int statePort = ConfigConvergenceChecker.getStatePort(serviceInfo).get(); + serviceObject.setString("host", hostName); + serviceObject.setLong("port", statePort); + serviceObject.setString("type", serviceInfo.getServiceType()); + serviceObject.setString("url", response.uri.toString() + "/" + hostName + ":" + statePort); + serviceObject.setLong("currentGeneration", service.currentGeneration); + }); + object.setString("url", response.uri.toString()); + object.setLong("currentGeneration", response.currentGeneration); + object.setLong("wantedGeneration", response.wantedGeneration); + object.setBool("converged", response.currentGeneration >= response.wantedGeneration); + } + } + } diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/application/ConfigConvergenceCheckerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/application/ConfigConvergenceCheckerTest.java index 8b21e9c3916..6afb9ef086d 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/application/ConfigConvergenceCheckerTest.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/application/ConfigConvergenceCheckerTest.java @@ -8,7 +8,6 @@ import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ApplicationName; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.TenantName; -import com.yahoo.container.jdisc.HttpResponse; import com.yahoo.vespa.config.server.ServerCache; import com.yahoo.vespa.config.server.monitoring.MetricUpdater; import org.junit.Before; @@ -16,22 +15,20 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.UncheckedIOException; import java.net.URI; import java.time.Duration; import java.util.Arrays; -import java.util.function.Consumer; +import java.util.List; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.okJson; import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; -import static com.yahoo.test.json.JsonTestHelper.assertJsonEquals; -import static org.assertj.core.api.Assertions.assertThat; +import static com.yahoo.vespa.config.server.application.ConfigConvergenceChecker.ServiceListResponse; +import static com.yahoo.vespa.config.server.application.ConfigConvergenceChecker.ServiceResponse; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; /** * @author Ulf Lilleengen @@ -72,63 +69,35 @@ public class ConfigConvergenceCheckerTest { @Test public void service_convergence() { { // Known service - String serviceName = hostAndPort(this.service); - URI requestUrl = testServer().resolve("/serviceconverge/" + serviceName); wireMock.stubFor(get(urlEqualTo("/state/v1/config")).willReturn(okJson("{\"config\":{\"generation\":3}}"))); - HttpResponse serviceResponse = checker.getServiceConfigGenerationResponse(application, hostAndPort(this.service), requestUrl, clientTimeout); - assertResponse("{\n" + - " \"url\": \"" + requestUrl.toString() + "\",\n" + - " \"host\": \"" + hostAndPort(this.service) + "\",\n" + - " \"wantedGeneration\": 3,\n" + - " \"converged\": true,\n" + - " \"currentGeneration\": 3\n" + - "}", - 200, - serviceResponse); + + ServiceResponse response = checker.getServiceConfigGeneration(application, hostAndPort(this.service), clientTimeout); + assertEquals(3, response.wantedGeneration.longValue()); + assertEquals(3, response.currentGeneration.longValue()); + assertTrue(response.converged); + assertEquals(ServiceResponse.Status.ok, response.status); } { // Missing service - String serviceName = "notPresent:1337"; - URI requestUrl = testServer().resolve("/serviceconverge/" + serviceName); - HttpResponse response = checker.getServiceConfigGenerationResponse(application, "notPresent:1337", requestUrl, clientTimeout); - assertResponse("{\n" + - " \"url\": \"" + requestUrl.toString() + "\",\n" + - " \"host\": \"" + serviceName + "\",\n" + - " \"wantedGeneration\": 3,\n" + - " \"problem\": \"Host:port (service) no longer part of application, refetch list of services.\"\n" + - "}", - 410, - response); + ServiceResponse response = checker.getServiceConfigGeneration(application, "notPresent:1337", clientTimeout); + assertEquals(3, response.wantedGeneration.longValue()); + assertEquals(ServiceResponse.Status.hostNotFound, response.status); } } @Test public void service_list_convergence() { { - String serviceName = hostAndPort(this.service); URI requestUrl = testServer().resolve("/serviceconverge"); - URI serviceUrl = testServer().resolve("/serviceconverge/" + serviceName); wireMock.stubFor(get(urlEqualTo("/state/v1/config")).willReturn(okJson("{\"config\":{\"generation\":3}}"))); - HttpResponse response = checker.getServiceConfigGenerationsResponse(application, requestUrl, clientTimeout); - assertResponse("{\n" + - " \"services\": [\n" + - " {\n" + - " \"host\": \"" + serviceUrl.getHost() + "\",\n" + - " \"port\": " + serviceUrl.getPort() + ",\n" + - " \"type\": \"container\",\n" + - " \"url\": \"" + serviceUrl.toString() + "\",\n" + - " \"currentGeneration\":" + 3 + "\n" + - " }\n" + - " ],\n" + - " \"url\": \"" + requestUrl.toString() + "\",\n" + - " \"currentGeneration\": 3,\n" + - " \"wantedGeneration\": 3,\n" + - " \"converged\": true\n" + - "}", - 200, - response); - } + ServiceListResponse response = checker.getServiceConfigGenerations(application, requestUrl, clientTimeout); + assertEquals(3, response.wantedGeneration); + assertEquals(3, response.currentGeneration); + List<ServiceListResponse.Service> services = response.services; + assertEquals(1, services.size()); + assertService(this.service, services.get(0), 3); + } { // Model with two hosts on different generations MockModel model = new MockModel(Arrays.asList( @@ -143,53 +112,29 @@ public class ConfigConvergenceCheckerTest { wireMock2.stubFor(get(urlEqualTo("/state/v1/config")).willReturn(okJson("{\"config\":{\"generation\":3}}"))); URI requestUrl = testServer().resolve("/serviceconverge"); - URI serviceUrl = testServer().resolve("/serviceconverge/" + hostAndPort(service)); - URI serviceUrl2 = testServer().resolve("/serviceconverge/" + hostAndPort(service2)); - HttpResponse response = checker.getServiceConfigGenerationsResponse(application, requestUrl, clientTimeout); - assertResponse("{\n" + - " \"services\": [\n" + - " {\n" + - " \"host\": \"" + service.getHost() + "\",\n" + - " \"port\": " + service.getPort() + ",\n" + - " \"type\": \"container\",\n" + - " \"url\": \"" + serviceUrl.toString() + "\",\n" + - " \"currentGeneration\":" + 4 + "\n" + - " },\n" + - " {\n" + - " \"host\": \"" + service2.getHost() + "\",\n" + - " \"port\": " + service2.getPort() + ",\n" + - " \"type\": \"container\",\n" + - " \"url\": \"" + serviceUrl2.toString() + "\",\n" + - " \"currentGeneration\":" + 3 + "\n" + - " }\n" + - " ],\n" + - " \"url\": \"" + requestUrl.toString() + "\",\n" + - " \"currentGeneration\": 3,\n" + - " \"wantedGeneration\": 4,\n" + - " \"converged\": false\n" + - "}", - 200, - response); + + ServiceListResponse response = checker.getServiceConfigGenerations(application, requestUrl, clientTimeout); + assertEquals(4, response.wantedGeneration); + assertEquals(3, response.currentGeneration); + + List<ServiceListResponse.Service> services = response.services; + assertEquals(2, services.size()); + assertService(this.service, services.get(0), 4); + assertService(this.service2, services.get(1), 3); } } + @Test public void service_convergence_timeout() { - URI requestUrl = testServer().resolve("/serviceconverge"); wireMock.stubFor(get(urlEqualTo("/state/v1/config")).willReturn(aResponse() .withFixedDelay((int) clientTimeout.plus(Duration.ofSeconds(1)).toMillis()) .withBody("response too slow"))); - HttpResponse response = checker.getServiceConfigGenerationResponse(application, hostAndPort(service), requestUrl, Duration.ofMillis(1)); - // Message contained in a SocketTimeoutException may differ across platforms, so we do a partial match of the response here - assertResponse( - responseBody -> - assertThat(responseBody) - .startsWith("{\"url\":\"" + requestUrl.toString() + "\",\"host\":\"" + hostAndPort(requestUrl) + - "\",\"wantedGeneration\":3,\"error\":\"") - .contains("java.net.SocketTimeoutException: 1 MILLISECONDS") - .endsWith("\"}"), - 404, - response); + ServiceResponse response = checker.getServiceConfigGeneration(application, hostAndPort(service), Duration.ofMillis(1)); + + assertEquals(3, response.wantedGeneration.longValue()); + assertEquals(ServiceResponse.Status.notFound, response.status); + assertTrue(response.errorMessage.get().contains("java.net.SocketTimeoutException: 1 MILLISECONDS")); } private URI testServer() { @@ -204,19 +149,11 @@ public class ConfigConvergenceCheckerTest { return uri.getHost() + ":" + uri.getPort(); } - private static void assertResponse(String expectedJson, int status, HttpResponse response) { - assertResponse((responseBody) -> assertJsonEquals(new String(responseBody.getBytes()), expectedJson), status, response); - } - - private static void assertResponse(Consumer<String> assertFunc, int status, HttpResponse response) { - ByteArrayOutputStream responseBody = new ByteArrayOutputStream(); - try { - response.render(responseBody); - assertFunc.accept(responseBody.toString()); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - assertEquals(status, response.getStatus()); + private void assertService(URI uri, ServiceListResponse.Service service1, long expectedGeneration) { + assertEquals(expectedGeneration, service1.currentGeneration.longValue()); + assertEquals(uri.getHost(), service1.serviceInfo.getHostName()); + assertEquals(uri.getPort(), ConfigConvergenceChecker.getStatePort(service1.serviceInfo).get().intValue()); + assertEquals("container", service1.serviceInfo.getServiceType()); } } diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandlerTest.java index 0b8bf8e84bd..04483e0191d 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandlerTest.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandlerTest.java @@ -4,6 +4,8 @@ package com.yahoo.vespa.config.server.http.v2; import com.yahoo.cloud.config.ConfigserverConfig; import com.yahoo.component.Version; import com.yahoo.config.model.api.ModelFactory; +import com.yahoo.config.model.api.PortInfo; +import com.yahoo.config.model.api.ServiceInfo; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ApplicationName; import com.yahoo.config.provision.InstanceName; @@ -50,14 +52,18 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.io.UncheckedIOException; +import java.net.URI; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.Instant; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; +import java.util.function.Consumer; import java.util.stream.Stream; import static com.yahoo.container.jdisc.HttpRequest.createTestRequest; @@ -65,8 +71,12 @@ import static com.yahoo.jdisc.http.HttpRequest.Method.DELETE; import static com.yahoo.jdisc.http.HttpRequest.Method.GET; import static com.yahoo.jdisc.http.HttpRequest.Method.POST; import static com.yahoo.test.json.JsonTestHelper.assertJsonEquals; +import static com.yahoo.vespa.config.server.application.ConfigConvergenceChecker.ServiceListResponse; +import static com.yahoo.vespa.config.server.application.ConfigConvergenceChecker.ServiceResponse; import static com.yahoo.vespa.config.server.http.HandlerTest.assertHttpStatusCodeAndMessage; import static com.yahoo.vespa.config.server.http.SessionHandlerTest.getRenderedString; +import static com.yahoo.vespa.config.server.http.v2.ApplicationHandler.HttpServiceListResponse; +import static com.yahoo.vespa.config.server.http.v2.ApplicationHandler.HttpServiceResponse.createResponse; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -146,7 +156,7 @@ public class ApplicationHandlerTest { Tenant mytenant = applicationRepository.getTenant(applicationId); deleteAndAssertOKResponse(mytenant, applicationId); } - + { applicationRepository.deploy(testApp, prepareParams(applicationId)); deleteAndAssertOKResponseMocked(applicationId, true); @@ -539,6 +549,143 @@ public class ApplicationHandlerTest { "}\n"); } + @Test + public void service_convergence() { + String hostAndPort = "localhost:1234"; + URI uri = URI.create("https://" + hostAndPort + "/serviceconvergence/container"); + + { // Known service + HttpResponse response = createResponse(new ServiceResponse(ServiceResponse.Status.ok, + 3L, + 3L, + true), + hostAndPort, + uri); + assertResponse("{\n" + + " \"url\": \"" + uri.toString() + "\",\n" + + " \"host\": \"" + hostAndPort + "\",\n" + + " \"wantedGeneration\": 3,\n" + + " \"converged\": true,\n" + + " \"currentGeneration\": 3\n" + + "}", + 200, + response); + } + + { // Missing service + HttpResponse response = createResponse(new ServiceResponse(ServiceResponse.Status.hostNotFound, + 3L), + hostAndPort, + uri); + + assertResponse("{\n" + + " \"url\": \"" + uri.toString() + "\",\n" + + " \"host\": \"" + hostAndPort + "\",\n" + + " \"wantedGeneration\": 3,\n" + + " \"problem\": \"Host:port (service) no longer part of application, refetch list of services.\"\n" + + "}", + 410, + response); + } + } + + @Test + public void service_list_convergence() { + URI requestUrl = URI.create("https://configserver/serviceconvergence"); + + String hostname = "localhost"; + int port = 1234; + String hostAndPort = hostname + ":" + port; + URI serviceUrl = URI.create("https://configserver/serviceconvergence/" + hostAndPort); + + { + HttpServiceListResponse response = + new HttpServiceListResponse(new ServiceListResponse(Map.of(createServiceInfo(hostname, port), 3L), + requestUrl, + 3L, + 3L)); + assertResponse("{\n" + + " \"services\": [\n" + + " {\n" + + " \"host\": \"" + hostname + "\",\n" + + " \"port\": " + port + ",\n" + + " \"type\": \"container\",\n" + + " \"url\": \"" + serviceUrl.toString() + "\",\n" + + " \"currentGeneration\":" + 3 + "\n" + + " }\n" + + " ],\n" + + " \"url\": \"" + requestUrl.toString() + "\",\n" + + " \"currentGeneration\": 3,\n" + + " \"wantedGeneration\": 3,\n" + + " \"converged\": true\n" + + "}", + 200, + response); + } + + { // Two hosts on different generations + String hostname2 = "localhost2"; + int port2 = 5678; + String hostAndPort2 = hostname2 + ":" + port2; + URI serviceUrl2 = URI.create("https://configserver/serviceconvergence/" + hostAndPort2); + + Map<ServiceInfo, Long> serviceInfos = new HashMap<>(); + serviceInfos.put(createServiceInfo(hostname, port), 4L); + serviceInfos.put(createServiceInfo(hostname2, port2), 3L); + + HttpServiceListResponse response = + new HttpServiceListResponse(new ServiceListResponse(serviceInfos, + requestUrl, + 4L, + 3L)); + assertResponse("{\n" + + " \"services\": [\n" + + " {\n" + + " \"host\": \"" + hostname + "\",\n" + + " \"port\": " + port + ",\n" + + " \"type\": \"container\",\n" + + " \"url\": \"" + serviceUrl.toString() + "\",\n" + + " \"currentGeneration\":" + 4 + "\n" + + " },\n" + + " {\n" + + " \"host\": \"" + hostname2 + "\",\n" + + " \"port\": " + port2 + ",\n" + + " \"type\": \"container\",\n" + + " \"url\": \"" + serviceUrl2.toString() + "\",\n" + + " \"currentGeneration\":" + 3 + "\n" + + " }\n" + + " ],\n" + + " \"url\": \"" + requestUrl.toString() + "\",\n" + + " \"currentGeneration\": 3,\n" + + " \"wantedGeneration\": 4,\n" + + " \"converged\": false\n" + + "}", + 200, + response); + } + } + + @Test + public void service_convergence_timeout() { + String hostAndPort = "localhost:1234"; + URI uri = URI.create("https://" + hostAndPort + "/serviceconvergence/container"); + + HttpResponse response = createResponse(new ServiceResponse(ServiceResponse.Status.notFound, + 3L, + "some error message"), + hostAndPort, + uri); + + assertResponse("{\n" + + " \"url\": \"" + uri.toString() + "\",\n" + + " \"host\": \"" + hostAndPort + "\",\n" + + " \"wantedGeneration\": 3,\n" + + " \"error\": \"some error message\"" + + "}", + 404, + response); + } + private void assertNotAllowed(Method method) throws IOException { String url = "http://myhost:14000/application/v2/tenant/" + mytenantName + "/application/default"; deleteAndAssertResponse(url, Response.Status.METHOD_NOT_ALLOWED, HttpErrorResponse.ErrorCode.METHOD_NOT_ALLOWED, "{\"error-code\":\"METHOD_NOT_ALLOWED\",\"message\":\"Method '" + method + "' is not supported\"}", @@ -668,4 +815,29 @@ public class ApplicationHandlerTest { return new PrepareParams.Builder().applicationId(applicationId).build(); } + private static void assertResponse(String expectedJson, int status, HttpResponse response) { + assertResponse((responseBody) -> assertJsonEquals(new String(responseBody + .getBytes()), expectedJson), status, response); + } + + private static void assertResponse(Consumer<String> assertFunc, int status, HttpResponse response) { + ByteArrayOutputStream responseBody = new ByteArrayOutputStream(); + try { + response.render(responseBody); + assertFunc.accept(responseBody.toString()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + assertEquals(status, response.getStatus()); + } + + private ServiceInfo createServiceInfo(String hostname, int port) { + return new ServiceInfo("container", + "container", + List.of(new PortInfo(port, List.of("state"))), + Map.of(), + "configId", + hostname); + } + } |