/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.facebook.presto.client;
import com.facebook.presto.spi.type.TimeZoneKey;
import com.google.common.base.Splitter;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import io.airlift.http.client.FullJsonResponseHandler;
import io.airlift.http.client.HttpClient;
import io.airlift.http.client.HttpClient.HttpResponseFuture;
import io.airlift.http.client.HttpStatus;
import io.airlift.http.client.Request;
import io.airlift.json.JsonCodec;
import io.airlift.units.Duration;
import javax.annotation.concurrent.ThreadSafe;
import java.io.Closeable;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import static com.facebook.presto.client.PrestoHeaders.PRESTO_ADDED_PREPARE;
import static com.facebook.presto.client.PrestoHeaders.PRESTO_CLEAR_SESSION;
import static com.facebook.presto.client.PrestoHeaders.PRESTO_CLEAR_TRANSACTION_ID;
import static com.facebook.presto.client.PrestoHeaders.PRESTO_DEALLOCATED_PREPARE;
import static com.facebook.presto.client.PrestoHeaders.PRESTO_SET_SESSION;
import static com.facebook.presto.client.PrestoHeaders.PRESTO_STARTED_TRANSACTION_ID;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.net.HttpHeaders.USER_AGENT;
import static io.airlift.http.client.FullJsonResponseHandler.JsonResponse;
import static io.airlift.http.client.FullJsonResponseHandler.createFullJsonResponseHandler;
import static io.airlift.http.client.HttpStatus.Family;
import static io.airlift.http.client.HttpStatus.familyForStatusCode;
import static io.airlift.http.client.HttpUriBuilder.uriBuilderFrom;
import static io.airlift.http.client.Request.Builder.prepareDelete;
import static io.airlift.http.client.Request.Builder.prepareGet;
import static io.airlift.http.client.Request.Builder.preparePost;
import static io.airlift.http.client.StaticBodyGenerator.createStaticBodyGenerator;
import static io.airlift.http.client.StatusResponseHandler.StatusResponse;
import static io.airlift.http.client.StatusResponseHandler.createStatusResponseHandler;
import static java.lang.String.format;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.requireNonNull;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
@ThreadSafe
public class StatementClient
implements Closeable
{
private static final Splitter SESSION_HEADER_SPLITTER = Splitter.on('=').limit(2).trimResults();
private static final String USER_AGENT_VALUE = StatementClient.class.getSimpleName() +
"/" +
firstNonNull(StatementClient.class.getPackage().getImplementationVersion(), "unknown");
private final HttpClient httpClient;
private final FullJsonResponseHandler<QueryResults> responseHandler;
private final boolean debug;
private final String query;
private final AtomicReference<QueryResults> currentResults = new AtomicReference<>();
private final Map<String, String> setSessionProperties = new ConcurrentHashMap<>();
private final Set<String> resetSessionProperties = Sets.newConcurrentHashSet();
private final Map<String, String> addedPreparedStatements = new ConcurrentHashMap<>();
private final Set<String> deallocatedPreparedStatements = Sets.newConcurrentHashSet();
private final AtomicReference<String> startedtransactionId = new AtomicReference<>();
private final AtomicBoolean clearTransactionId = new AtomicBoolean();
private final AtomicBoolean closed = new AtomicBoolean();
private final AtomicBoolean gone = new AtomicBoolean();
private final AtomicBoolean valid = new AtomicBoolean(true);
private final TimeZoneKey timeZone;
private final long requestTimeoutNanos;
private final String user;
public StatementClient(HttpClient httpClient, JsonCodec<QueryResults> queryResultsCodec, ClientSession session, String query)
{
requireNonNull(httpClient, "httpClient is null");
requireNonNull(queryResultsCodec, "queryResultsCodec is null");
requireNonNull(session, "session is null");
requireNonNull(query, "query is null");
this.httpClient = httpClient;
this.responseHandler = createFullJsonResponseHandler(queryResultsCodec);
this.debug = session.isDebug();
this.timeZone = session.getTimeZone();
this.query = query;
this.requestTimeoutNanos = session.getClientRequestTimeout().roundTo(NANOSECONDS);
this.user = session.getUser();
Request request = buildQueryRequest(session, query);
JsonResponse<QueryResults> response = httpClient.execute(request, responseHandler);
if (response.getStatusCode() != HttpStatus.OK.code() || !response.hasValue()) {
throw requestFailedException("starting query", request, response);
}
processResponse(response);
}
private Request buildQueryRequest(ClientSession session, String query)
{
Request.Builder builder = prepareRequest(preparePost(), uriBuilderFrom(session.getServer()).replacePath("/v1/statement").build())
.setBodyGenerator(createStaticBodyGenerator(query, UTF_8));
if (session.getSource() != null) {
builder.setHeader(PrestoHeaders.PRESTO_SOURCE, session.getSource());
}
if (session.getClientInfo() != null) {
builder.setHeader(PrestoHeaders.PRESTO_CLIENT_INFO, session.getClientInfo());
}
if (session.getCatalog() != null) {
builder.setHeader(PrestoHeaders.PRESTO_CATALOG, session.getCatalog());
}
if (session.getSchema() != null) {
builder.setHeader(PrestoHeaders.PRESTO_SCHEMA, session.getSchema());
}
builder.setHeader(PrestoHeaders.PRESTO_TIME_ZONE, session.getTimeZone().getId());
if (session.getLocale() != null) {
builder.setHeader(PrestoHeaders.PRESTO_LANGUAGE, session.getLocale().toLanguageTag());
}
Map<String, String> property = session.getProperties();
for (Entry<String, String> entry : property.entrySet()) {
builder.addHeader(PrestoHeaders.PRESTO_SESSION, entry.getKey() + "=" + entry.getValue());
}
Map<String, String> statements = session.getPreparedStatements();
for (Entry<String, String> entry : statements.entrySet()) {
builder.addHeader(PrestoHeaders.PRESTO_PREPARED_STATEMENT, urlEncode(entry.getKey()) + "=" + urlEncode(entry.getValue()));
}
builder.setHeader(PrestoHeaders.PRESTO_TRANSACTION_ID, session.getTransactionId() == null ? "NONE" : session.getTransactionId());
return builder.build();
}
public String getQuery()
{
return query;
}
public TimeZoneKey getTimeZone()
{
return timeZone;
}
public boolean isDebug()
{
return debug;
}
public boolean isClosed()
{
return closed.get();
}
public boolean isGone()
{
return gone.get();
}
public boolean isFailed()
{
return currentResults.get().getError() != null;
}
public StatementStats getStats()
{
return currentResults.get().getStats();
}
public QueryResults current()
{
checkState(isValid(), "current position is not valid (cursor past end)");
return currentResults.get();
}
public QueryResults finalResults()
{
checkState((!isValid()) || isFailed(), "current position is still valid");
return currentResults.get();
}
public Map<String, String> getSetSessionProperties()
{
return ImmutableMap.copyOf(setSessionProperties);
}
public Set<String> getResetSessionProperties()
{
return ImmutableSet.copyOf(resetSessionProperties);
}
public Map<String, String> getAddedPreparedStatements()
{
return ImmutableMap.copyOf(addedPreparedStatements);
}
public Set<String> getDeallocatedPreparedStatements()
{
return ImmutableSet.copyOf(deallocatedPreparedStatements);
}
public String getStartedtransactionId()
{
return startedtransactionId.get();
}
public boolean isClearTransactionId()
{
return clearTransactionId.get();
}
public boolean isValid()
{
return valid.get() && (!isGone()) && (!isClosed());
}
private Request.Builder prepareRequest(Request.Builder builder, URI nextUri)
{
builder.setHeader(PrestoHeaders.PRESTO_USER, user);
builder.setHeader(USER_AGENT, USER_AGENT_VALUE)
.setUri(nextUri);
return builder;
}
public boolean advance()
{
URI nextUri = current().getNextUri();
if (isClosed() || (nextUri == null)) {
valid.set(false);
return false;
}
Request request = prepareRequest(prepareGet(), nextUri).build();
Exception cause = null;
long start = System.nanoTime();
long attempts = 0;
do {
// back-off on retry
if (attempts > 0) {
try {
MILLISECONDS.sleep(attempts * 100);
}
catch (InterruptedException e) {
try {
close();
}
finally {
Thread.currentThread().interrupt();
}
throw new RuntimeException("StatementClient thread was interrupted");
}
}
attempts++;
JsonResponse<QueryResults> response;
try {
response = httpClient.execute(request, responseHandler);
}
catch (RuntimeException e) {
cause = e;
continue;
}
if (response.getStatusCode() == HttpStatus.OK.code() && response.hasValue()) {
processResponse(response);
return true;
}
if (response.getStatusCode() != HttpStatus.SERVICE_UNAVAILABLE.code()) {
throw requestFailedException("fetching next", request, response);
}
}
while (((System.nanoTime() - start) < requestTimeoutNanos) && !isClosed());
gone.set(true);
throw new RuntimeException("Error fetching next", cause);
}
private void processResponse(JsonResponse<QueryResults> response)
{
for (String setSession : response.getHeaders(PRESTO_SET_SESSION)) {
List<String> keyValue = SESSION_HEADER_SPLITTER.splitToList(setSession);
if (keyValue.size() != 2) {
continue;
}
setSessionProperties.put(keyValue.get(0), keyValue.size() > 1 ? keyValue.get(1) : "");
}
for (String clearSession : response.getHeaders(PRESTO_CLEAR_SESSION)) {
resetSessionProperties.add(clearSession);
}
for (String entry : response.getHeaders(PRESTO_ADDED_PREPARE)) {
List<String> keyValue = SESSION_HEADER_SPLITTER.splitToList(entry);
if (keyValue.size() != 2) {
continue;
}
addedPreparedStatements.put(urlDecode(keyValue.get(0)), urlDecode(keyValue.get(1)));
}
for (String entry : response.getHeaders(PRESTO_DEALLOCATED_PREPARE)) {
deallocatedPreparedStatements.add(urlDecode(entry));
}
String startedTransactionId = response.getHeader(PRESTO_STARTED_TRANSACTION_ID);
if (startedTransactionId != null) {
this.startedtransactionId.set(startedTransactionId);
}
if (response.getHeader(PRESTO_CLEAR_TRANSACTION_ID) != null) {
clearTransactionId.set(true);
}
currentResults.set(response.getValue());
}
private RuntimeException requestFailedException(String task, Request request, JsonResponse<QueryResults> response)
{
gone.set(true);
if (!response.hasValue()) {
return new RuntimeException(
format("Error %s at %s returned an invalid response: %s [Error: %s]", task, request.getUri(), response, response.getResponseBody()),
response.getException());
}
return new RuntimeException(format("Error %s at %s returned %s: %s", task, request.getUri(), response.getStatusCode(), response.getStatusMessage()));
}
public boolean cancelLeafStage(Duration timeout)
{
checkState(!isClosed(), "client is closed");
URI uri = current().getPartialCancelUri();
if (uri == null) {
return false;
}
Request request = prepareRequest(prepareDelete(), uri).build();
HttpResponseFuture<StatusResponse> response = httpClient.executeAsync(request, createStatusResponseHandler());
try {
StatusResponse status = response.get(timeout.toMillis(), MILLISECONDS);
return familyForStatusCode(status.getStatusCode()) == Family.SUCCESSFUL;
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw Throwables.propagate(e);
}
catch (ExecutionException e) {
throw Throwables.propagate(e.getCause());
}
catch (TimeoutException e) {
return false;
}
}
@Override
public void close()
{
if (!closed.getAndSet(true)) {
URI uri = currentResults.get().getNextUri();
if (uri != null) {
Request request = prepareRequest(prepareDelete(), uri).build();
httpClient.executeAsync(request, createStatusResponseHandler());
}
}
}
private static String urlEncode(String value)
{
try {
return URLEncoder.encode(value, "UTF-8");
}
catch (UnsupportedEncodingException e) {
throw new AssertionError(e);
}
}
private static String urlDecode(String value)
{
try {
return URLDecoder.decode(value, "UTF-8");
}
catch (UnsupportedEncodingException e) {
throw new AssertionError(e);
}
}
}