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.
1347 lines
57 KiB
Java
1347 lines
57 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.IOException;
|
|
import java.nio.ByteBuffer;
|
|
import java.util.List;
|
|
import java.util.Objects;
|
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
|
import java.util.concurrent.Flow;
|
|
import java.util.concurrent.atomic.AtomicReference;
|
|
import java.nio.channels.SelectableChannel;
|
|
import java.nio.channels.SelectionKey;
|
|
import java.nio.channels.SocketChannel;
|
|
import java.util.ArrayList;
|
|
import java.util.concurrent.locks.Lock;
|
|
import java.util.concurrent.locks.ReentrantLock;
|
|
import java.util.function.Consumer;
|
|
import java.util.function.Supplier;
|
|
import jdk.internal.net.http.common.BufferSupplier;
|
|
import jdk.internal.net.http.common.Demand;
|
|
import jdk.internal.net.http.common.FlowTube;
|
|
import jdk.internal.net.http.common.Log;
|
|
import jdk.internal.net.http.common.Logger;
|
|
import jdk.internal.net.http.common.SequentialScheduler;
|
|
import jdk.internal.net.http.common.SequentialScheduler.DeferredCompleter;
|
|
import jdk.internal.net.http.common.SequentialScheduler.RestartableTask;
|
|
import jdk.internal.net.http.common.Utils;
|
|
|
|
/**
|
|
* A SocketTube is a terminal tube plugged directly into the socket.
|
|
* The read subscriber should call {@code subscribe} on the SocketTube before
|
|
* the SocketTube is subscribed to the write publisher.
|
|
*/
|
|
final class SocketTube implements FlowTube {
|
|
|
|
final Logger debug = Utils.getDebugLogger(this::dbgString, Utils.DEBUG);
|
|
|
|
private final HttpClientImpl client;
|
|
private final SocketChannel channel;
|
|
private final SliceBufferSource sliceBuffersSource;
|
|
private final Object lock = new Object();
|
|
private final AtomicReference<Throwable> errorRef = new AtomicReference<>();
|
|
private final InternalReadPublisher readPublisher;
|
|
private final InternalWriteSubscriber writeSubscriber;
|
|
private final String connectionLabel;
|
|
|
|
public SocketTube(HttpClientImpl client, SocketChannel channel,
|
|
Supplier<ByteBuffer> buffersFactory,
|
|
String connectionLabel) {
|
|
this.client = client;
|
|
this.channel = channel;
|
|
this.sliceBuffersSource = new SliceBufferSource(buffersFactory);
|
|
this.connectionLabel = connectionLabel;
|
|
this.readPublisher = new InternalReadPublisher();
|
|
this.writeSubscriber = new InternalWriteSubscriber();
|
|
}
|
|
|
|
/**
|
|
* Returns {@code true} if this flow is finished.
|
|
* This happens when this flow internal read subscription is completed,
|
|
* either normally (EOF reading) or exceptionally (EOF writing, or
|
|
* underlying socket closed, or some exception occurred while reading or
|
|
* writing to the socket).
|
|
*
|
|
* @return {@code true} if this flow is finished.
|
|
*/
|
|
public boolean isFinished() {
|
|
InternalReadPublisher.InternalReadSubscription subscription =
|
|
readPublisher.subscriptionImpl;
|
|
return subscription != null && subscription.completed
|
|
|| subscription == null && errorRef.get() != null;
|
|
}
|
|
|
|
// ===================================================================== //
|
|
// Flow.Publisher //
|
|
// ======================================================================//
|
|
|
|
/**
|
|
* {@inheritDoc }
|
|
* @apiNote This method should be called first. In particular, the caller
|
|
* must ensure that this method must be called by the read
|
|
* subscriber before the write publisher can call {@code onSubscribe}.
|
|
* Failure to adhere to this contract may result in assertion errors.
|
|
*/
|
|
@Override
|
|
public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> s) {
|
|
Objects.requireNonNull(s);
|
|
assert s instanceof TubeSubscriber : "Expected TubeSubscriber, got:" + s;
|
|
readPublisher.subscribe(s);
|
|
}
|
|
|
|
|
|
// ===================================================================== //
|
|
// Flow.Subscriber //
|
|
// ======================================================================//
|
|
|
|
/**
|
|
* {@inheritDoc }
|
|
* @apiNote The caller must ensure that {@code subscribe} is called by
|
|
* the read subscriber before {@code onSubscribe} is called by
|
|
* the write publisher.
|
|
* Failure to adhere to this contract may result in assertion errors.
|
|
*/
|
|
@Override
|
|
public void onSubscribe(Flow.Subscription subscription) {
|
|
writeSubscriber.onSubscribe(subscription);
|
|
}
|
|
|
|
@Override
|
|
public void onNext(List<ByteBuffer> item) {
|
|
writeSubscriber.onNext(item);
|
|
}
|
|
|
|
@Override
|
|
public void onError(Throwable throwable) {
|
|
writeSubscriber.onError(throwable);
|
|
}
|
|
|
|
@Override
|
|
public void onComplete() {
|
|
writeSubscriber.onComplete();
|
|
}
|
|
|
|
// ===================================================================== //
|
|
// Events //
|
|
// ======================================================================//
|
|
|
|
void signalClosed(Throwable cause) {
|
|
// Ensures that the subscriber will be terminated and that future
|
|
// subscribers will be notified when the connection is closed.
|
|
if (Log.channel()) {
|
|
Log.logChannel("Connection close signalled: connection closed locally ({0})",
|
|
channelDescr());
|
|
}
|
|
readPublisher.subscriptionImpl.signalError(
|
|
new IOException("connection closed locally", cause));
|
|
}
|
|
|
|
/**
|
|
* A restartable task used to process tasks in sequence.
|
|
*/
|
|
private static class SocketFlowTask implements RestartableTask {
|
|
final Runnable task;
|
|
private final Lock lock = new ReentrantLock();
|
|
SocketFlowTask(Runnable task) {
|
|
this.task = task;
|
|
}
|
|
@Override
|
|
public final void run(DeferredCompleter taskCompleter) {
|
|
try {
|
|
// The logics of the sequential scheduler should ensure that
|
|
// the restartable task is running in only one thread at
|
|
// a given time: there should never be contention.
|
|
boolean locked = lock.tryLock();
|
|
assert locked : "contention detected in SequentialScheduler";
|
|
try {
|
|
task.run();
|
|
} finally {
|
|
if (locked) lock.unlock();
|
|
}
|
|
} finally {
|
|
taskCompleter.complete();
|
|
}
|
|
}
|
|
}
|
|
|
|
// This is best effort - there's no guarantee that the printed set of values
|
|
// is consistent. It should only be considered as weakly accurate - in
|
|
// particular in what concerns the events states, especially when displaying
|
|
// a read event state from a write event callback and conversely.
|
|
void debugState(String when) {
|
|
if (debug.on()) {
|
|
StringBuilder state = new StringBuilder();
|
|
|
|
InternalReadPublisher.InternalReadSubscription sub =
|
|
readPublisher.subscriptionImpl;
|
|
InternalReadPublisher.ReadEvent readEvent =
|
|
sub == null ? null : sub.readEvent;
|
|
Demand rdemand = sub == null ? null : sub.demand;
|
|
InternalWriteSubscriber.WriteEvent writeEvent =
|
|
writeSubscriber.writeEvent;
|
|
Demand wdemand = writeSubscriber.writeDemand;
|
|
int rops = readEvent == null ? 0 : readEvent.interestOps();
|
|
long rd = rdemand == null ? 0 : rdemand.get();
|
|
int wops = writeEvent == null ? 0 : writeEvent.interestOps();
|
|
long wd = wdemand == null ? 0 : wdemand.get();
|
|
|
|
state.append(when).append(" Reading: [ops=")
|
|
.append(Utils.describeOps(rops)).append(", demand=").append(rd)
|
|
.append(", stopped=")
|
|
.append((sub == null ? false : sub.readScheduler.isStopped()))
|
|
.append("], Writing: [ops=").append(Utils.describeOps(wops))
|
|
.append(", demand=").append(wd)
|
|
.append("]");
|
|
debug.log(state.toString());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A repeatable event that can be paused or resumed by changing its
|
|
* interestOps. When the event is fired, it is first paused before being
|
|
* signaled. It is the responsibility of the code triggered by
|
|
* {@code signalEvent} to resume the event if required.
|
|
*/
|
|
private abstract static class SocketFlowEvent extends AsyncEvent {
|
|
final SocketChannel channel;
|
|
final int defaultInterest;
|
|
volatile int interestOps;
|
|
volatile boolean registered;
|
|
SocketFlowEvent(int defaultInterest, SocketChannel channel) {
|
|
super(AsyncEvent.REPEATING);
|
|
this.defaultInterest = defaultInterest;
|
|
this.channel = channel;
|
|
}
|
|
final boolean registered() {return registered;}
|
|
final void resume() {
|
|
interestOps = defaultInterest;
|
|
registered = true;
|
|
}
|
|
final void pause() {interestOps = 0;}
|
|
@Override
|
|
public final SelectableChannel channel() {return channel;}
|
|
@Override
|
|
public final int interestOps() {return interestOps;}
|
|
|
|
@Override
|
|
public final void handle() {
|
|
pause(); // pause, then signal
|
|
signalEvent(); // won't be fired again until resumed.
|
|
}
|
|
@Override
|
|
public final void abort(IOException error) {
|
|
debug().log(() -> this.getClass().getSimpleName() + " abort: " + error);
|
|
pause(); // pause, then signal
|
|
signalError(error); // should not be resumed after abort (not checked)
|
|
}
|
|
|
|
protected abstract void signalEvent();
|
|
protected abstract void signalError(Throwable error);
|
|
abstract Logger debug();
|
|
}
|
|
|
|
// ===================================================================== //
|
|
// Writing //
|
|
// ======================================================================//
|
|
|
|
// This class makes the assumption that the publisher will call onNext
|
|
// sequentially, and that onNext won't be called if the demand has not been
|
|
// incremented by request(1).
|
|
// It has a 'queue of 1' meaning that it will call request(1) in
|
|
// onSubscribe, and then only after its 'current' buffer list has been
|
|
// fully written and current set to null;
|
|
private final class InternalWriteSubscriber
|
|
implements Flow.Subscriber<List<ByteBuffer>> {
|
|
|
|
volatile WriteSubscription subscription;
|
|
volatile List<ByteBuffer> current;
|
|
volatile boolean completed;
|
|
final AsyncTriggerEvent startSubscription =
|
|
new AsyncTriggerEvent(this::signalError, this::startSubscription);
|
|
final WriteEvent writeEvent = new WriteEvent(channel, this);
|
|
final Demand writeDemand = new Demand();
|
|
|
|
@Override
|
|
public void onSubscribe(Flow.Subscription subscription) {
|
|
WriteSubscription previous = this.subscription;
|
|
if (debug.on()) debug.log("subscribed for writing");
|
|
try {
|
|
boolean needEvent = current == null;
|
|
if (needEvent) {
|
|
if (previous != null && previous.upstreamSubscription != subscription) {
|
|
previous.dropSubscription();
|
|
}
|
|
}
|
|
this.subscription = new WriteSubscription(subscription);
|
|
if (needEvent) {
|
|
if (debug.on())
|
|
debug.log("write: registering startSubscription event");
|
|
client.registerEvent(startSubscription);
|
|
}
|
|
} catch (Throwable t) {
|
|
signalError(t);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onNext(List<ByteBuffer> bufs) {
|
|
assert current == null : dbgString() // this is a queue of 1.
|
|
+ "w.onNext current: " + current;
|
|
assert subscription != null : dbgString()
|
|
+ "w.onNext: subscription is null";
|
|
current = bufs;
|
|
tryFlushCurrent(client.isSelectorThread()); // may be in selector thread
|
|
// For instance in HTTP/2, a received SETTINGS frame might trigger
|
|
// the sending of a SETTINGS frame in turn which might cause
|
|
// onNext to be called from within the same selector thread that the
|
|
// original SETTINGS frames arrived on. If rs is the read-subscriber
|
|
// and ws is the write-subscriber then the following can occur:
|
|
// ReadEvent -> rs.onNext(bytes) -> process server SETTINGS -> write
|
|
// client SETTINGS -> ws.onNext(bytes) -> tryFlushCurrent
|
|
debugState("leaving w.onNext");
|
|
}
|
|
|
|
// Don't use a SequentialScheduler here: rely on onNext() being invoked
|
|
// sequentially, and not being invoked if there is no demand, request(1).
|
|
// onNext is usually called from within a user / executor thread.
|
|
// Initial writing will be performed in that thread. If for some reason,
|
|
// not all the data can be written, a writeEvent will be registered, and
|
|
// writing will resume in the selector manager thread when the
|
|
// writeEvent is fired.
|
|
//
|
|
// If this method is invoked in the selector manager thread (because of
|
|
// a writeEvent), then the executor will be used to invoke request(1),
|
|
// ensuring that onNext() won't be invoked from within the selector
|
|
// thread. If not in the selector manager thread, then request(1) is
|
|
// invoked directly.
|
|
void tryFlushCurrent(boolean inSelectorThread) {
|
|
List<ByteBuffer> bufs = current;
|
|
if (bufs == null) return;
|
|
if (client.isSelectorClosed()) {
|
|
signalError(client.selectorClosedException());
|
|
return;
|
|
}
|
|
try {
|
|
assert inSelectorThread == client.isSelectorThread() :
|
|
"should " + (inSelectorThread ? "" : "not ")
|
|
+ " be in the selector thread";
|
|
long remaining = Utils.remaining(bufs);
|
|
if (debug.on()) debug.log("trying to write: %d", remaining);
|
|
long written = writeAvailable(bufs);
|
|
if (debug.on()) debug.log("wrote: %d", written);
|
|
assert written >= 0 : "negative number of bytes written:" + written;
|
|
assert written <= remaining;
|
|
if (remaining - written == 0) {
|
|
current = null;
|
|
if (writeDemand.tryDecrement()) {
|
|
if (client.isSelectorClosed()) {
|
|
signalError(client.selectorClosedException());
|
|
return;
|
|
}
|
|
Runnable requestMore = this::requestMore;
|
|
if (inSelectorThread) {
|
|
assert client.isSelectorThread();
|
|
client.theExecutor().execute(requestMore);
|
|
} else {
|
|
assert !client.isSelectorThread();
|
|
requestMore.run();
|
|
}
|
|
}
|
|
} else {
|
|
resumeWriteEvent(inSelectorThread);
|
|
}
|
|
} catch (Throwable t) {
|
|
signalError(t);
|
|
}
|
|
}
|
|
|
|
// Kick off the initial request:1 that will start the writing side.
|
|
// Invoked in the selector manager thread.
|
|
void startSubscription() {
|
|
try {
|
|
if (debug.on()) debug.log("write: starting subscription");
|
|
if (Log.channel()) {
|
|
Log.logChannel("Start requesting bytes for writing to channel: {0}",
|
|
channelDescr());
|
|
}
|
|
assert client.isSelectorThread();
|
|
// make sure read registrations are handled before;
|
|
readPublisher.subscriptionImpl.handlePending();
|
|
if (debug.on()) debug.log("write: offloading requestMore");
|
|
// start writing;
|
|
client.theExecutor().execute(this::requestMore);
|
|
} catch(Throwable t) {
|
|
signalError(t);
|
|
}
|
|
}
|
|
|
|
void requestMore() {
|
|
WriteSubscription subscription = this.subscription;
|
|
subscription.requestMore();
|
|
}
|
|
|
|
@Override
|
|
public void onError(Throwable throwable) {
|
|
signalError(throwable);
|
|
}
|
|
|
|
@Override
|
|
public void onComplete() {
|
|
completed = true;
|
|
// no need to pause the write event here: the write event will
|
|
// be paused if there is nothing more to write.
|
|
List<ByteBuffer> bufs = current;
|
|
long remaining = bufs == null ? 0 : Utils.remaining(bufs);
|
|
if (debug.on())
|
|
debug.log( "write completed, %d yet to send", remaining);
|
|
debugState("InternalWriteSubscriber::onComplete");
|
|
}
|
|
|
|
void resumeWriteEvent(boolean inSelectorThread) {
|
|
if (debug.on()) debug.log("scheduling write event");
|
|
resumeEvent(writeEvent, this::signalError);
|
|
}
|
|
|
|
void signalWritable() {
|
|
if (debug.on()) debug.log("channel is writable");
|
|
tryFlushCurrent(true);
|
|
}
|
|
|
|
void signalError(Throwable error) {
|
|
if (debug.on()) debug.log(() -> "write error: " + error);
|
|
if (Log.channel()) {
|
|
Log.logChannel("Failed to write to channel ({0}: {1})",
|
|
channelDescr(), error);
|
|
}
|
|
completed = true;
|
|
readPublisher.signalError(error);
|
|
Flow.Subscription subscription = this.subscription;
|
|
if (subscription != null) subscription.cancel();
|
|
}
|
|
|
|
// A repeatable WriteEvent which is paused after firing and can
|
|
// be resumed if required - see SocketFlowEvent;
|
|
final class WriteEvent extends SocketFlowEvent {
|
|
final InternalWriteSubscriber sub;
|
|
WriteEvent(SocketChannel channel, InternalWriteSubscriber sub) {
|
|
super(SelectionKey.OP_WRITE, channel);
|
|
this.sub = sub;
|
|
}
|
|
@Override
|
|
protected final void signalEvent() {
|
|
try {
|
|
client.eventUpdated(this);
|
|
sub.signalWritable();
|
|
} catch(Throwable t) {
|
|
sub.signalError(t);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void signalError(Throwable error) {
|
|
sub.signalError(error);
|
|
}
|
|
|
|
@Override
|
|
Logger debug() { return debug; }
|
|
}
|
|
|
|
final class WriteSubscription implements Flow.Subscription {
|
|
final Flow.Subscription upstreamSubscription;
|
|
volatile boolean cancelled;
|
|
WriteSubscription(Flow.Subscription subscription) {
|
|
this.upstreamSubscription = subscription;
|
|
}
|
|
|
|
@Override
|
|
public void request(long n) {
|
|
if (cancelled) return;
|
|
upstreamSubscription.request(n);
|
|
}
|
|
|
|
@Override
|
|
public void cancel() {
|
|
if (cancelled) return;
|
|
if (debug.on()) debug.log("write: cancel");
|
|
if (Log.channel()) {
|
|
Log.logChannel("Cancelling write subscription");
|
|
}
|
|
dropSubscription();
|
|
upstreamSubscription.cancel();
|
|
}
|
|
|
|
void dropSubscription() {
|
|
if (debug.on()) debug.log("write: resetting demand to 0");
|
|
synchronized (InternalWriteSubscriber.this) {
|
|
cancelled = true;
|
|
writeDemand.reset();
|
|
}
|
|
}
|
|
|
|
void requestMore() {
|
|
try {
|
|
if (completed || cancelled) return;
|
|
boolean requestMore;
|
|
long d;
|
|
// don't fiddle with demand after cancel.
|
|
// see dropSubscription.
|
|
synchronized (InternalWriteSubscriber.this) {
|
|
if (cancelled) return;
|
|
d = writeDemand.get();
|
|
requestMore = writeDemand.increaseIfFulfilled();
|
|
}
|
|
if (requestMore) {
|
|
if (debug.on()) debug.log("write: requesting more...");
|
|
upstreamSubscription.request(1);
|
|
} else {
|
|
if (debug.on())
|
|
debug.log("write: no need to request more: %d", d);
|
|
}
|
|
} catch (Throwable t) {
|
|
if (debug.on())
|
|
debug.log("write: error while requesting more: " + t);
|
|
signalError(t);
|
|
} finally {
|
|
debugState("leaving requestMore: ");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ===================================================================== //
|
|
// Reading //
|
|
// ===================================================================== //
|
|
|
|
// The InternalReadPublisher uses a SequentialScheduler to ensure that
|
|
// onNext/onError/onComplete are called sequentially on the caller's
|
|
// subscriber.
|
|
// However, it relies on the fact that the only time where
|
|
// runOrSchedule() is called from a user/executor thread is in signalError,
|
|
// right after the errorRef has been set.
|
|
// Because the sequential scheduler's task always checks for errors first,
|
|
// and always terminate the scheduler on error, then it is safe to assume
|
|
// that if it reaches the point where it reads from the channel, then
|
|
// it is running in the SelectorManager thread. This is because all
|
|
// other invocation of runOrSchedule() are triggered from within a
|
|
// ReadEvent.
|
|
//
|
|
// When pausing/resuming the event, some shortcuts can then be taken
|
|
// when we know we're running in the selector manager thread
|
|
// (in that case there's no need to call client.eventUpdated(readEvent);
|
|
//
|
|
private final class InternalReadPublisher
|
|
implements Flow.Publisher<List<ByteBuffer>> {
|
|
private final InternalReadSubscription subscriptionImpl
|
|
= new InternalReadSubscription();
|
|
private final AtomicReference<ReadSubscription> pendingSubscriptions
|
|
= new AtomicReference<>();
|
|
private volatile ReadSubscription subscription;
|
|
|
|
@Override
|
|
public void subscribe(Flow.Subscriber<? super List<ByteBuffer>> s) {
|
|
Objects.requireNonNull(s);
|
|
if (debug.on()) debug.log("Offering new subscriber: %s", s);
|
|
subscriptionImpl.offer(FlowTube.asTubeSubscriber(s));
|
|
debugState("leaving read.subscribe: ");
|
|
}
|
|
|
|
void signalError(Throwable error) {
|
|
if (debug.on()) debug.log("error signalled " + error);
|
|
if (!errorRef.compareAndSet(null, error)) {
|
|
return;
|
|
}
|
|
if (Log.channel()) {
|
|
Log.logChannel("Error signalled on channel {0}: {1}",
|
|
channelDescr(), error);
|
|
}
|
|
subscriptionImpl.handleError();
|
|
}
|
|
|
|
final class ReadSubscription implements Flow.Subscription {
|
|
final InternalReadSubscription impl;
|
|
final TubeSubscriber subscriber;
|
|
final AtomicReference<Throwable> errorRef = new AtomicReference<>();
|
|
final BufferSource bufferSource;
|
|
volatile boolean subscribed;
|
|
volatile boolean cancelled;
|
|
volatile boolean completed;
|
|
private volatile boolean stopped;
|
|
|
|
public ReadSubscription(InternalReadSubscription impl,
|
|
TubeSubscriber subscriber) {
|
|
this.impl = impl;
|
|
this.bufferSource = subscriber.supportsRecycling()
|
|
? new SSLDirectBufferSource(client)
|
|
: SocketTube.this.sliceBuffersSource;
|
|
this.subscriber = subscriber;
|
|
}
|
|
|
|
@Override
|
|
public void cancel() {
|
|
cancelled = true;
|
|
}
|
|
|
|
@Override
|
|
public void request(long n) {
|
|
if (!cancelled && !stopped) {
|
|
// should be safe to not synchronize here.
|
|
impl.request(n);
|
|
} else {
|
|
if (debug.on())
|
|
debug.log("subscription stopped or cancelled, ignoring request %d", n);
|
|
}
|
|
}
|
|
|
|
void signalCompletion() {
|
|
assert subscribed || cancelled;
|
|
if (completed || cancelled) return;
|
|
synchronized (this) {
|
|
if (completed) return;
|
|
completed = true;
|
|
}
|
|
Throwable error = errorRef.get();
|
|
if (error != null) {
|
|
if (debug.on())
|
|
debug.log("forwarding error to subscriber: " + error);
|
|
subscriber.onError(error);
|
|
} else {
|
|
if (debug.on()) debug.log("completing subscriber");
|
|
subscriber.onComplete();
|
|
}
|
|
}
|
|
|
|
void signalOnSubscribe() {
|
|
if (subscribed || cancelled) return;
|
|
synchronized (this) {
|
|
if (subscribed || cancelled) return;
|
|
subscribed = true;
|
|
}
|
|
subscriber.onSubscribe(this);
|
|
if (debug.on()) debug.log("onSubscribe called");
|
|
if (errorRef.get() != null) {
|
|
signalCompletion();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called when switching subscriber on the {@link InternalReadSubscription}.
|
|
* This subscriber is the old subscriber. Demand on the internal
|
|
* subscription will be reset and reading will be paused until the
|
|
* new subscriber is subscribed.
|
|
* This should ensure that no data is routed to this subscriber
|
|
* until the new subscriber is subscribed.
|
|
*/
|
|
synchronized void stopReading() {
|
|
stopped = true;
|
|
}
|
|
|
|
synchronized boolean tryDecrementDemand() {
|
|
if (stopped) return false;
|
|
return impl.demand.tryDecrement();
|
|
}
|
|
|
|
synchronized boolean isStopped() {
|
|
return stopped;
|
|
}
|
|
|
|
synchronized void increaseDemand(long n) {
|
|
if (!stopped) impl.demand.increase(n);
|
|
}
|
|
}
|
|
|
|
final class InternalReadSubscription implements Flow.Subscription {
|
|
|
|
private final Demand demand = new Demand();
|
|
final SequentialScheduler readScheduler;
|
|
private volatile boolean completed;
|
|
private final ReadEvent readEvent;
|
|
private final AsyncEvent subscribeEvent;
|
|
|
|
InternalReadSubscription() {
|
|
readScheduler = new SequentialScheduler(new SocketFlowTask(this::read));
|
|
subscribeEvent = new AsyncTriggerEvent(this::signalError,
|
|
this::handleSubscribeEvent);
|
|
readEvent = new ReadEvent(channel, this);
|
|
}
|
|
|
|
/*
|
|
* This method must be invoked before any other method of this class.
|
|
*/
|
|
final void signalSubscribe() {
|
|
if (readScheduler.isStopped() || completed) {
|
|
// if already completed or stopped we can handle any
|
|
// pending connection directly from here.
|
|
if (debug.on())
|
|
debug.log("handling pending subscription while completed");
|
|
handlePending();
|
|
} else {
|
|
try {
|
|
if (debug.on()) debug.log("registering subscribe event");
|
|
client.registerEvent(subscribeEvent);
|
|
} catch (Throwable t) {
|
|
signalError(t);
|
|
handlePending();
|
|
}
|
|
}
|
|
}
|
|
|
|
final void handleSubscribeEvent() {
|
|
assert client.isSelectorThread();
|
|
debug.log("subscribe event raised");
|
|
if (Log.channel()) Log.logChannel("Start reading from {0}", channelDescr());
|
|
handlePending();
|
|
}
|
|
|
|
|
|
/*
|
|
* Although this method is thread-safe, the Reactive-Streams spec seems
|
|
* to not require it to be as such. It's a responsibility of the
|
|
* subscriber to signal demand in a thread-safe manner.
|
|
*
|
|
* See Reactive Streams specification, rules 2.7 and 3.4.
|
|
*/
|
|
@Override
|
|
public final void request(long n) {
|
|
if (n > 0L) {
|
|
boolean wasFulfilled = demand.increase(n);
|
|
if (wasFulfilled) {
|
|
if (debug.on()) debug.log("got some demand for reading");
|
|
resumeReadEvent();
|
|
// if demand has been changed from fulfilled
|
|
// to unfulfilled register read event;
|
|
}
|
|
} else {
|
|
signalError(new IllegalArgumentException("non-positive request"));
|
|
}
|
|
debugState("leaving request("+n+"): ");
|
|
}
|
|
|
|
@Override
|
|
public final void cancel() {
|
|
pauseReadEvent();
|
|
if (debug.on()) debug.log("Read subscription cancelled");
|
|
if (Log.channel()) {
|
|
Log.logChannel("Read subscription cancelled for channel {0}",
|
|
channelDescr());
|
|
}
|
|
if (debug.on()) debug.log("Stopping read scheduler");
|
|
readScheduler.stop();
|
|
}
|
|
|
|
private void resumeReadEvent() {
|
|
if (debug.on()) debug.log("resuming read event");
|
|
resumeEvent(readEvent, this::signalError);
|
|
}
|
|
|
|
private void pauseReadEvent() {
|
|
if (debug.on()) debug.log("pausing read event");
|
|
pauseEvent(readEvent, this::signalError);
|
|
}
|
|
|
|
|
|
final void handleError() {
|
|
assert errorRef.get() != null;
|
|
readScheduler.runOrSchedule();
|
|
}
|
|
|
|
final void signalError(Throwable error) {
|
|
if (debug.on()) debug.log("signal read error: " + error);
|
|
if (!errorRef.compareAndSet(null, error)) {
|
|
return;
|
|
}
|
|
if (debug.on()) debug.log("got read error: " + error);
|
|
if (Log.channel()) {
|
|
Log.logChannel("Read error signalled on channel {0}: {1}",
|
|
channelDescr(), error);
|
|
}
|
|
readScheduler.runOrSchedule();
|
|
}
|
|
|
|
final void signalReadable() {
|
|
readScheduler.runOrSchedule();
|
|
}
|
|
|
|
/** The body of the task that runs in SequentialScheduler. */
|
|
final void read() {
|
|
// It is important to only call pauseReadEvent() when stopping
|
|
// the scheduler. The event is automatically paused before
|
|
// firing, and trying to pause it again could cause a race
|
|
// condition between this loop, which calls tryDecrementDemand(),
|
|
// and the thread that calls request(n), which will try to resume
|
|
// reading.
|
|
try {
|
|
while(!readScheduler.isStopped()) {
|
|
if (completed) return;
|
|
|
|
// make sure we have a subscriber
|
|
if (handlePending()) {
|
|
if (debug.on())
|
|
debug.log("pending subscriber subscribed");
|
|
return;
|
|
}
|
|
|
|
// If an error was signaled, we might not be in the
|
|
// the selector thread, and that is OK, because we
|
|
// will just call onError and return.
|
|
ReadSubscription current = subscription;
|
|
Throwable error = errorRef.get();
|
|
if (current == null) {
|
|
assert error != null;
|
|
if (debug.on())
|
|
debug.log("error raised before subscriber subscribed: %s",
|
|
(Object)error);
|
|
return;
|
|
}
|
|
TubeSubscriber subscriber = current.subscriber;
|
|
if (error != null) {
|
|
completed = true;
|
|
// safe to pause here because we're finished anyway.
|
|
pauseReadEvent();
|
|
if (debug.on())
|
|
debug.log("Sending error " + error
|
|
+ " to subscriber " + subscriber);
|
|
if (Log.channel()) {
|
|
Log.logChannel("Raising error with subscriber for {0}: {1}",
|
|
channelDescr(), error);
|
|
}
|
|
current.errorRef.compareAndSet(null, error);
|
|
current.signalCompletion();
|
|
if (debug.on()) debug.log("Stopping read scheduler");
|
|
readScheduler.stop();
|
|
debugState("leaving read() loop with error: ");
|
|
return;
|
|
}
|
|
|
|
// If we reach here then we must be in the selector thread.
|
|
assert client.isSelectorThread();
|
|
if (current.tryDecrementDemand()) {
|
|
// we have demand.
|
|
try {
|
|
List<ByteBuffer> bytes = readAvailable(current.bufferSource);
|
|
if (bytes == EOF) {
|
|
if (!completed) {
|
|
if (debug.on()) debug.log("got read EOF");
|
|
if (Log.channel()) {
|
|
Log.logChannel("EOF read from channel: {0}",
|
|
channelDescr());
|
|
}
|
|
completed = true;
|
|
// safe to pause here because we're finished
|
|
// anyway.
|
|
pauseReadEvent();
|
|
current.signalCompletion();
|
|
if (debug.on()) debug.log("Stopping read scheduler");
|
|
readScheduler.stop();
|
|
}
|
|
debugState("leaving read() loop after EOF: ");
|
|
return;
|
|
} else if (Utils.remaining(bytes) > 0) {
|
|
// the subscriber is responsible for offloading
|
|
// to another thread if needed.
|
|
if (debug.on())
|
|
debug.log("read bytes: " + Utils.remaining(bytes));
|
|
assert !current.completed;
|
|
subscriber.onNext(bytes);
|
|
// we could continue looping until the demand
|
|
// reaches 0. However, that would risk starving
|
|
// other connections (bound to other socket
|
|
// channels) - as other selected keys activated
|
|
// by the selector manager thread might be
|
|
// waiting for this event to terminate.
|
|
// So resume the read event and return now...
|
|
resumeReadEvent();
|
|
if (errorRef.get() != null) continue;
|
|
debugState("leaving read() loop after onNext: ");
|
|
return;
|
|
} else {
|
|
// nothing available!
|
|
if (debug.on()) debug.log("no more bytes available");
|
|
// re-increment the demand and resume the read
|
|
// event. This ensures that this loop is
|
|
// executed again when the socket becomes
|
|
// readable again.
|
|
if (!current.isStopped()) {
|
|
current.increaseDemand(1);
|
|
resumeReadEvent();
|
|
}
|
|
if (errorRef.get() != null) continue;
|
|
debugState("leaving read() loop with no bytes");
|
|
return;
|
|
}
|
|
} catch (Throwable x) {
|
|
signalError(x);
|
|
continue;
|
|
}
|
|
} else {
|
|
if (debug.on()) debug.log("no more demand for reading");
|
|
// the event is paused just after firing, so it should
|
|
// still be paused here, unless the demand was just
|
|
// incremented from 0 to n, in which case, the
|
|
// event will be resumed, causing this loop to be
|
|
// invoked again when the socket becomes readable:
|
|
// This is what we want.
|
|
// Trying to pause the event here would actually
|
|
// introduce a race condition between this loop and
|
|
// request(n).
|
|
if (errorRef.get() != null) continue;
|
|
debugState("leaving read() loop with no demand");
|
|
break;
|
|
}
|
|
}
|
|
} catch (Throwable t) {
|
|
if (debug.on()) debug.log("Unexpected exception in read loop", t);
|
|
signalError(t);
|
|
} finally {
|
|
if (readScheduler.isStopped()) {
|
|
if (debug.on()) debug.log("Read scheduler stopped");
|
|
if (Log.channel()) {
|
|
Log.logChannel("Stopped reading from channel {0}", channelDescr());
|
|
}
|
|
}
|
|
handlePending();
|
|
}
|
|
}
|
|
|
|
synchronized void offer(TubeSubscriber sub) {
|
|
ReadSubscription target = new ReadSubscription(this, sub);
|
|
ReadSubscription previous = pendingSubscriptions.getAndSet(target);
|
|
if (previous != null) {
|
|
if (debug.on())
|
|
debug.log("read publisher: dropping pending subscriber: "
|
|
+ previous.subscriber);
|
|
previous.errorRef.compareAndSet(null, errorRef.get());
|
|
// make sure no data will be routed to the old subscriber.
|
|
previous.stopReading();
|
|
previous.signalOnSubscribe();
|
|
if (completed) {
|
|
previous.signalCompletion();
|
|
} else {
|
|
previous.subscriber.dropSubscription();
|
|
}
|
|
}
|
|
if (debug.on()) {
|
|
debug.log("read publisher got new subscriber: " + sub);
|
|
}
|
|
signalSubscribe();
|
|
}
|
|
|
|
synchronized boolean handlePending() {
|
|
boolean subscribed = false;
|
|
ReadSubscription current = subscription;
|
|
ReadSubscription pending = pendingSubscriptions.getAndSet(null);
|
|
if (pending != null) {
|
|
subscribed = true;
|
|
if (debug.on())
|
|
debug.log("handling pending subscription for %s",
|
|
pending.subscriber);
|
|
if (current != null && !completed) {
|
|
debug.log("dropping subscription for current %s",
|
|
current.subscriber);
|
|
current.stopReading();
|
|
current.subscriber.dropSubscription();
|
|
}
|
|
if (debug.on()) debug.log("read demand reset to 0");
|
|
demand.reset(); // subscriber will increase demand if it needs to.
|
|
pending.errorRef.compareAndSet(null, errorRef.get());
|
|
if (!readScheduler.isStopped()) {
|
|
subscription = pending;
|
|
} else {
|
|
if (debug.on()) debug.log("socket tube is already stopped");
|
|
}
|
|
if (debug.on()) debug.log("calling onSubscribe on " + pending.subscriber);
|
|
pending.signalOnSubscribe();
|
|
if (completed) {
|
|
pending.errorRef.compareAndSet(null, errorRef.get());
|
|
pending.signalCompletion();
|
|
}
|
|
}
|
|
return subscribed;
|
|
}
|
|
}
|
|
|
|
|
|
// A repeatable ReadEvent which is paused after firing and can
|
|
// be resumed if required - see SocketFlowEvent;
|
|
final class ReadEvent extends SocketFlowEvent {
|
|
final InternalReadSubscription sub;
|
|
ReadEvent(SocketChannel channel, InternalReadSubscription sub) {
|
|
super(SelectionKey.OP_READ, channel);
|
|
this.sub = sub;
|
|
}
|
|
@Override
|
|
protected final void signalEvent() {
|
|
try {
|
|
client.eventUpdated(this);
|
|
sub.signalReadable();
|
|
} catch(Throwable t) {
|
|
sub.signalError(t);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected final void signalError(Throwable error) {
|
|
if (debug.on()) debug.log("signalError to %s (%s)", sub, error);
|
|
sub.signalError(error);
|
|
}
|
|
|
|
@Override
|
|
Logger debug() { return debug; }
|
|
}
|
|
}
|
|
|
|
// ===================================================================== //
|
|
// Buffer Management //
|
|
// ===================================================================== //
|
|
|
|
// This interface is used by readAvailable(BufferSource);
|
|
public interface BufferSource {
|
|
/**
|
|
* Returns a buffer to read data from the socket.
|
|
*
|
|
* @implNote
|
|
* Different implementation can have different strategies, as to
|
|
* which kind of buffer to return, or whether to return the same
|
|
* buffer. The only constraints are that:
|
|
* a. the buffer returned must not be null
|
|
* b. the buffer position indicates where to start reading
|
|
* c. the buffer limit indicates where to stop reading.
|
|
* d. the buffer is 'free' - that is - it is not used
|
|
* or retained by anybody else
|
|
*
|
|
* @return A buffer to read data from the socket.
|
|
*/
|
|
ByteBuffer getBuffer();
|
|
|
|
/**
|
|
* Appends the read-data in {@code buffer} to the list of buffer to
|
|
* be sent downstream to the subscriber. May return a new
|
|
* list, or append to the given list.
|
|
*
|
|
* @implNote
|
|
* Different implementation can have different strategies, but
|
|
* must obviously be consistent with the implementation of the
|
|
* getBuffer() method. For instance, an implementation could
|
|
* decide to add the buffer to the list and return a new buffer
|
|
* next time getBuffer() is called, or could decide to add a buffer
|
|
* slice to the list and return the same buffer (if remaining
|
|
* space is available) next time getBuffer() is called.
|
|
*
|
|
* @param list The list before adding the data. Can be null.
|
|
* @param buffer The buffer containing the data to add to the list.
|
|
* @param start The start position at which data were read.
|
|
* The current buffer position indicates the end.
|
|
* @return A possibly new list where a buffer containing the
|
|
* data read from the socket has been added.
|
|
*/
|
|
List<ByteBuffer> append(List<ByteBuffer> list, ByteBuffer buffer, int start);
|
|
|
|
/**
|
|
* Returns the given unused {@code buffer}, previously obtained from
|
|
* {@code getBuffer}.
|
|
*
|
|
* @implNote This method can be used, if necessary, to return
|
|
* the unused buffer to the pull.
|
|
*
|
|
* @param buffer The unused buffer.
|
|
*/
|
|
default void returnUnused(ByteBuffer buffer) { }
|
|
}
|
|
|
|
// An implementation of BufferSource used for unencrypted data.
|
|
// This buffer source uses heap buffers and avoids wasting memory
|
|
// by forwarding read-only buffer slices downstream.
|
|
// Buffers allocated through this source are simply GC'ed when
|
|
// they are no longer referenced.
|
|
private static final class SliceBufferSource implements BufferSource {
|
|
private final Supplier<ByteBuffer> factory;
|
|
private volatile ByteBuffer current;
|
|
|
|
public SliceBufferSource() {
|
|
this(Utils::getBuffer);
|
|
}
|
|
public SliceBufferSource(Supplier<ByteBuffer> factory) {
|
|
this.factory = Objects.requireNonNull(factory);
|
|
}
|
|
|
|
// Reuses the same buffer if some space remains available.
|
|
// Otherwise, returns a new heap buffer.
|
|
@Override
|
|
public final ByteBuffer getBuffer() {
|
|
ByteBuffer buf = current;
|
|
buf = (buf == null || !buf.hasRemaining())
|
|
? (current = factory.get()) : buf;
|
|
assert buf.hasRemaining();
|
|
return buf;
|
|
}
|
|
|
|
// Adds a read-only slice to the list, potentially returning a
|
|
// new list with that slice at the end.
|
|
@Override
|
|
public final List<ByteBuffer> append(List <ByteBuffer> list, ByteBuffer buf, int start) {
|
|
// creates a slice to add to the list
|
|
int limit = buf.limit();
|
|
buf.limit(buf.position());
|
|
buf.position(start);
|
|
ByteBuffer slice = buf.slice();
|
|
|
|
// restore buffer state to what it was before creating the slice
|
|
buf.position(buf.limit());
|
|
buf.limit(limit);
|
|
|
|
// add the buffer to the list
|
|
return SocketTube.listOf(list, slice.asReadOnlyBuffer());
|
|
}
|
|
}
|
|
|
|
|
|
// An implementation of BufferSource used for encrypted data.
|
|
// This buffer source uses direct byte buffers that will be
|
|
// recycled by the SocketTube subscriber.
|
|
//
|
|
private static final class SSLDirectBufferSource implements BufferSource {
|
|
private final BufferSupplier factory;
|
|
private final HttpClientImpl client;
|
|
private ByteBuffer current;
|
|
|
|
public SSLDirectBufferSource(HttpClientImpl client) {
|
|
this.client = Objects.requireNonNull(client);
|
|
this.factory = Objects.requireNonNull(client.getSSLBufferSupplier());
|
|
}
|
|
|
|
// Obtains a 'free' byte buffer from the pool, or returns
|
|
// the same buffer if nothing was read at the previous cycle.
|
|
// The subscriber will be responsible for recycling this
|
|
// buffer into the pool (see SSLFlowDelegate.Reader)
|
|
@Override
|
|
public final ByteBuffer getBuffer() {
|
|
assert client.isSelectorThread();
|
|
ByteBuffer buf = current;
|
|
if (buf == null) {
|
|
buf = current = factory.get();
|
|
}
|
|
assert buf.hasRemaining();
|
|
assert buf.position() == 0;
|
|
return buf;
|
|
}
|
|
|
|
// Adds the buffer to the list. The buffer will be later returned to the
|
|
// pool by the subscriber (see SSLFlowDelegate.Reader).
|
|
// The next buffer returned by getBuffer() will be obtained from the
|
|
// pool. It might be the same buffer or another one.
|
|
// Because socket tube can read up to MAX_BUFFERS = 3 buffers, and because
|
|
// recycling will happen in the flow before onNext returns, then the
|
|
// pool can not grow larger than MAX_BUFFERS = 3 buffers, even though
|
|
// it's shared by all SSL connections opened on that client.
|
|
@Override
|
|
public final List<ByteBuffer> append(List <ByteBuffer> list, ByteBuffer buf, int start) {
|
|
assert client.isSelectorThread();
|
|
assert buf.isDirect();
|
|
assert start == 0;
|
|
assert current == buf;
|
|
current = null;
|
|
buf.limit(buf.position());
|
|
buf.position(start);
|
|
// add the buffer to the list
|
|
return SocketTube.listOf(list, buf);
|
|
}
|
|
|
|
@Override
|
|
public void returnUnused(ByteBuffer buffer) {
|
|
// if current is null, then the buffer will have been added to the
|
|
// list, through append. Otherwise, current is not null, and needs
|
|
// to be returned to prevent the buffer supplier pool from growing
|
|
// to more than MAX_BUFFERS.
|
|
assert buffer == current;
|
|
ByteBuffer buf = current;
|
|
if (buf != null) {
|
|
assert buf.position() == 0;
|
|
current = null;
|
|
// the supplier assert if buf has remaining
|
|
buf.limit(buf.position());
|
|
factory.recycle(buf);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ===================================================================== //
|
|
// Socket Channel Read/Write //
|
|
// ===================================================================== //
|
|
static final int MAX_BUFFERS = 3;
|
|
static final List<ByteBuffer> EOF = List.of();
|
|
static final List<ByteBuffer> NOTHING = List.of(Utils.EMPTY_BYTEBUFFER);
|
|
|
|
// readAvailable() will read bytes into the 'current' ByteBuffer until
|
|
// the ByteBuffer is full, or 0 or -1 (EOF) is returned by read().
|
|
// When that happens, a slice of the data that has been read so far
|
|
// is inserted into the returned buffer list, and if the current buffer
|
|
// has remaining space, that space will be used to read more data when
|
|
// the channel becomes readable again.
|
|
private List<ByteBuffer> readAvailable(BufferSource buffersSource) throws IOException {
|
|
ByteBuffer buf = buffersSource.getBuffer();
|
|
assert buf.hasRemaining();
|
|
|
|
int read;
|
|
int pos = buf.position();
|
|
List<ByteBuffer> list = null;
|
|
while (buf.hasRemaining()) {
|
|
try {
|
|
while ((read = channel.read(buf)) > 0) {
|
|
if (!buf.hasRemaining())
|
|
break;
|
|
}
|
|
} catch (IOException x) {
|
|
if (buf.position() == pos && list == null) {
|
|
// make sure that the buffer source will recycle
|
|
// 'buf' if needed
|
|
buffersSource.returnUnused(buf);
|
|
// no bytes have been read, just throw...
|
|
throw x;
|
|
} else {
|
|
// some bytes have been read, return them and fail next time
|
|
errorRef.compareAndSet(null, x);
|
|
read = 0; // ensures outer loop will exit
|
|
}
|
|
}
|
|
|
|
// nothing read;
|
|
if (buf.position() == pos) {
|
|
// An empty list signals the end of data, and should only be
|
|
// returned if read == -1. If some data has already been read,
|
|
// then it must be returned. -1 will be returned next time
|
|
// the caller attempts to read something.
|
|
buffersSource.returnUnused(buf);
|
|
if (list == null) {
|
|
// nothing read - list was null - return EOF or NOTHING
|
|
list = read == -1 ? EOF : NOTHING;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// check whether this buffer has still some free space available.
|
|
// if so, we will keep it for the next round.
|
|
list = buffersSource.append(list, buf, pos);
|
|
|
|
if (read <= 0 || list.size() == MAX_BUFFERS) {
|
|
break;
|
|
}
|
|
|
|
buf = buffersSource.getBuffer();
|
|
pos = buf.position();
|
|
assert buf.hasRemaining();
|
|
}
|
|
return list;
|
|
}
|
|
|
|
private static <T> List<T> listOf(List<T> list, T item) {
|
|
int size = list == null ? 0 : list.size();
|
|
switch (size) {
|
|
case 0: return List.of(item);
|
|
case 1: return List.of(list.get(0), item);
|
|
case 2: return List.of(list.get(0), list.get(1), item);
|
|
default: // slow path if MAX_BUFFERS > 3
|
|
List<T> res = list instanceof ArrayList ? list : new ArrayList<>(list);
|
|
res.add(item);
|
|
return res;
|
|
}
|
|
}
|
|
|
|
private long writeAvailable(List<ByteBuffer> bytes) throws IOException {
|
|
ByteBuffer[] srcs = bytes.toArray(Utils.EMPTY_BB_ARRAY);
|
|
final long remaining = Utils.remaining(srcs);
|
|
long written = 0;
|
|
while (remaining > written) {
|
|
try {
|
|
long w = channel.write(srcs);
|
|
assert w >= 0 : "negative number of bytes written:" + w;
|
|
if (w == 0) {
|
|
break;
|
|
}
|
|
written += w;
|
|
} catch (IOException x) {
|
|
if (written == 0) {
|
|
// no bytes were written just throw
|
|
throw x;
|
|
} else {
|
|
// return how many bytes were written, will fail next time
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return written;
|
|
}
|
|
|
|
private void resumeEvent(SocketFlowEvent event,
|
|
Consumer<Throwable> errorSignaler) {
|
|
boolean registrationRequired;
|
|
synchronized (lock) {
|
|
registrationRequired = !event.registered();
|
|
event.resume();
|
|
}
|
|
try {
|
|
if (registrationRequired) {
|
|
client.registerEvent(event);
|
|
} else {
|
|
client.eventUpdated(event);
|
|
}
|
|
} catch(Throwable t) {
|
|
errorSignaler.accept(t);
|
|
}
|
|
}
|
|
|
|
private void pauseEvent(SocketFlowEvent event,
|
|
Consumer<Throwable> errorSignaler) {
|
|
synchronized (lock) {
|
|
event.pause();
|
|
}
|
|
try {
|
|
client.eventUpdated(event);
|
|
} catch(Throwable t) {
|
|
errorSignaler.accept(t);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void connectFlows(TubePublisher writePublisher,
|
|
TubeSubscriber readSubscriber) {
|
|
if (debug.on()) debug.log("connecting flows");
|
|
this.subscribe(readSubscriber);
|
|
writePublisher.subscribe(this);
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return dbgString();
|
|
}
|
|
|
|
final String dbgString() {
|
|
return "SocketTube("+ connectionLabel +")";
|
|
}
|
|
|
|
final String channelDescr() {
|
|
return String.valueOf(channel);
|
|
}
|
|
}
|