ai.vespa.hosted.client.AbstractConfigServerClient Maven / Gradle / Ivy
// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package ai.vespa.hosted.client;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.Method;
import org.apache.hc.core5.http.ParseException;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.HttpEntities;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org.apache.hc.core5.net.URIBuilder;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.logging.Logger;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.FINE;
import static java.util.logging.Level.WARNING;
/**
* @author jonmv
*/
public abstract class AbstractConfigServerClient implements ConfigServerClient {
private static final Logger log = Logger.getLogger(AbstractConfigServerClient.class.getName());
/** Executes the request with the given context. The caller must close the response. */
abstract ClassicHttpResponse execute(ClassicHttpRequest request, HttpClientContext context) throws IOException;
/** Executes the given request with response/error handling and retries. */
private T execute(RequestBuilder builder,
BiFunction handler,
ExceptionHandler catcher) {
HttpClientContext context = HttpClientContext.create();
context.setRequestConfig(builder.config);
Throwable thrown = null;
for (URI host : builder.hosts) {
ClassicHttpRequest request = ClassicRequestBuilder.create(builder.method.name())
.setUri(concat(host, builder.uriBuilder))
.build();
request.setEntity(builder.entity);
try {
try {
return handler.apply(execute(request, context), request);
}
catch (IOException e) {
catcher.handle(e, request);
throw RetryException.wrap(e, request);
}
}
catch (RetryException e) {
if (thrown == null)
thrown = e.getCause();
else
thrown.addSuppressed(e.getCause());
if (builder.entity != null && ! builder.entity.isRepeatable()) {
log.log(WARNING, "Cannot retry " + request + " as entity is not repeatable");
break;
}
log.log(FINE, e.getCause(), () -> request + " failed; will retry");
}
}
if (thrown != null) {
if (thrown instanceof IOException)
throw new UncheckedIOException((IOException) thrown);
else if (thrown instanceof RuntimeException)
throw (RuntimeException) thrown;
else
throw new IllegalStateException("Illegal retry cause: " + thrown.getClass(), thrown);
}
throw new IllegalStateException("No hosts to perform the request against");
}
/** Append path to the given host, which may already contain a root path. */
static URI concat(URI host, URIBuilder pathAndQuery) {
URIBuilder builder = new URIBuilder(host);
List pathSegments = new ArrayList<>(builder.getPathSegments());
if ( ! pathSegments.isEmpty() && pathSegments.get(pathSegments.size() - 1).isEmpty())
pathSegments.remove(pathSegments.size() - 1);
pathSegments.addAll(pathAndQuery.getPathSegments());
try {
return builder.setPathSegments(pathSegments)
.setParameters(pathAndQuery.getQueryParams())
.build();
}
catch (URISyntaxException e) {
throw new IllegalArgumentException("URISyntaxException should not be possible here", e);
}
}
@Override
public ConfigServerClient.RequestBuilder send(HostStrategy hosts, Method method) {
return new RequestBuilder(hosts, method);
}
/** Builder for a request against a given set of hosts. */
class RequestBuilder implements ConfigServerClient.RequestBuilder {
private final Method method;
private final HostStrategy hosts;
private final URIBuilder uriBuilder = new URIBuilder();
private final List pathSegments = new ArrayList<>();
private HttpEntity entity;
private RequestConfig config = ConfigServerClient.defaultRequestConfig;
private ResponseVerifier verifier = ConfigServerClient.throwOnError;
private ExceptionHandler catcher = ConfigServerClient.retryAll;
private RequestBuilder(HostStrategy hosts, Method method) {
if ( ! hosts.iterator().hasNext())
throw new IllegalArgumentException("Host strategy cannot be empty");
this.hosts = hosts;
this.method = requireNonNull(method);
}
@Override
public RequestBuilder at(List pathSegments) {
this.pathSegments.addAll(pathSegments);
return this;
}
@Override
public ConfigServerClient.RequestBuilder body(byte[] json) {
return body(HttpEntities.create(json, ContentType.APPLICATION_JSON));
}
@Override
public RequestBuilder body(HttpEntity entity) {
this.entity = requireNonNull(entity);
return this;
}
@Override
public ConfigServerClient.RequestBuilder emptyParameters(List keys) {
for (String key : keys)
uriBuilder.setParameter(key, null);
return this;
}
@Override
public RequestBuilder parameters(List pairs) {
if (pairs.size() % 2 != 0)
throw new IllegalArgumentException("Must supply parameter key/values in pairs");
for (int i = 0; i < pairs.size(); ) {
String key = pairs.get(i++), value = pairs.get(i++);
if (value != null)
uriBuilder.setParameter(key, value);
}
return this;
}
@Override
public RequestBuilder timeout(Duration timeout) {
return config(RequestConfig.copy(config)
.setResponseTimeout(timeout.toMillis(), TimeUnit.MILLISECONDS)
.build());
}
@Override
public RequestBuilder config(RequestConfig config) {
this.config = requireNonNull(config);
return this;
}
@Override
public RequestBuilder catching(ExceptionHandler catcher) {
this.catcher = requireNonNull(catcher);
return this;
}
@Override
public RequestBuilder throwing(ResponseVerifier verifier) {
this.verifier = requireNonNull(verifier);
return this;
}
@Override
public String read() {
return handle((response, __) -> {
try (response) {
return response.getEntity() == null ? "" : EntityUtils.toString(response.getEntity());
}
catch (ParseException e) {
throw new IllegalStateException(e); // This isn't actually thrown by apache >_<
}
});
}
@Override
public T read(Function mapper) {
return handle((response, __) -> {
try (response) {
return mapper.apply(response.getEntity() == null ? new byte[0] : EntityUtils.toByteArray(response.getEntity()));
}
});
}
@Override
public void discard() throws UncheckedIOException, ResponseException {
handle((response, __) -> {
try (response) {
return null;
}
});
}
@Override
public HttpInputStream stream() throws UncheckedIOException, ResponseException {
return handle((response, __) -> new HttpInputStream(response));
}
@Override
public T handle(ResponseHandler handler) {
uriBuilder.setPathSegments(pathSegments);
return execute(this,
(response, request) -> {
try {
verifier.verify(response, request); // This throws on unacceptable responses.
return handler.handle(response, request);
}
catch (IOException | RuntimeException | Error e) {
try {
response.close();
}
catch (IOException f) {
e.addSuppressed(f);
}
if (e instanceof IOException) {
catcher.handle((IOException) e, request);
throw RetryException.wrap((IOException) e, request);
}
else
sneakyThrow(e); // e is a runtime exception or an error, so this is fine.
throw new AssertionError("Should not happen");
}
},
catcher);
}
}
@SuppressWarnings("unchecked")
private static void sneakyThrow(Throwable t) throws T {
throw (T) t;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy