![JAR search and dependency download from the Maven repository](/logo.png)
io.nextop.client.node.http.HttpNode Maven / Gradle / Ivy
package io.nextop.client.node.http;
import io.nextop.*;
import io.nextop.client.MessageControl;
import io.nextop.client.MessageControlState;
import io.nextop.client.node.AbstractMessageControlNode;
import io.nextop.client.retry.SendStrategy;
import io.nextop.org.apache.http.*;
import io.nextop.org.apache.http.client.HttpRequestRetryHandler;
import io.nextop.org.apache.http.client.config.RequestConfig;
import io.nextop.org.apache.http.client.methods.*;
import io.nextop.org.apache.http.client.protocol.HttpClientContext;
import io.nextop.org.apache.http.client.protocol.RequestClientConnControl;
import io.nextop.org.apache.http.client.utils.URIUtils;
import io.nextop.org.apache.http.config.ConnectionConfig;
import io.nextop.org.apache.http.config.MessageConstraints;
import io.nextop.org.apache.http.conn.*;
import io.nextop.org.apache.http.conn.HttpConnectionFactory;
import io.nextop.org.apache.http.conn.routing.HttpRoute;
import io.nextop.org.apache.http.entity.ContentLengthStrategy;
import io.nextop.org.apache.http.impl.DefaultConnectionReuseStrategy;
import io.nextop.org.apache.http.impl.client.DefaultConnectionKeepAliveStrategy;
import io.nextop.org.apache.http.impl.conn.ConnectionShutdownException;
import io.nextop.org.apache.http.impl.conn.DefaultHttpResponseParserFactory;
import io.nextop.org.apache.http.impl.conn.DefaultManagedHttpClientConnection;
import io.nextop.org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import io.nextop.org.apache.http.impl.entity.LaxContentLengthStrategy;
import io.nextop.org.apache.http.impl.entity.StrictContentLengthStrategy;
import io.nextop.org.apache.http.impl.execchain.*;
import io.nextop.org.apache.http.impl.io.DefaultHttpRequestWriterFactory;
import io.nextop.org.apache.http.io.HttpMessageParserFactory;
import io.nextop.org.apache.http.io.HttpMessageWriterFactory;
import io.nextop.org.apache.http.io.SessionInputBuffer;
import io.nextop.org.apache.http.io.SessionOutputBuffer;
import io.nextop.org.apache.http.protocol.*;
import rx.functions.Func1;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.net.Socket;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CodingErrorAction;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public final class HttpNode extends AbstractMessageControlNode {
public static final class Config {
public final String userAgent;
public final int maxConcurrentConnections;
public Config(String userAgent, int maxConcurrentConnections) {
this.userAgent = userAgent;
this.maxConcurrentConnections = maxConcurrentConnections;
}
}
public static final Config DEFAULT_CONFIG = new Config(
/** FIXME set a user agent with the version, e.g. Nextop/0.1.4 */ "Nextop",
2
);
/** retry once immediately, then defer to the retake strategy.
* this is a heuristic in case the request got redirected to a bad box. */
public static final SendStrategy DEFAULT_SEND_STRATEGY = new SendStrategy.Builder().init(0, TimeUnit.MILLISECONDS
).repeat(1
).build();
public static final SendStrategy DEFAULT_RETAKE_STRATEGY = new SendStrategy.Builder()
.withUniformRandom(2000, TimeUnit.MILLISECONDS)
.repeatIndefinitely()
.build();
/** guaranteed to repeat indefinitely, in case a custom retake strategy expires */
static final SendStrategy FALLBACK_RETAKE_STRATEGY = DEFAULT_RETAKE_STRATEGY;
/** yield at this many of bytes to emit progress,
* transfer request, etc. */
static final int DEFAULT_YIELD_Q_BYTES = 4 * 1024;
final Config config;
final PoolingHttpClientConnectionManager clientConnectionManager =
new PoolingHttpClientConnectionManager(new NextopHttpClientConnectionFactory());
// volatile for reads
volatile boolean active = false;
@Nullable
List looperThreads = null;
// configuration
/* retry in the http node is controlled by two strategies: send and retake.
* send = [send attempt, [send delay, [send attempt, ...]?]?]
* retake (for yieldable messages) = [[send], [move to back, other sends, delay?, [send], [move to back, other sends, delay?, [send], ...]?]?]
* retake (for un-yieldable messages) = [[send], [delay?, [send], [delay?, [send], ...]?]?]
*
* the delay in the send sequence is controlled by #sendStrategy
* the delay in the retake sequences is controlled by #retakeStrategy
*/
// this is the strategy for one entry before possibly yielding
// each time the entry is taken, the strategy is run from the beginning
// this is a really aggressive strategy that relies on CONNECTIVITY STATUS
// to stop retrying on a bad connection
volatile SendStrategy sendStrategy = DEFAULT_SEND_STRATEGY;
// if an entry yields or is otherwise taken after a failed take (either a protocol error or sendStrategy expired)
// this strategy runs. this strategy should repeat indefinitely, since an entry can circulate indefinitely
volatile SendStrategy retakeStrategy = DEFAULT_RETAKE_STRATEGY;
@Nullable
volatile Wire.Adapter wireAdapter = null;
public HttpNode() {
this(DEFAULT_CONFIG);
}
public HttpNode(Config config) {
this.config = config;
}
/////// CONFIG ///////
public void setSendStrategy(SendStrategy sendStrategy) {
this.sendStrategy = sendStrategy;
// loopers pick this up eventually
}
public void setWireAdapter(Wire.Adapter wireAdapter) {
this.wireAdapter = wireAdapter;
// loopers pick this up eventually
}
/////// NODE ///////
@Override
protected void initSelf(@Nullable Bundle savedState) {
// ready to receive
upstream.onActive(true);
}
@Override
public void onActive(boolean active) {
if (this.active != active) {
this.active = active;
if (active) {
assert null == looperThreads;
MessageControlState mcs = getMessageControlState();
SharedLooperState sls = new SharedLooperState();
// note that the message control state coordinates between multiple loopers
// (and between multiple nodes)
int n = config.maxConcurrentConnections;
Thread[] threads = new Thread[n];
for (int i = 0; i < n; ++i) {
threads[i] = new RequestLooper(mcs, sls);
}
looperThreads = Arrays.asList(threads);
for (int i = 0; i < n; ++i) {
threads[i].start();
}
} else {
assert null != looperThreads;
for (Thread t : looperThreads) {
t.interrupt();
}
looperThreads = null;
}
}
}
@Override
public void onMessageControl(MessageControl mc) {
assert MessageControl.Direction.SEND.equals(mc.dir);
assert active;
if (active) {
MessageControlState mcs = getMessageControlState();
if (!mcs.onActiveMessageControl(mc, upstream)) {
mcs.add(mc);
}
}
// TODO else send back upstream?
}
private static final class SharedLooperState {
final Map mostRecentSends = new ConcurrentHashMap(8);
/** this is here to handle a one bad case: an entry is retaken that can yield,
* but it has been less than this number of ms since the last yield.
* To avoid spinning the CPU yielding the same id(s), wait this number of ms before the next eval. */
final int retakeYieldQMs = 50;
SharedLooperState() {
}
static final class MostRecentSend {
final long nanos;
final SendStrategy activeStrategy;
MostRecentSend(long nanos, SendStrategy activeStrategy) {
this.nanos = nanos;
this.activeStrategy = activeStrategy;
}
}
}
static final Func1 IS_SENDABLE = new Func1() {
@Override
public Boolean call(MessageControlState.Entry entry) {
// HTTP can't send to the Nextop local route
return !Message.isLocal(entry.message.route);
}
};
final class RequestLooper extends Thread {
final MessageControlState mcs;
final SharedLooperState sls;
// set at the beginning of a request; reset at the end of request
@Nullable
ProgressCallback progressCallback = null;
/** local cache of HttpNode#wireAdapter */
@Nullable
Wire.Adapter wireAdapter = null;
RequestLooper(MessageControlState mcs, SharedLooperState sls) {
this.mcs = mcs;
this.sls = sls;
}
@Override
public void run() {
top:
while (active) {
@Nullable MessageControlState.Entry entry;
try {
entry = mcs.takeFirstAvailable(IS_SENDABLE, HttpNode.this,
Integer.MAX_VALUE, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
continue top;
}
if (null != entry) {
@Nullable SharedLooperState.MostRecentSend mostRecentSend = sls.mostRecentSends.get(entry.id);
if (null != mostRecentSend) {
int delayMs = (int) mostRecentSend.activeStrategy.getDelay(TimeUnit.MILLISECONDS);
int elapsedMs = (int) TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - mostRecentSend.nanos);
if (elapsedMs < delayMs) {
// - if entry is not yieldable, wait delayMs - elapsedMs
// - else (yieldable)
// - if elapsesMs < retakeYieldQMs
// - if delayMs - elapsesMs < retakeYieldQMs, wait delayMs - elapsesMs then exec
// - else wait retakeYieldQMs then yield
// - else yield
int remainingMs = delayMs - elapsedMs;
if (!Message.isYieldable(entry.message)) {
try {
Thread.sleep(remainingMs);
} catch (InterruptedException e) {
mcs.release(entry.id, HttpNode.this);
continue;
}
} else {
// message is yieldable
if (elapsedMs < sls.retakeYieldQMs) {
if (remainingMs < sls.retakeYieldQMs) {
try {
Thread.sleep(remainingMs);
} catch (InterruptedException e) {
mcs.release(entry.id, HttpNode.this);
continue;
}
} else {
try {
Thread.sleep(sls.retakeYieldQMs);
} catch (InterruptedException e) {
mcs.release(entry.id, HttpNode.this);
continue;
}
mcs.yield(entry.id);
mcs.release(entry.id, HttpNode.this);
continue;
}
} else {
mcs.yield(entry.id);
mcs.release(entry.id, HttpNode.this);
continue;
}
}
}
}
this.wireAdapter = HttpNode.this.wireAdapter;
assert null == entry.end;
try {
end(entry, execute(entry));
} catch (IOException e) {
retake(entry);
} catch (HttpException e) {
retake(entry);
} catch (Throwable t) {
// an internal issue
// can never recover from this (assume the system is deterministic)
end(entry, MessageControlState.End.ERROR);
}
}
}
}
private void retake(MessageControlState.Entry entry) {
assert null == entry.end;
SendStrategy nextStrategy;
@Nullable SharedLooperState.MostRecentSend mostRecentSend = sls.mostRecentSends.get(entry.id);
if (null != mostRecentSend) {
nextStrategy = mostRecentSend.activeStrategy.retry();
} else {
nextStrategy = retakeStrategy.retry();
}
if (!nextStrategy.isSend()) {
// this case indicates a bug in a custom retake strategy, where the strategy does not repeat indefinitely
nextStrategy = FALLBACK_RETAKE_STRATEGY.retry();
}
assert nextStrategy.isSend();
sls.mostRecentSends.put(entry.id, new SharedLooperState.MostRecentSend(System.nanoTime(), nextStrategy));
// at this point the entry was elected to yield
// in this case, check whether the message has indicated it can be moved to the end of the line
if (Message.isYieldable(entry.message)) {
mcs.yield(entry.id);
}
mcs.release(entry.id, HttpNode.this);
}
private void end(final MessageControlState.Entry entry, MessageControlState.End end) {
assert null == entry.end;
sls.mostRecentSends.remove(entry.id);
mcs.remove(entry.id, end);
final Route route = entry.message.inboxRoute();
switch (end) {
case COMPLETED:
post(new Runnable() {
@Override
public void run() {
upstream.onMessageControl(MessageControl.receive(MessageControl.Type.COMPLETE, route));
}
});
break;
case ERROR:
post(new Runnable() {
@Override
public void run() {
upstream.onMessageControl(MessageControl.receive(MessageControl.Type.ERROR, route));
}
});
break;
default:
throw new IllegalStateException();
}
}
private MessageControlState.End execute(final MessageControlState.Entry entry) throws IOException, HttpException {
final HttpRequest request;
try {
request = Message.toHttpRequest(entry.message);
} catch (URISyntaxException e) {
// can never send this
return MessageControlState.End.ERROR;
}
final HttpHost target;
try {
target = Message.toHttpHost(entry.message);
} catch (URISyntaxException e) {
// can never send this
return MessageControlState.End.ERROR;
}
final Message responseMessage;
progressCallback = new ProgressAdapter(entry);
try {
HttpResponse response = doExecute(createExecChain(entry),
target, request, null);
responseMessage = Message.fromHttpResponse(response).setRoute(entry.message.inboxRoute()).build();
} finally {
progressCallback = null;
}
post(new Runnable() {
@Override
public void run() {
upstream.onMessageControl(MessageControl.receive(responseMessage));
}
});
return MessageControlState.End.COMPLETED;
}
/** lifted version of {@link io.nextop.org.apache.http.impl.client.CloseableHttpClient#doExecute} */
private CloseableHttpResponse doExecute(
ClientExecChain execChain,
HttpHost target,
HttpRequest request,
@Nullable HttpContext context) throws IOException, HttpException {
HttpExecutionAware execAware = null;
if (request instanceof HttpExecutionAware) {
execAware = (HttpExecutionAware) request;
}
final HttpRequestWrapper wrapper = HttpRequestWrapper.wrap(request);
final HttpClientContext localcontext = HttpClientContext.adapt(
null != context ? context : new BasicHttpContext());
final HttpRoute route = new HttpRoute(target);
RequestConfig config = null;
if (request instanceof Configurable) {
config = ((Configurable) request).getConfig();
}
if (config != null) {
localcontext.setRequestConfig(config);
}
return execChain.execute(route, wrapper, localcontext, execAware);
}
private ClientExecChain createExecChain(MessageControlState.Entry entry) {
NextopClientExec nextopExec = new NextopClientExec(
new NextopHttpRequestExecutor(progressCallback),
clientConnectionManager,
DefaultConnectionReuseStrategy.INSTANCE,
DefaultConnectionKeepAliveStrategy.INSTANCE,
config.userAgent
);
return new RetryExec(nextopExec, new NextopHttpRequestRetryHandler(sendStrategy, entry, mcs));
}
}
final class ProgressAdapter implements ProgressCallback {
final MessageControlState.Entry entry;
MessageControlState mcs = getMessageControlState();
ProgressAdapter(MessageControlState.Entry entry) {
this.entry = entry;
}
@Override
public void onSendStarted(int tryCount) {
post(new Runnable() {
@Override
public void run() {
mcs.setOutboxTransferProgress(entry.id,
MessageControlState.TransferProgress.none(entry.id));
}
});
}
@Override
public void onSendProgress(final long sentBytes, final long sendTotalBytes) {
post(new Runnable() {
@Override
public void run() {
mcs.setOutboxTransferProgress(entry.id,
MessageControlState.TransferProgress.create(entry.id, sentBytes, sendTotalBytes));
}
});
}
@Override
public void onSendCompleted(final long sentBytes, final long sendTotalBytes) {
post(new Runnable() {
@Override
public void run() {
mcs.setOutboxTransferProgress(entry.id,
MessageControlState.TransferProgress.create(entry.id, sentBytes, sendTotalBytes));
}
});
}
@Override
public void onReceiveStarted(int tryCount) {
post(new Runnable() {
@Override
public void run() {
mcs.setInboxTransferProgress(entry.id,
MessageControlState.TransferProgress.none(entry.id));
}
});
}
@Override
public void onReceiveProgress(final long receivedBytes, final long receiveTotalBytes) {
post(new Runnable() {
@Override
public void run() {
mcs.setInboxTransferProgress(entry.id,
MessageControlState.TransferProgress.create(entry.id, receivedBytes, receiveTotalBytes));
}
});
}
@Override
public void onReceiveCompleted(final long receivedBytes, final long receiveTotalBytes) {
post(new Runnable() {
@Override
public void run() {
mcs.setInboxTransferProgress(entry.id,
MessageControlState.TransferProgress.create(entry.id, receivedBytes, receiveTotalBytes));
}
});
}
}
/** can be called from any thread. Expect the IO thread to call. */
static interface ProgressCallback {
void onSendStarted(int tryCount);
void onSendProgress(long sentBytes, long sendTotalBytes);
void onSendCompleted(long sentBytes, long sendTotalBytes);
void onReceiveStarted(int tryCount);
void onReceiveProgress(long receivedBytes, long receiveTotalBytes);
void onReceiveCompleted(long receivedBytes, long receiveTotalBytes);
}
// PoolingHttpClientConnectionManager
// -- uses ManagedHttpClientConnectionFactory
// -- uses LoggingManagedHttpClientConnection #getOutputStream(socket) #getInputStream(Socket)
// -- implements ManagedHttpClientConnection #bind(Socket)
// -- extends DefaultManagedHttpClientConnection WANT TO USE THIS
// DefaultConnectionReuseStrategy
// FIXME wrap in a retry exec with NextopHttpRequestRetryHandler
// FIXME use the message property idempotent to influence retry also
// DefaultHttpRequestRetryHandler
// FIXME create Nextop exec chain per request
// FIXME
// NextopRetryExec:
// check that request is still the head before retry (this is sort of the solution to head of line blocking)
static final class NextopHttpRequestRetryHandler implements HttpRequestRetryHandler {
private SendStrategy sendStrategy;
private final MessageControlState.Entry entry;
private final MessageControlState mcs;
NextopHttpRequestRetryHandler(SendStrategy sendStrategy,
MessageControlState.Entry entry, MessageControlState mcs) {
this.sendStrategy = sendStrategy;
this.entry = entry;
this.mcs = mcs;
}
@Override
public boolean retryRequest(final IOException exception,
final int executionCount,
final HttpContext context) {
sendStrategy = sendStrategy.retry();
if (!sendStrategy.isSend()) {
return false;
}
// check ended - don't retry an ended entry
if (null != entry.end) {
return false;
}
// retry if not fully sent (server starts processing on fully received message)
// or if the request is idempotent (either because it is nullipotent or marked as idempotent)
if (HttpClientContext.adapt(context).isRequestSent() && Message.isIdempotent(entry.message)) {
return false;
}
// fail if there is a higher priority request
int timeoutMs = (int) sendStrategy.getDelay(TimeUnit.MILLISECONDS);
try {
return !mcs.hasFirstAvailable(entry.id, timeoutMs, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
return false;
}
}
}
// implement a subclass of HttpRequestExector that surfaces SendIOException(final chunk), ReceiveIOException
// implement a custom RetryHandler that always retries if send failed on not final chunk,
/** based on org.apache.http.impl.execchain.MinimalClientExec
*/
static final class NextopClientExec implements ClientExecChain {
// ProgressCallback progressCallback;
private final HttpRequestExecutor requestExecutor;
private final HttpClientConnectionManager connManager;
private final ConnectionReuseStrategy reuseStrategy;
private final ConnectionKeepAliveStrategy keepAliveStrategy;
private final HttpProcessor httpProcessor;
public NextopClientExec(
final HttpRequestExecutor requestExecutor,
final HttpClientConnectionManager connManager,
final ConnectionReuseStrategy reuseStrategy,
final ConnectionKeepAliveStrategy keepAliveStrategy,
String userAgent) {
this.httpProcessor = new ImmutableHttpProcessor(
new RequestContent(),
new RequestTargetHost(),
new RequestClientConnControl(),
new RequestUserAgent(userAgent));
this.requestExecutor = requestExecutor;
this.connManager = connManager;
this.reuseStrategy = reuseStrategy;
this.keepAliveStrategy = keepAliveStrategy;
// this.progressCallback = progressCallback;
}
static void rewriteRequestURI(
final HttpRequestWrapper request,
final HttpRoute route) throws ProtocolException {
try {
URI uri = request.getURI();
if (uri != null) {
// Make sure the request URI is relative
if (uri.isAbsolute()) {
uri = URIUtils.rewriteURI(uri, null, true);
} else {
uri = URIUtils.rewriteURI(uri);
}
request.setURI(uri);
}
} catch (final URISyntaxException ex) {
throw new ProtocolException("Invalid URI: " + request.getRequestLine().getUri(), ex);
}
}
@Override
public CloseableHttpResponse execute(
final HttpRoute route,
final HttpRequestWrapper request,
final HttpClientContext context,
final HttpExecutionAware execAware) throws IOException, HttpException {
rewriteRequestURI(request, route);
final ConnectionRequest connRequest = connManager.requestConnection(route, null);
if (execAware != null) {
if (execAware.isAborted()) {
connRequest.cancel();
throw new RequestAbortedException("Request aborted");
} else {
execAware.setCancellable(connRequest);
}
}
final RequestConfig config = context.getRequestConfig();
final HttpClientConnection managedConn;
try {
final int timeout = config.getConnectionRequestTimeout();
managedConn = connRequest.get(timeout > 0 ? timeout : 0, TimeUnit.MILLISECONDS);
} catch(final InterruptedException interrupted) {
Thread.currentThread().interrupt();
throw new RequestAbortedException("Request aborted", interrupted);
} catch(final ExecutionException ex) {
Throwable cause = ex.getCause();
if (cause == null) {
cause = ex;
}
throw new RequestAbortedException("Request execution failed", cause);
}
final NextopConnectionHolder releaseTrigger = new NextopConnectionHolder(connManager, managedConn);
try {
if (execAware != null) {
if (execAware.isAborted()) {
releaseTrigger.close();
throw new RequestAbortedException("Request aborted");
} else {
execAware.setCancellable(releaseTrigger);
}
}
if (!managedConn.isOpen()) {
final int timeout = config.getConnectTimeout();
this.connManager.connect(
managedConn,
route,
timeout > 0 ? timeout : 0,
context);
this.connManager.routeComplete(managedConn, route, context);
}
final int timeout = config.getSocketTimeout();
if (timeout >= 0) {
managedConn.setSocketTimeout(timeout);
}
HttpHost target = null;
final HttpRequest original = request.getOriginal();
if (original instanceof HttpUriRequest) {
final URI uri = ((HttpUriRequest) original).getURI();
if (uri.isAbsolute()) {
target = new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme());
}
}
if (target == null) {
target = route.getTargetHost();
}
context.setAttribute(HttpCoreContext.HTTP_TARGET_HOST, target);
context.setAttribute(HttpCoreContext.HTTP_REQUEST, request);
context.setAttribute(HttpCoreContext.HTTP_CONNECTION, managedConn);
context.setAttribute(HttpClientContext.HTTP_ROUTE, route);
// TODO managedConn is an instance of CPoolProxy
// TODO an easy way to call getConnection or get the connection out of it without reflection?
httpProcessor.process(request, context);
final HttpResponse response = requestExecutor.execute(request, managedConn, context);
httpProcessor.process(response, context);
// The connection is in or can be brought to a re-usable state.
if (reuseStrategy.keepAlive(response, context)) {
// Set the idle duration of this connection
final long duration = keepAliveStrategy.getKeepAliveDuration(response, context);
releaseTrigger.setValidFor(duration, TimeUnit.MILLISECONDS);
releaseTrigger.markReusable();
} else {
releaseTrigger.markNonReusable();
}
// check for entity, release connection if possible
final HttpEntity entity = response.getEntity();
if (entity == null || !entity.isStreaming()) {
// connection not needed and (assumed to be) in re-usable state
releaseTrigger.releaseConnection();
return new NextopHttpResponseProxy(response, null);
} else {
return new NextopHttpResponseProxy(response, releaseTrigger);
}
} catch (final ConnectionShutdownException ex) {
final InterruptedIOException ioex = new InterruptedIOException(
"Connection has been shut down");
ioex.initCause(ex);
throw ioex;
} catch (final HttpException ex) {
releaseTrigger.abortConnection();
throw ex;
} catch (final IOException ex) {
releaseTrigger.abortConnection();
throw ex;
} catch (final RuntimeException ex) {
releaseTrigger.abortConnection();
throw ex;
}
}
}
static final class NextopHttpClientConnectionFactory
implements HttpConnectionFactory {
private final HttpMessageWriterFactory requestWriterFactory;
private final HttpMessageParserFactory responseParserFactory;
private final ContentLengthStrategy incomingContentStrategy;
private final ContentLengthStrategy outgoingContentStrategy;
private final AtomicInteger connectionCounter = new AtomicInteger(0);
public NextopHttpClientConnectionFactory(
@Nullable HttpMessageWriterFactory requestWriterFactory,
@Nullable HttpMessageParserFactory responseParserFactory,
@Nullable ContentLengthStrategy incomingContentStrategy,
@Nullable ContentLengthStrategy outgoingContentStrategy) {
super();
this.requestWriterFactory = requestWriterFactory != null ? requestWriterFactory :
DefaultHttpRequestWriterFactory.INSTANCE;
this.responseParserFactory = responseParserFactory != null ? responseParserFactory :
DefaultHttpResponseParserFactory.INSTANCE;
this.incomingContentStrategy = incomingContentStrategy != null ? incomingContentStrategy :
LaxContentLengthStrategy.INSTANCE;
this.outgoingContentStrategy = outgoingContentStrategy != null ? outgoingContentStrategy :
StrictContentLengthStrategy.INSTANCE;
}
public NextopHttpClientConnectionFactory(
@Nullable HttpMessageWriterFactory requestWriterFactory,
@Nullable HttpMessageParserFactory responseParserFactory) {
this(requestWriterFactory, responseParserFactory, null, null);
}
public NextopHttpClientConnectionFactory(
@Nullable HttpMessageParserFactory responseParserFactory) {
this(null, responseParserFactory);
}
public NextopHttpClientConnectionFactory() {
this(null, null);
}
@Override
public NextopHttpClientConnection create(final HttpRoute route, final ConnectionConfig config) {
final ConnectionConfig cconfig = config != null ? config : ConnectionConfig.DEFAULT;
CharsetDecoder chardecoder = null;
CharsetEncoder charencoder = null;
final Charset charset = cconfig.getCharset();
final CodingErrorAction malformedInputAction = cconfig.getMalformedInputAction() != null ?
cconfig.getMalformedInputAction() : CodingErrorAction.REPORT;
final CodingErrorAction unmappableInputAction = cconfig.getUnmappableInputAction() != null ?
cconfig.getUnmappableInputAction() : CodingErrorAction.REPORT;
if (charset != null) {
chardecoder = charset.newDecoder();
chardecoder.onMalformedInput(malformedInputAction);
chardecoder.onUnmappableCharacter(unmappableInputAction);
charencoder = charset.newEncoder();
charencoder.onMalformedInput(malformedInputAction);
charencoder.onUnmappableCharacter(unmappableInputAction);
}
final String id = String.format("nextop-http-%d", connectionCounter.getAndIncrement());
return new NextopHttpClientConnection(
id,
cconfig.getBufferSize(),
cconfig.getFragmentSizeHint(),
chardecoder,
charencoder,
cconfig.getMessageConstraints(),
incomingContentStrategy,
outgoingContentStrategy,
requestWriterFactory,
responseParserFactory);
}
}
// be able to reset progress
// be able to attach callback that gets called after A bytes of upload, B bytes of download indiviudally
static final class NextopHttpClientConnection extends DefaultManagedHttpClientConnection {
final int yieldQBytes = DEFAULT_YIELD_Q_BYTES;
private boolean wireSet = false;
@Nullable
private Wire wire = null;
public NextopHttpClientConnection(
final String id,
final int buffersize,
final int fragmentSizeHint,
final CharsetDecoder chardecoder,
final CharsetEncoder charencoder,
final MessageConstraints constraints,
final ContentLengthStrategy incomingContentStrategy,
final ContentLengthStrategy outgoingContentStrategy,
final HttpMessageWriterFactory requestWriterFactory,
final HttpMessageParserFactory responseParserFactory) {
super(id, buffersize, fragmentSizeHint,
chardecoder, charencoder,
constraints,
incomingContentStrategy, outgoingContentStrategy,
requestWriterFactory, responseParserFactory);
}
@Nullable
private ProgressCallback getProgressCallback() {
// TODO passing this via the thread is nasty, but is there a good way for the ExecChain to inject into this
// TODO (through the pool adapter)
RequestLooper t = (RequestLooper) Thread.currentThread();
return t.progressCallback;
}
@Nullable
private Wire.Adapter getAdapter() {
// TODO (see notes in #getProgressCallback)
RequestLooper t = (RequestLooper) Thread.currentThread();
return t.wireAdapter;
}
/** sets {@link #wire} from the socket input/output,
* if there is an adapter (which is most commonly used to condition the wire).
* After this call, {@link #wire} may be null. */
private void setWire(Socket socket) throws IOException {
if (!wireSet) {
wireSet = true;
@Nullable Wire.Adapter adapter = getAdapter();
if (null != adapter) {
InputStream is = super.getSocketInputStream(socket);
OutputStream os = super.getSocketOutputStream(socket);
try {
wire = adapter.adapt(Wires.io(is, os));
} catch (InterruptedException e) {
throw new IOException(e);
}
}
}
}
@Override
protected InputStream getSocketInputStream(Socket socket) throws IOException {
setWire(socket);
if (null != wire) {
return Wires.inputStream(wire);
} else {
return super.getSocketInputStream(socket);
}
}
@Override
protected OutputStream getSocketOutputStream(Socket socket) throws IOException {
setWire(socket);
if (null != wire) {
return Wires.outputStream(wire);
} else {
return super.getSocketOutputStream(socket);
}
}
// FIXME if TCP error on close, throw SendIOException
// FIXME this means all packets sent up to the tcp window size,
// FIXME but failed to ack the end
// FIXME otherwise, up to the end of the entity was not sent, so the server knows it has a hanging request
@Override
protected OutputStream createOutputStream(final long len, SessionOutputBuffer outbuffer) {
@Nullable final ProgressCallback progressCallback = getProgressCallback();
final long sendTotalBytes = 0 < len ? len : 0;
final OutputStream os = super.createOutputStream(len, outbuffer);
return new OutputStream() {
long sentBytes = 0L;
long lastNotificationIndex = -1L;
/** scales the total in the case the actual transfer is exceeding the total (bug in the size calc) */
private long scaledSendTotalBytes(long b) {
long t = sendTotalBytes;
while (0 < t && t <= b) {
long u = 161 * t / 100;
if (t < u) {
t = u;
} else {
t *= 2;
}
}
return t;
}
private void onSendProgress(long bytes) {
sentBytes += bytes;
if (null != progressCallback) {
long notificationIndex = sentBytes / yieldQBytes;
if (lastNotificationIndex != notificationIndex) {
lastNotificationIndex = notificationIndex;
progressCallback.onSendProgress(sentBytes, scaledSendTotalBytes(sentBytes));
}
}
}
private void onSendCompleted() {
if (null != progressCallback) {
progressCallback.onSendCompleted(sentBytes, sentBytes);
}
}
@Override
public void write(int b) throws IOException {
os.write(b);
onSendProgress(1);
}
@Override
public void write(byte[] b) throws IOException {
write(b, 0, b.length);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
for (int i = 0; i < len; i += yieldQBytes) {
int c = Math.min(yieldQBytes, len - i);
os.write(b, off + i, c);
onSendProgress(c);
// FIXME
// try {
// Thread.sleep(200);
// } catch (InterruptedException e) {
// // ignore
// }
}
}
@Override
public void flush() throws IOException {
os.flush();
}
@Override
public void close() throws IOException {
os.close();
onSendCompleted();
}
};
}
@Override
public void sendRequestEntity(HttpEntityEnclosingRequest request) throws HttpException, IOException {
super.sendRequestEntity(request);
}
@Override
protected InputStream createInputStream(final long len, SessionInputBuffer inbuffer) {
@Nullable final ProgressCallback progressCallback = getProgressCallback();
final long receiveTotalBytes = 0 < len ? len : 0;
final InputStream is = super.createInputStream(len, inbuffer);
return new InputStream() {
long receivedBytes = 0L;
long lastNotificationIndex = -1L;
/** scales the total in the case the actual transfer is exceeding the total (bug in the size calc) */
private long scaledReceiveTotalBytes(long b) {
long t = receiveTotalBytes;
while (0 < t && t <= b) {
long u = 161 * t / 100;
if (t < u) {
t = u;
} else {
t *= 2;
}
}
return t;
}
private void onReceiveProgress(long bytes) {
receivedBytes += bytes;
if (null != progressCallback) {
long notificationIndex = receivedBytes / yieldQBytes;
if (lastNotificationIndex != notificationIndex) {
lastNotificationIndex = notificationIndex;
progressCallback.onReceiveProgress(receivedBytes, scaledReceiveTotalBytes(receivedBytes));
}
}
}
private void onReceiveCompleted() {
if (null != progressCallback) {
progressCallback.onReceiveCompleted(receivedBytes, receivedBytes);
}
}
@Override
public int read() throws IOException {
int b = is.read();
onReceiveProgress(1);
return b;
}
@Override
public int read(byte[] b) throws IOException {
return read(b, 0, b.length);
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
for (int i = 0; i < len; i += yieldQBytes) {
int c = Math.min(yieldQBytes, len - i);
int r = is.read(b, off + i, c);
if (0 < r) {
onReceiveProgress(r);
}
if (r < c) {
return i + r;
}
}
return len;
}
@Override
public void close() throws IOException {
super.close();
onReceiveCompleted();
}
@Override
public long skip(long n) throws IOException {
return is.skip(n);
}
@Override
public int available() throws IOException {
return is.available();
}
@Override
public boolean markSupported() {
return is.markSupported();
}
@Override
public void mark(int readlimit) {
is.mark(readlimit);
}
@Override
public void reset() throws IOException {
is.reset();
}
};
}
// OVERRIDE sendRequestEntity
// throw a SendIO
}
// not to be shared. one per exec chain/request
static class NextopHttpRequestExecutor extends HttpRequestExecutor {
ProgressCallback progressCallback;
int sendTryCount = 0;
int receiveTryCount = 0;
NextopHttpRequestExecutor(ProgressCallback progressCallback) {
this.progressCallback = progressCallback;
}
@Override
protected HttpResponse doSendRequest(
final HttpRequest request,
final HttpClientConnection conn,
final HttpContext context) throws IOException, HttpException {
++sendTryCount;
if (null != progressCallback) {
progressCallback.onSendStarted(sendTryCount);
}
return super.doSendRequest(request, conn, context);
}
@Override
protected HttpResponse doReceiveResponse(
final HttpRequest request,
final HttpClientConnection conn,
final HttpContext context) throws HttpException, IOException {
++receiveTryCount;
if (null != progressCallback) {
progressCallback.onReceiveStarted(receiveTryCount);
}
return super.doReceiveResponse(request, conn, context);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy