/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 gobblin.writer.http;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.conn.ConnectionRequest;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Optional;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import gobblin.instrumented.writer.InstrumentedDataWriter;
import gobblin.util.ExecutorsUtils;
/**
* Base class for HTTP writers. Defines the main extension points for different implementations.
*/
public abstract class AbstractHttpWriter<D> extends InstrumentedDataWriter<D> implements HttpWriterDecoration<D> {
// Immutable state
protected final Logger log;
protected final boolean debugLogEnabled;
protected final CloseableHttpClient client;
private final ListeningExecutorService singleThreadPool;
// Mutable state
private URI curHttpHost = null;
private long numRecordsWritten = 0L;
private long numBytesWritten = 0L; //AbstractHttpWriter won't update as it could be expensive.
Optional<HttpUriRequest> curRequest = Optional.absent();
class HttpClientConnectionManagerWithConnTracking extends DelegatingHttpClientConnectionManager {
public HttpClientConnectionManagerWithConnTracking(HttpClientConnectionManager fallback) {
super(fallback);
}
@Override
public ConnectionRequest requestConnection(HttpRoute route, Object state) {
try {
onConnect(new URI(route.getTargetHost().toURI()));
} catch (IOException | URISyntaxException e) {
throw new RuntimeException("onConnect() callback failure: " + e, e);
}
return super.requestConnection(route, state);
}
}
@SuppressWarnings("rawtypes")
public AbstractHttpWriter(AbstractHttpWriterBuilder builder) {
super(builder.getState());
this.log = builder.getLogger().isPresent() ? (Logger)builder.getLogger() : LoggerFactory.getLogger(this.getClass());
this.debugLogEnabled = this.log.isDebugEnabled();
HttpClientBuilder httpClientBuilder = builder.getHttpClientBuilder();
httpClientBuilder.setConnectionManager(new HttpClientConnectionManagerWithConnTracking(builder.getHttpConnManager()));
this.client = httpClientBuilder.build();
this.singleThreadPool = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
if (builder.getSvcEndpoint().isPresent()) {
setCurServerHost((URI) builder.getSvcEndpoint().get());
}
}
/**
* {@inheritDoc}
*/
@Override
public void cleanup() throws IOException {
this.client.close();
ExecutorsUtils.shutdownExecutorService(this.singleThreadPool, Optional.of(log));
}
/**
* {@inheritDoc}
*/
@Override
public void close() throws IOException {
cleanup();
super.close();
}
/**
* {@inheritDoc}
*/
@Override
public long recordsWritten() {
return this.numRecordsWritten;
}
/**
* {@inheritDoc}
*/
@Override
public long bytesWritten() throws IOException {
return this.numBytesWritten;
}
/**
* Send and process the request. If it's a retry request, skip onNewRecord method call and go straight sending request.
* {@inheritDoc}
*/
@Override
public void writeImpl(D record) throws IOException {
if (!isRetry()) {
//If currentRequest is still here, it means this is retry request.
//In this case, don't invoke onNewRecord again as onNewRecord is not guaranteed to be idempotent.
//(e.g: If you do batch processing duplicate record can go in, etc.)
curRequest = onNewRecord(record);
}
if (curRequest.isPresent()) {
ListenableFuture<CloseableHttpResponse> responseFuture = sendRequest(curRequest.get());
try (CloseableHttpResponse response = waitForResponse(responseFuture)) {
processResponse(response);
}
curRequest = Optional.absent(); //Clear request if successful
}
numRecordsWritten++;
}
/**
* Prior to commit, it will invoke flush method to flush any remaining item if writer uses batch
* {@inheritDoc}
* @see gobblin.instrumented.writer.InstrumentedDataWriterBase#commit()
*/
@Override
public void commit() throws IOException {
flush();
super.commit();
}
/**
* If writer supports batch, override this method.
* (Be aware of failure and retry as flush can be called multiple times in case of failure @see SalesforceRestWriter )
*/
public void flush() { }
/**
* Sends request using single thread pool so that it can be easily terminated(use case: time out)
* {@inheritDoc}
* @see gobblin.writer.http.HttpWriterDecoration#sendRequest(org.apache.http.client.methods.HttpUriRequest)
*/
@Override
public ListenableFuture<CloseableHttpResponse> sendRequest(final HttpUriRequest request) throws IOException {
return singleThreadPool.submit(new Callable<CloseableHttpResponse>() {
@Override
public CloseableHttpResponse call() throws Exception {
return client.execute(request);
}
});
}
/**
* Checks if it's retry request.
* All successful request should make currentRequest absent. If currentRequest still exists, it means there was a failure.
* There's couple of methods need this indicator such as onNewRecord, since it is not a new record.
* @return true if current request it holds is retry.
*/
public boolean isRetry() {
return curRequest.isPresent();
}
/**
* Default implementation is to use HttpClients socket timeout which is waiting based on elapsed time between
* last packet sent from client till receive it from server.
*
* {@inheritDoc}
* @see gobblin.writer.http.HttpWriterDecoration#waitForResponse(com.google.common.util.concurrent.ListenableFuture)
*/
@Override
public CloseableHttpResponse waitForResponse(ListenableFuture<CloseableHttpResponse> responseFuture) {
try {
return responseFuture.get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
/**
* Default implementation where any status code equal to or greater than 400 is regarded as a failure.
* {@inheritDoc}
* @see gobblin.writer.http.HttpWriterDecoration#processResponse(org.apache.http.HttpResponse)
*/
@Override
public void processResponse(CloseableHttpResponse response) throws IOException, UnexpectedResponseException {
if (response.getStatusLine().getStatusCode() >= 400) {
if (response.getEntity() != null) {
throw new RuntimeException("Failed. " + EntityUtils.toString(response.getEntity())
+ " , response: " + ToStringBuilder.reflectionToString(response, ToStringStyle.SHORT_PREFIX_STYLE));
}
throw new RuntimeException("Failed. Response: " + ToStringBuilder.reflectionToString(response, ToStringStyle.SHORT_PREFIX_STYLE));
}
}
public Logger getLog() {
return this.log;
}
public URI getCurServerHost() {
if (null == this.curHttpHost) {
setCurServerHost(chooseServerHost());
}
if (null == this.curHttpHost) {
throw new RuntimeException("No server host selected!");
}
return this.curHttpHost;
}
/** Clears the current http host so that next request will trigger a new selection using
* {@link #chooseServerHost() */
void clearCurServerHost() {
this.curHttpHost = null;
}
void setCurServerHost(URI curHttpHost) {
this.log.info("Setting current HTTP server host to: " + curHttpHost);
this.curHttpHost = curHttpHost;
}
}