Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com Patches, unit tests, benchmarks, whitepaper, and outreach briefs. Public domain — no copyright claimed. Use freely.
749 lines
29 KiB
Java
749 lines
29 KiB
Java
/*
|
|
* Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved.
|
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
|
*
|
|
* This code is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License version 2 only, as
|
|
* published by the Free Software Foundation. Oracle designates this
|
|
* particular file as subject to the "Classpath" exception as provided
|
|
* by Oracle in the LICENSE file that accompanied this code.
|
|
*
|
|
* This code is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
* version 2 for more details (a copy is included in the LICENSE file that
|
|
* accompanied this code).
|
|
*
|
|
* You should have received a copy of the GNU General Public License version
|
|
* 2 along with this work; if not, write to the Free Software Foundation,
|
|
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*
|
|
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
|
* or visit www.oracle.com if you need additional information or have any
|
|
* questions.
|
|
*/
|
|
|
|
package jdk.internal.net.http;
|
|
|
|
import java.io.EOFException;
|
|
import java.io.IOException;
|
|
import java.nio.ByteBuffer;
|
|
import java.util.Arrays;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Set;
|
|
import java.util.concurrent.ConcurrentLinkedDeque;
|
|
import java.util.concurrent.Executor;
|
|
import java.util.concurrent.ExecutorService;
|
|
import java.util.concurrent.Flow;
|
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
import java.util.concurrent.atomic.AtomicLong;
|
|
import java.util.concurrent.atomic.AtomicReference;
|
|
import java.util.function.Consumer;
|
|
import jdk.internal.net.http.common.Demand;
|
|
import jdk.internal.net.http.common.FlowTube.TubeSubscriber;
|
|
import jdk.internal.net.http.common.Log;
|
|
import jdk.internal.net.http.common.Logger;
|
|
import jdk.internal.net.http.common.MinimalFuture;
|
|
import jdk.internal.net.http.common.SequentialScheduler;
|
|
import jdk.internal.net.http.common.ConnectionExpiredException;
|
|
import jdk.internal.net.http.common.Utils;
|
|
|
|
|
|
/**
|
|
* A helper class that will queue up incoming data until the receiving
|
|
* side is ready to handle it.
|
|
*/
|
|
class Http1AsyncReceiver {
|
|
|
|
final Logger debug = Utils.getDebugLogger(this::dbgString, Utils.DEBUG);
|
|
|
|
/**
|
|
* A delegate that can asynchronously receive data from an upstream flow,
|
|
* parse, it, then possibly transform it and either store it (response
|
|
* headers) or possibly pass it to a downstream subscriber (response body).
|
|
* Usually, there will be one Http1AsyncDelegate in charge of receiving
|
|
* and parsing headers, and another one in charge of receiving, parsing,
|
|
* and forwarding body. Each will sequentially subscribe with the
|
|
* Http1AsyncReceiver in turn. There may be additional delegates which
|
|
* subscribe to the Http1AsyncReceiver, mainly for the purpose of handling
|
|
* errors while the connection is busy transmitting the request body and the
|
|
* Http1Exchange::readBody method hasn't been called yet, and response
|
|
* delegates haven't subscribed yet.
|
|
*/
|
|
static interface Http1AsyncDelegate {
|
|
/**
|
|
* Receives and handles a byte buffer reference.
|
|
* @param ref A byte buffer reference coming from upstream.
|
|
* @return false, if the byte buffer reference should be kept in the queue.
|
|
* Usually, this means that either the byte buffer reference
|
|
* was handled and parsing is finished, or that the receiver
|
|
* didn't handle the byte reference at all.
|
|
* There may or may not be any remaining data in the
|
|
* byte buffer, and the byte buffer reference must not have
|
|
* been cleared.
|
|
* true, if the byte buffer reference was fully read and
|
|
* more data can be received.
|
|
*/
|
|
public boolean tryAsyncReceive(ByteBuffer ref);
|
|
|
|
/**
|
|
* Called when an exception is raised.
|
|
* @param ex The raised Throwable.
|
|
*/
|
|
public void onReadError(Throwable ex);
|
|
|
|
/**
|
|
* Must be called before any other method on the delegate.
|
|
* The subscription can be either used directly by the delegate
|
|
* to request more data (e.g. if the delegate is a header parser),
|
|
* or can be forwarded to a downstream subscriber (if the delegate
|
|
* is a body parser that wraps a response BodySubscriber).
|
|
* In all cases, it is the responsibility of the delegate to ensure
|
|
* that request(n) and demand.tryDecrement() are called appropriately.
|
|
* No data will be sent to {@code tryAsyncReceive} unless
|
|
* the subscription has some demand.
|
|
*
|
|
* @param s A subscription that allows the delegate to control the
|
|
* data flow.
|
|
*/
|
|
public void onSubscribe(AbstractSubscription s);
|
|
|
|
/**
|
|
* Returns the subscription that was passed to {@code onSubscribe}
|
|
* @return the subscription that was passed to {@code onSubscribe}..
|
|
*/
|
|
public AbstractSubscription subscription();
|
|
|
|
/**
|
|
* Called to make sure resources are released
|
|
* when the Http1AsyncReceiver is stopped.
|
|
* @param error The Http1AsyncReceiver pending error ref,
|
|
* if any.
|
|
*/
|
|
public void close(Throwable error);
|
|
|
|
}
|
|
|
|
/**
|
|
* A simple subclass of AbstractSubscription that ensures the
|
|
* SequentialScheduler will be run when request() is called and demand
|
|
* becomes positive again.
|
|
*/
|
|
private static final class Http1AsyncDelegateSubscription
|
|
extends AbstractSubscription
|
|
{
|
|
private final Runnable onCancel;
|
|
private final Consumer<Throwable> onError;
|
|
private final SequentialScheduler scheduler;
|
|
private volatile boolean cancelled;
|
|
Http1AsyncDelegateSubscription(SequentialScheduler scheduler,
|
|
Runnable onCancel,
|
|
Consumer<Throwable> onError) {
|
|
this.scheduler = scheduler;
|
|
this.onCancel = onCancel;
|
|
this.onError = onError;
|
|
}
|
|
@Override
|
|
public void request(long n) {
|
|
if (cancelled) return;
|
|
try {
|
|
final Demand demand = demand();
|
|
if (demand.increase(n)) {
|
|
scheduler.runOrSchedule();
|
|
}
|
|
} catch (IllegalArgumentException x) {
|
|
cancelled = true;
|
|
onError.accept(x);
|
|
}
|
|
}
|
|
@Override
|
|
public void cancel() {
|
|
cancelled = true;
|
|
onCancel.run();
|
|
}
|
|
}
|
|
|
|
private final ConcurrentLinkedDeque<ByteBuffer> queue
|
|
= new ConcurrentLinkedDeque<>();
|
|
private final SequentialScheduler scheduler =
|
|
SequentialScheduler.lockingScheduler(this::flush);
|
|
final MinimalFuture<Void> whenFinished;
|
|
private final Executor executor;
|
|
private final Http1TubeSubscriber subscriber = new Http1TubeSubscriber();
|
|
private final AtomicReference<Http1AsyncDelegate> pendingDelegateRef;
|
|
private final AtomicLong received = new AtomicLong();
|
|
final AtomicBoolean canRequestMore = new AtomicBoolean();
|
|
|
|
private volatile Throwable error;
|
|
private volatile Http1AsyncDelegate delegate;
|
|
// This reference is only used to prevent early GC of the exchange.
|
|
private volatile Http1Exchange<?> owner;
|
|
// Only used for checking whether we run on the selector manager thread.
|
|
private final HttpClientImpl client;
|
|
private boolean retry;
|
|
private volatile boolean stopRequested;
|
|
|
|
public Http1AsyncReceiver(Executor executor, Http1Exchange<?> owner) {
|
|
this.pendingDelegateRef = new AtomicReference<>();
|
|
this.executor = executor;
|
|
this.whenFinished = new MinimalFuture<>();
|
|
this.owner = owner;
|
|
this.client = owner.client;
|
|
}
|
|
|
|
// This is the main loop called by the SequentialScheduler.
|
|
// It attempts to empty the queue until the scheduler is stopped,
|
|
// or the delegate is unregistered, or the delegate is unable to
|
|
// process the data (because it's not ready or already done), which
|
|
// it signals by returning 'true';
|
|
private void flush() {
|
|
ByteBuffer buf;
|
|
try {
|
|
// we should not be running in the selector here,
|
|
// except if the custom Executor supplied to the client is
|
|
// something like (r) -> r.run();
|
|
assert !client.isSelectorThread()
|
|
|| !(client.theExecutor().delegate() instanceof ExecutorService) :
|
|
"Http1AsyncReceiver::flush should not run in the selector: "
|
|
+ Thread.currentThread().getName();
|
|
|
|
// First check whether we have a pending delegate that has
|
|
// just subscribed, and if so, create a Subscription for it
|
|
// and call onSubscribe.
|
|
handlePendingDelegate();
|
|
|
|
// Then start emptying the queue, if possible.
|
|
while ((buf = queue.peek()) != null && !stopRequested) {
|
|
Http1AsyncDelegate delegate = this.delegate;
|
|
if (debug.on())
|
|
debug.log("Got %s bytes for delegate %s",
|
|
buf.remaining(), delegate);
|
|
if (!hasDemand(delegate)) {
|
|
// The scheduler will be invoked again later when the demand
|
|
// becomes positive.
|
|
return;
|
|
}
|
|
|
|
assert delegate != null;
|
|
if (debug.on())
|
|
debug.log("Forwarding %s bytes to delegate %s",
|
|
buf.remaining(), delegate);
|
|
// The delegate has demand: feed it the next buffer.
|
|
if (!delegate.tryAsyncReceive(buf)) {
|
|
final long remaining = buf.remaining();
|
|
if (debug.on()) debug.log(() -> {
|
|
// If the scheduler is stopped, the queue may already
|
|
// be empty and the reference may already be released.
|
|
String remstr = scheduler.isStopped() ? "" :
|
|
" remaining in ref: "
|
|
+ remaining;
|
|
remstr += remstr
|
|
+ " total remaining: " + remaining();
|
|
return "Delegate done: " + remaining;
|
|
});
|
|
canRequestMore.set(false);
|
|
// The last buffer parsed may have remaining unparsed bytes.
|
|
// Don't take it out of the queue.
|
|
return; // done.
|
|
}
|
|
|
|
// removed parsed buffer from queue, and continue with next
|
|
// if available
|
|
ByteBuffer parsed = queue.remove();
|
|
canRequestMore.set(queue.isEmpty() && !stopRequested);
|
|
assert parsed == buf;
|
|
}
|
|
|
|
// queue is empty: let's see if we should request more
|
|
checkRequestMore();
|
|
|
|
} catch (Throwable t) {
|
|
synchronized (this) {
|
|
Throwable x = error;
|
|
if (x == null) error = t; // will be handled in the finally block
|
|
}
|
|
if (debug.on()) debug.log("Unexpected error caught in flush()", t);
|
|
} finally {
|
|
// Handles any pending error.
|
|
// The most recently subscribed delegate will get the error.
|
|
checkForErrors();
|
|
}
|
|
}
|
|
|
|
private String describe() {
|
|
Http1Exchange<?> exchange = owner;
|
|
if (exchange != null) {
|
|
return String.valueOf(exchange.request());
|
|
}
|
|
return "<uri unavailable>";
|
|
}
|
|
|
|
/**
|
|
* Must be called from within the scheduler main loop.
|
|
* Handles any pending errors by calling delegate.onReadError().
|
|
* If the error can be forwarded to the delegate, stops the scheduler.
|
|
*/
|
|
private void checkForErrors() {
|
|
// Handles any pending error.
|
|
// The most recently subscribed delegate will get the error.
|
|
// If the delegate is null, the error will be handled by the next
|
|
// delegate that subscribes.
|
|
// If the queue is not empty, wait until it is empty before
|
|
// handling the error.
|
|
Http1AsyncDelegate delegate = pendingDelegateRef.get();
|
|
if (delegate == null) delegate = this.delegate;
|
|
Throwable x = error;
|
|
if (delegate != null && x != null && (stopRequested || queue.isEmpty())) {
|
|
// forward error only after emptying the queue.
|
|
final Object captured = delegate;
|
|
if (debug.on())
|
|
debug.log(() -> "flushing " + x + "\n\t delegate: " + captured
|
|
+ "\t\t queue.isEmpty: " + queue.isEmpty());
|
|
scheduler.stop();
|
|
delegate.onReadError(x);
|
|
whenFinished.completeExceptionally(x);
|
|
if (Log.channel()) {
|
|
Log.logChannel("HTTP/1 read subscriber stopped for: {0}", describe());
|
|
}
|
|
if (stopRequested) {
|
|
// This is the special case where the subscriber
|
|
// has requested an illegal number of items.
|
|
// In this case, the error doesn't come from
|
|
// upstream, but from downstream, and we need to
|
|
// close the upstream connection.
|
|
Http1Exchange<?> exchg = owner;
|
|
stop();
|
|
if (exchg != null) exchg.connection().close(x);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Must be called from within the scheduler main loop.
|
|
* Figure out whether more data should be requested from the
|
|
* Http1TubeSubscriber.
|
|
*/
|
|
private void checkRequestMore() {
|
|
Http1AsyncDelegate delegate = this.delegate;
|
|
boolean more = this.canRequestMore.get();
|
|
boolean hasDemand = hasDemand(delegate);
|
|
if (debug.on())
|
|
debug.log("checkRequestMore: " + "canRequestMore=" + more
|
|
+ ", hasDemand=" + hasDemand
|
|
+ (delegate == null ? ", delegate=null" : ""));
|
|
if (hasDemand) {
|
|
subscriber.requestMore();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Must be called from within the scheduler main loop.
|
|
* Return true if the delegate is not null and has some demand.
|
|
* @param delegate The Http1AsyncDelegate delegate
|
|
* @return true if the delegate is not null and has some demand
|
|
*/
|
|
private boolean hasDemand(Http1AsyncDelegate delegate) {
|
|
if (delegate == null) return false;
|
|
AbstractSubscription subscription = delegate.subscription();
|
|
long demand = subscription.demand().get();
|
|
if (debug.on())
|
|
debug.log("downstream subscription demand is %s for %s", demand, delegate);
|
|
return demand > 0;
|
|
}
|
|
|
|
/**
|
|
* Must be called from within the scheduler main loop.
|
|
* Handles pending delegate subscription.
|
|
* Return true if there was some pending delegate subscription and a new
|
|
* delegate was subscribed, false otherwise.
|
|
*
|
|
* @return true if there was some pending delegate subscription and a new
|
|
* delegate was subscribed, false otherwise.
|
|
*/
|
|
private boolean handlePendingDelegate() {
|
|
Http1AsyncDelegate pending = pendingDelegateRef.get();
|
|
if (pending != null && pendingDelegateRef.compareAndSet(pending, null)) {
|
|
Http1AsyncDelegate delegate = this.delegate;
|
|
if (delegate != null) unsubscribe(delegate);
|
|
Consumer<Throwable> onSubscriptionError = (x) -> {
|
|
setRetryOnError(false);
|
|
stopRequested = true;
|
|
onReadError(x);
|
|
};
|
|
Runnable cancel = () -> {
|
|
if (debug.on())
|
|
debug.log("Downstream subscription cancelled by %s", pending);
|
|
// The connection should be closed, as some data may
|
|
// be left over in the stream.
|
|
try {
|
|
setRetryOnError(false);
|
|
pending.close(null);
|
|
onReadError(new IOException("subscription cancelled"));
|
|
unsubscribe(pending);
|
|
} finally {
|
|
Http1Exchange<?> exchg = owner;
|
|
stop();
|
|
if (exchg != null) exchg.connection().close();
|
|
}
|
|
};
|
|
// The subscription created by a delegate is only loosely
|
|
// coupled with the upstream subscription. This is partly because
|
|
// the header/body parser work with a flow of ByteBuffer, whereas
|
|
// we have a flow List<ByteBuffer> upstream.
|
|
Http1AsyncDelegateSubscription subscription =
|
|
new Http1AsyncDelegateSubscription(scheduler, cancel, onSubscriptionError);
|
|
try {
|
|
pending.onSubscribe(subscription);
|
|
} finally {
|
|
this.delegate = delegate = pending;
|
|
}
|
|
final Object captured = delegate;
|
|
if (debug.on())
|
|
debug.log("delegate is now " + captured
|
|
+ ", demand=" + subscription.demand().get()
|
|
+ ", canRequestMore=" + canRequestMore.get()
|
|
+ ", queue.isEmpty=" + queue.isEmpty());
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
synchronized void setRetryOnError(boolean retry) {
|
|
this.retry = retry;
|
|
}
|
|
|
|
void clear() {
|
|
if (debug.on()) debug.log("cleared");
|
|
this.pendingDelegateRef.set(null);
|
|
this.delegate = null;
|
|
this.owner = null;
|
|
}
|
|
|
|
void subscribe(Http1AsyncDelegate delegate) {
|
|
synchronized (this) {
|
|
pendingDelegateRef.set(delegate);
|
|
}
|
|
if (queue.isEmpty()) {
|
|
canRequestMore.set(true);
|
|
}
|
|
if (debug.on())
|
|
debug.log("Subscribed pending " + delegate + " queue.isEmpty: "
|
|
+ queue.isEmpty());
|
|
// Everything may have been received already. Make sure
|
|
// we parse it.
|
|
if (client.isSelectorThread()) {
|
|
scheduler.runOrSchedule(executor);
|
|
} else {
|
|
scheduler.runOrSchedule();
|
|
}
|
|
}
|
|
|
|
// Used for debugging only!
|
|
long remaining() {
|
|
return Utils.remaining(queue.toArray(Utils.EMPTY_BB_ARRAY));
|
|
}
|
|
|
|
void unsubscribe(Http1AsyncDelegate delegate) {
|
|
boolean unsubscribed = false;
|
|
synchronized (this) {
|
|
if (this.delegate == delegate) {
|
|
this.delegate = null;
|
|
unsubscribed = true;
|
|
}
|
|
}
|
|
if (unsubscribed) {
|
|
if (debug.on()) debug.log("Unsubscribed %s", delegate);
|
|
}
|
|
}
|
|
|
|
// Callback: Consumer of ByteBuffer
|
|
private void asyncReceive(ByteBuffer buf) {
|
|
if (debug.on())
|
|
debug.log("Putting %s bytes into the queue", buf.remaining());
|
|
received.addAndGet(buf.remaining());
|
|
queue.offer(buf);
|
|
|
|
// This callback is called from within the selector thread.
|
|
// Use an executor here to avoid doing the heavy lifting in the
|
|
// selector.
|
|
scheduler.runOrSchedule(executor);
|
|
}
|
|
|
|
// Callback: Consumer of Throwable
|
|
void onReadError(Throwable ex) {
|
|
Http1AsyncDelegate delegate;
|
|
Throwable recorded;
|
|
if (debug.on()) debug.log("onError: %s", (Object) ex);
|
|
synchronized (this) {
|
|
delegate = this.delegate;
|
|
recorded = error;
|
|
if (recorded == null) {
|
|
// retry is set to true by HttpExchange when the connection is
|
|
// already connected, which means it's been retrieved from
|
|
// the pool.
|
|
if (retry && (ex instanceof IOException)) {
|
|
// could be either EOFException, or
|
|
// IOException("connection reset by peer"), or
|
|
// SSLHandshakeException resulting from the server having
|
|
// closed the SSL session.
|
|
if (received.get() == 0) {
|
|
// If we receive such an exception before having
|
|
// received any byte, then in this case, we will
|
|
// throw ConnectionExpiredException
|
|
// to try & force a retry of the request.
|
|
retry = false;
|
|
ex = new ConnectionExpiredException(ex);
|
|
}
|
|
}
|
|
error = ex;
|
|
}
|
|
}
|
|
|
|
final Throwable t = (recorded == null ? ex : recorded);
|
|
if (debug.on())
|
|
debug.log("recorded " + t + "\n\t delegate: " + delegate
|
|
+ "\n\t queue.isEmpty: " + queue.isEmpty()
|
|
+ "\n\tstopRequested: " + stopRequested, ex);
|
|
if (Log.errors()) {
|
|
Log.logError("HTTP/1 read subscriber recorded error: {0} - {1}", describe(), t);
|
|
}
|
|
if (queue.isEmpty() || pendingDelegateRef.get() != null || stopRequested) {
|
|
// This callback is called from within the selector thread.
|
|
// Use an executor here to avoid doing the heavy lifting in the
|
|
// selector.
|
|
if (Log.errors()) {
|
|
Log.logError("HTTP/1 propagating recorded error: {0} - {1}", describe(), t);
|
|
}
|
|
scheduler.runOrSchedule(executor);
|
|
}
|
|
}
|
|
|
|
void stop() {
|
|
if (debug.on()) debug.log("stopping");
|
|
if (Log.channel() && !scheduler.isStopped()) {
|
|
Log.logChannel("HTTP/1 read subscriber stopped for {0}", describe());
|
|
}
|
|
scheduler.stop();
|
|
// make sure ref count is handled properly by
|
|
// closing the delegate.
|
|
Http1AsyncDelegate previous = delegate;
|
|
if (previous != null) previous.close(error);
|
|
delegate = null;
|
|
owner = null;
|
|
whenFinished.complete(null);
|
|
}
|
|
|
|
/**
|
|
* Returns the TubeSubscriber for reading from the connection flow.
|
|
* @return the TubeSubscriber for reading from the connection flow.
|
|
*/
|
|
TubeSubscriber subscriber() {
|
|
return subscriber;
|
|
}
|
|
|
|
/**
|
|
* A simple tube subscriber for reading from the connection flow.
|
|
*/
|
|
final class Http1TubeSubscriber implements TubeSubscriber {
|
|
volatile Flow.Subscription subscription;
|
|
volatile boolean completed;
|
|
volatile boolean dropped;
|
|
|
|
public void onSubscribe(Flow.Subscription subscription) {
|
|
// supports being called multiple time.
|
|
// doesn't cancel the previous subscription, since that is
|
|
// most probably the same as the new subscription.
|
|
if (debug.on()) debug.log("Received onSubscribed from upstream");
|
|
if (Log.channel()) {
|
|
Log.logChannel("HTTP/1 read subscriber got subscription from {0}", describe());
|
|
}
|
|
assert this.subscription == null || dropped == false;
|
|
this.subscription = subscription;
|
|
dropped = false;
|
|
canRequestMore.set(true);
|
|
if (delegate != null) {
|
|
scheduler.runOrSchedule(executor);
|
|
} else {
|
|
if (debug.on()) debug.log("onSubscribe: read delegate not present yet");
|
|
}
|
|
}
|
|
|
|
void requestMore() {
|
|
Flow.Subscription s = subscription;
|
|
if (s == null) return;
|
|
if (canRequestMore.compareAndSet(true, false)) {
|
|
if (!completed && !dropped) {
|
|
if (debug.on())
|
|
debug.log("Http1TubeSubscriber: requesting one more from upstream: " + s);
|
|
s.request(1);
|
|
return;
|
|
}
|
|
}
|
|
if (debug.on())
|
|
debug.log("Http1TubeSubscriber: no need to request more");
|
|
}
|
|
|
|
@Override
|
|
public void onNext(List<ByteBuffer> item) {
|
|
canRequestMore.set(item.isEmpty());
|
|
for (ByteBuffer buffer : item) {
|
|
asyncReceive(buffer);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onError(Throwable throwable) {
|
|
onReadError(throwable);
|
|
completed = true;
|
|
}
|
|
|
|
@Override
|
|
public void onComplete() {
|
|
onReadError(new EOFException("EOF reached while reading"));
|
|
completed = true;
|
|
}
|
|
|
|
public void dropSubscription() {
|
|
if (debug.on()) debug.log("Http1TubeSubscriber: dropSubscription");
|
|
// we could probably set subscription to null here...
|
|
// then we might not need the 'dropped' boolean?
|
|
dropped = true;
|
|
}
|
|
|
|
}
|
|
|
|
// Drains the content of the queue into a single ByteBuffer.
|
|
// The scheduler must be permanently stopped before calling drain().
|
|
ByteBuffer drain(ByteBuffer initial) {
|
|
// Revisit: need to clean that up.
|
|
//
|
|
ByteBuffer b = initial = (initial == null ? Utils.EMPTY_BYTEBUFFER : initial);
|
|
assert scheduler.isStopped();
|
|
|
|
if (queue.isEmpty()) return b;
|
|
|
|
// sanity check: we shouldn't have queued the same
|
|
// buffer twice.
|
|
ByteBuffer[] qbb = queue.toArray(new ByteBuffer[queue.size()]);
|
|
|
|
// the assertion looks suspicious, more investigation needed
|
|
//
|
|
// assert java.util.stream.Stream.of(qbb)
|
|
// .collect(Collectors.toSet())
|
|
// .size() == qbb.length : debugQBB(qbb);
|
|
|
|
// compute the number of bytes in the queue, the number of bytes
|
|
// in the initial buffer
|
|
// TODO: will need revisiting - as it is not guaranteed that all
|
|
// data will fit in single BB!
|
|
int size = Utils.remaining(qbb, Integer.MAX_VALUE);
|
|
int remaining = b.remaining();
|
|
int free = b.capacity() - b.position() - remaining;
|
|
if (debug.on())
|
|
debug.log("Flushing %s bytes from queue into initial buffer "
|
|
+ "(remaining=%s, free=%s)", size, remaining, free);
|
|
|
|
// check whether the initial buffer has enough space
|
|
if (size > free) {
|
|
if (debug.on())
|
|
debug.log("Allocating new buffer for initial: %s", (size + remaining));
|
|
// allocates a new buffer and copy initial to it
|
|
b = ByteBuffer.allocate(size + remaining);
|
|
Utils.copy(initial, b);
|
|
assert b.position() == remaining;
|
|
b.flip();
|
|
assert b.position() == 0;
|
|
assert b.limit() == remaining;
|
|
assert b.remaining() == remaining;
|
|
}
|
|
|
|
// store position and limit
|
|
int pos = b.position();
|
|
int limit = b.limit();
|
|
assert limit - pos == remaining;
|
|
assert b.capacity() >= remaining + size
|
|
: "capacity: " + b.capacity()
|
|
+ ", remaining: " + b.remaining()
|
|
+ ", size: " + size;
|
|
|
|
// prepare to copy the content of the queue
|
|
b.position(limit);
|
|
b.limit(pos + remaining + size);
|
|
assert b.remaining() >= size :
|
|
"remaining: " + b.remaining() + ", size: " + size;
|
|
|
|
// copy the content of the queue
|
|
int count = 0;
|
|
for (int i=0; i<qbb.length; i++) {
|
|
ByteBuffer b2 = qbb[i];
|
|
int r = b2.remaining();
|
|
assert b.remaining() >= r : "need at least " + r + " only "
|
|
+ b.remaining() + " available";
|
|
int copied = Utils.copy(b2, b);
|
|
assert copied == r : "copied="+copied+" available="+r;
|
|
assert b2.remaining() == 0;
|
|
count += copied;
|
|
}
|
|
assert count == size;
|
|
assert b.position() == pos + remaining + size :
|
|
"b.position="+b.position()+" != "+pos+"+"+remaining+"+"+size;
|
|
|
|
// reset limit and position
|
|
b.limit(limit+size);
|
|
b.position(pos);
|
|
|
|
// we can clear the refs
|
|
queue.clear();
|
|
final ByteBuffer bb = b;
|
|
if (debug.on())
|
|
debug.log("Initial buffer now has " + bb.remaining()
|
|
+ " pos=" + bb.position() + " limit=" + bb.limit());
|
|
|
|
return b;
|
|
}
|
|
|
|
private String debugQBB(ByteBuffer[] qbb) {
|
|
StringBuilder msg = new StringBuilder();
|
|
List<ByteBuffer> lbb = Arrays.asList(qbb);
|
|
Set<ByteBuffer> sbb = new HashSet<>(Arrays.asList(qbb));
|
|
|
|
int uniquebb = sbb.size();
|
|
msg.append("qbb: ").append(lbb.size())
|
|
.append(" (unique: ").append(uniquebb).append("), ")
|
|
.append("duplicates: ");
|
|
String sep = "";
|
|
for (ByteBuffer b : lbb) {
|
|
if (!sbb.remove(b)) {
|
|
msg.append(sep)
|
|
.append(b)
|
|
.append("[remaining=")
|
|
.append(b.remaining())
|
|
.append(", position=")
|
|
.append(b.position())
|
|
.append(", capacity=")
|
|
.append(b.capacity())
|
|
.append("]");
|
|
sep = ", ";
|
|
}
|
|
}
|
|
return msg.toString();
|
|
}
|
|
|
|
volatile String dbgTag;
|
|
String dbgString() {
|
|
String tag = dbgTag;
|
|
if (tag == null) {
|
|
Http1Exchange<?> exchg = owner;
|
|
Object flow = (exchg != null)
|
|
? exchg.connection().getConnectionFlow()
|
|
: null;
|
|
if (flow != null) {
|
|
dbgTag = tag = "Http1AsyncReceiver(" + flow + ")";
|
|
} else {
|
|
tag = "Http1AsyncReceiver(?)";
|
|
}
|
|
}
|
|
return tag;
|
|
}
|
|
}
|