mirror of https://github.com/tikv/client-java.git
* cherry pick #481 to release-3.1 Signed-off-by: ti-srebot <ti-srebot@pingcap.com> * resolve confilcts Signed-off-by: marsishandsome <marsishandsome@gmail.com> Co-authored-by: Liangliang Gu <marsishandsome@gmail.com>
This commit is contained in:
parent
3bb2eada85
commit
a577795d90
10
pom.xml
10
pom.xml
|
|
@ -78,6 +78,16 @@
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.perfmark</groupId>
|
||||||
|
<artifactId>perfmark-api</artifactId>
|
||||||
|
<version>0.24.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.perfmark</groupId>
|
||||||
|
<artifactId>perfmark-traceviewer</artifactId>
|
||||||
|
<version>0.24.0</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.antlr</groupId>
|
<groupId>org.antlr</groupId>
|
||||||
<artifactId>antlr4-runtime</artifactId>
|
<artifactId>antlr4-runtime</artifactId>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,797 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2014 The gRPC Authors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.grpc.internal;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkArgument;
|
||||||
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
import static com.google.common.base.Preconditions.checkState;
|
||||||
|
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
|
||||||
|
import static io.grpc.Contexts.statusFromCancelled;
|
||||||
|
import static io.grpc.Status.DEADLINE_EXCEEDED;
|
||||||
|
import static io.grpc.internal.GrpcUtil.CONTENT_ACCEPT_ENCODING_KEY;
|
||||||
|
import static io.grpc.internal.GrpcUtil.CONTENT_ENCODING_KEY;
|
||||||
|
import static io.grpc.internal.GrpcUtil.MESSAGE_ACCEPT_ENCODING_KEY;
|
||||||
|
import static io.grpc.internal.GrpcUtil.MESSAGE_ENCODING_KEY;
|
||||||
|
import static java.lang.Math.max;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import com.google.common.base.MoreObjects;
|
||||||
|
import io.grpc.Attributes;
|
||||||
|
import io.grpc.CallOptions;
|
||||||
|
import io.grpc.ClientCall;
|
||||||
|
import io.grpc.Codec;
|
||||||
|
import io.grpc.Compressor;
|
||||||
|
import io.grpc.CompressorRegistry;
|
||||||
|
import io.grpc.Context;
|
||||||
|
import io.grpc.Context.CancellationListener;
|
||||||
|
import io.grpc.Deadline;
|
||||||
|
import io.grpc.DecompressorRegistry;
|
||||||
|
import io.grpc.InternalConfigSelector;
|
||||||
|
import io.grpc.InternalDecompressorRegistry;
|
||||||
|
import io.grpc.Metadata;
|
||||||
|
import io.grpc.MethodDescriptor;
|
||||||
|
import io.grpc.MethodDescriptor.MethodType;
|
||||||
|
import io.grpc.Status;
|
||||||
|
import io.grpc.internal.ManagedChannelServiceConfig.MethodInfo;
|
||||||
|
import io.perfmark.Link;
|
||||||
|
import io.perfmark.PerfMark;
|
||||||
|
import io.perfmark.Tag;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.concurrent.CancellationException;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.ScheduledFuture;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
/** Implementation of {@link ClientCall}. */
|
||||||
|
final class ClientCallImpl<ReqT, RespT> extends ClientCall<ReqT, RespT> {
|
||||||
|
|
||||||
|
private static final Logger log = Logger.getLogger(ClientCallImpl.class.getName());
|
||||||
|
private static final byte[] FULL_STREAM_DECOMPRESSION_ENCODINGS =
|
||||||
|
"gzip".getBytes(Charset.forName("US-ASCII"));
|
||||||
|
|
||||||
|
private final MethodDescriptor<ReqT, RespT> method;
|
||||||
|
private final Tag tag;
|
||||||
|
private final Executor callExecutor;
|
||||||
|
private final boolean callExecutorIsDirect;
|
||||||
|
private final CallTracer channelCallsTracer;
|
||||||
|
private final Context context;
|
||||||
|
private volatile ScheduledFuture<?> deadlineCancellationFuture;
|
||||||
|
private final boolean unaryRequest;
|
||||||
|
private CallOptions callOptions;
|
||||||
|
private ClientStream stream;
|
||||||
|
private volatile boolean cancelListenersShouldBeRemoved;
|
||||||
|
private boolean cancelCalled;
|
||||||
|
private boolean halfCloseCalled;
|
||||||
|
private final ClientStreamProvider clientStreamProvider;
|
||||||
|
private final ContextCancellationListener cancellationListener =
|
||||||
|
new ContextCancellationListener();
|
||||||
|
private final ScheduledExecutorService deadlineCancellationExecutor;
|
||||||
|
private boolean fullStreamDecompression;
|
||||||
|
private DecompressorRegistry decompressorRegistry = DecompressorRegistry.getDefaultInstance();
|
||||||
|
private CompressorRegistry compressorRegistry = CompressorRegistry.getDefaultInstance();
|
||||||
|
|
||||||
|
ClientCallImpl(
|
||||||
|
MethodDescriptor<ReqT, RespT> method,
|
||||||
|
Executor executor,
|
||||||
|
CallOptions callOptions,
|
||||||
|
ClientStreamProvider clientStreamProvider,
|
||||||
|
ScheduledExecutorService deadlineCancellationExecutor,
|
||||||
|
CallTracer channelCallsTracer,
|
||||||
|
// TODO(zdapeng): remove this arg
|
||||||
|
@Nullable InternalConfigSelector configSelector) {
|
||||||
|
this.method = method;
|
||||||
|
// TODO(carl-mastrangelo): consider moving this construction to ManagedChannelImpl.
|
||||||
|
this.tag = PerfMark.createTag(method.getFullMethodName(), System.identityHashCode(this));
|
||||||
|
// If we know that the executor is a direct executor, we don't need to wrap it with a
|
||||||
|
// SerializingExecutor. This is purely for performance reasons.
|
||||||
|
// See https://github.com/grpc/grpc-java/issues/368
|
||||||
|
if (executor == directExecutor()) {
|
||||||
|
this.callExecutor = new SerializeReentrantCallsDirectExecutor();
|
||||||
|
callExecutorIsDirect = true;
|
||||||
|
} else {
|
||||||
|
this.callExecutor = new SerializingExecutor(executor);
|
||||||
|
callExecutorIsDirect = false;
|
||||||
|
}
|
||||||
|
this.channelCallsTracer = channelCallsTracer;
|
||||||
|
// Propagate the context from the thread which initiated the call to all callbacks.
|
||||||
|
this.context = Context.current();
|
||||||
|
this.unaryRequest =
|
||||||
|
method.getType() == MethodType.UNARY || method.getType() == MethodType.SERVER_STREAMING;
|
||||||
|
this.callOptions = callOptions;
|
||||||
|
this.clientStreamProvider = clientStreamProvider;
|
||||||
|
this.deadlineCancellationExecutor = deadlineCancellationExecutor;
|
||||||
|
PerfMark.event("ClientCall.<init>", tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class ContextCancellationListener implements CancellationListener {
|
||||||
|
@Override
|
||||||
|
public void cancelled(Context context) {
|
||||||
|
stream.cancel(statusFromCancelled(context));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Provider of {@link ClientStream}s. */
|
||||||
|
interface ClientStreamProvider {
|
||||||
|
ClientStream newStream(
|
||||||
|
MethodDescriptor<?, ?> method, CallOptions callOptions, Metadata headers, Context context);
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientCallImpl<ReqT, RespT> setFullStreamDecompression(boolean fullStreamDecompression) {
|
||||||
|
this.fullStreamDecompression = fullStreamDecompression;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientCallImpl<ReqT, RespT> setDecompressorRegistry(DecompressorRegistry decompressorRegistry) {
|
||||||
|
this.decompressorRegistry = decompressorRegistry;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientCallImpl<ReqT, RespT> setCompressorRegistry(CompressorRegistry compressorRegistry) {
|
||||||
|
this.compressorRegistry = compressorRegistry;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
static void prepareHeaders(
|
||||||
|
Metadata headers,
|
||||||
|
DecompressorRegistry decompressorRegistry,
|
||||||
|
Compressor compressor,
|
||||||
|
boolean fullStreamDecompression) {
|
||||||
|
headers.discardAll(MESSAGE_ENCODING_KEY);
|
||||||
|
if (compressor != Codec.Identity.NONE) {
|
||||||
|
headers.put(MESSAGE_ENCODING_KEY, compressor.getMessageEncoding());
|
||||||
|
}
|
||||||
|
|
||||||
|
headers.discardAll(MESSAGE_ACCEPT_ENCODING_KEY);
|
||||||
|
byte[] advertisedEncodings =
|
||||||
|
InternalDecompressorRegistry.getRawAdvertisedMessageEncodings(decompressorRegistry);
|
||||||
|
if (advertisedEncodings.length != 0) {
|
||||||
|
headers.put(MESSAGE_ACCEPT_ENCODING_KEY, advertisedEncodings);
|
||||||
|
}
|
||||||
|
|
||||||
|
headers.discardAll(CONTENT_ENCODING_KEY);
|
||||||
|
headers.discardAll(CONTENT_ACCEPT_ENCODING_KEY);
|
||||||
|
if (fullStreamDecompression) {
|
||||||
|
headers.put(CONTENT_ACCEPT_ENCODING_KEY, FULL_STREAM_DECOMPRESSION_ENCODINGS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start(Listener<RespT> observer, Metadata headers) {
|
||||||
|
PerfMark.startTask("ClientCall.start", tag);
|
||||||
|
try {
|
||||||
|
startInternal(observer, headers);
|
||||||
|
} finally {
|
||||||
|
PerfMark.stopTask("ClientCall.start", tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startInternal(Listener<RespT> observer, Metadata headers) {
|
||||||
|
checkState(stream == null, "Already started");
|
||||||
|
checkState(!cancelCalled, "call was cancelled");
|
||||||
|
checkNotNull(observer, "observer");
|
||||||
|
checkNotNull(headers, "headers");
|
||||||
|
|
||||||
|
if (context.isCancelled()) {
|
||||||
|
// Context is already cancelled so no need to create a real stream, just notify the observer
|
||||||
|
// of cancellation via callback on the executor
|
||||||
|
stream = NoopClientStream.INSTANCE;
|
||||||
|
final Listener<RespT> finalObserver = observer;
|
||||||
|
class ClosedByContext extends ContextRunnable {
|
||||||
|
ClosedByContext() {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void runInContext() {
|
||||||
|
closeObserver(finalObserver, statusFromCancelled(context), new Metadata());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callExecutor.execute(new ClosedByContext());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
applyMethodConfig();
|
||||||
|
final String compressorName = callOptions.getCompressor();
|
||||||
|
Compressor compressor;
|
||||||
|
if (compressorName != null) {
|
||||||
|
compressor = compressorRegistry.lookupCompressor(compressorName);
|
||||||
|
if (compressor == null) {
|
||||||
|
stream = NoopClientStream.INSTANCE;
|
||||||
|
final Listener<RespT> finalObserver = observer;
|
||||||
|
class ClosedByNotFoundCompressor extends ContextRunnable {
|
||||||
|
ClosedByNotFoundCompressor() {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void runInContext() {
|
||||||
|
closeObserver(
|
||||||
|
finalObserver,
|
||||||
|
Status.INTERNAL.withDescription(
|
||||||
|
String.format("Unable to find compressor by name %s", compressorName)),
|
||||||
|
new Metadata());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callExecutor.execute(new ClosedByNotFoundCompressor());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
compressor = Codec.Identity.NONE;
|
||||||
|
}
|
||||||
|
prepareHeaders(headers, decompressorRegistry, compressor, fullStreamDecompression);
|
||||||
|
|
||||||
|
Deadline effectiveDeadline = effectiveDeadline();
|
||||||
|
boolean deadlineExceeded = effectiveDeadline != null && effectiveDeadline.isExpired();
|
||||||
|
if (!deadlineExceeded) {
|
||||||
|
logIfContextNarrowedTimeout(
|
||||||
|
effectiveDeadline, context.getDeadline(), callOptions.getDeadline());
|
||||||
|
stream = clientStreamProvider.newStream(method, callOptions, headers, context);
|
||||||
|
} else {
|
||||||
|
stream =
|
||||||
|
new FailingClientStream(
|
||||||
|
DEADLINE_EXCEEDED.withDescription(
|
||||||
|
"ClientCall started after deadline exceeded: " + effectiveDeadline));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callExecutorIsDirect) {
|
||||||
|
stream.optimizeForDirectExecutor();
|
||||||
|
}
|
||||||
|
if (callOptions.getAuthority() != null) {
|
||||||
|
stream.setAuthority(callOptions.getAuthority());
|
||||||
|
}
|
||||||
|
if (callOptions.getMaxInboundMessageSize() != null) {
|
||||||
|
stream.setMaxInboundMessageSize(callOptions.getMaxInboundMessageSize());
|
||||||
|
}
|
||||||
|
if (callOptions.getMaxOutboundMessageSize() != null) {
|
||||||
|
stream.setMaxOutboundMessageSize(callOptions.getMaxOutboundMessageSize());
|
||||||
|
}
|
||||||
|
if (effectiveDeadline != null) {
|
||||||
|
stream.setDeadline(effectiveDeadline);
|
||||||
|
}
|
||||||
|
stream.setCompressor(compressor);
|
||||||
|
if (fullStreamDecompression) {
|
||||||
|
stream.setFullStreamDecompression(fullStreamDecompression);
|
||||||
|
}
|
||||||
|
stream.setDecompressorRegistry(decompressorRegistry);
|
||||||
|
channelCallsTracer.reportCallStarted();
|
||||||
|
stream.start(new ClientStreamListenerImpl(observer));
|
||||||
|
|
||||||
|
// Delay any sources of cancellation after start(), because most of the transports are broken if
|
||||||
|
// they receive cancel before start. Issue #1343 has more details
|
||||||
|
|
||||||
|
// Propagate later Context cancellation to the remote side.
|
||||||
|
context.addListener(cancellationListener, directExecutor());
|
||||||
|
if (effectiveDeadline != null
|
||||||
|
// If the context has the effective deadline, we don't need to schedule an extra task.
|
||||||
|
&& !effectiveDeadline.equals(context.getDeadline())
|
||||||
|
// If the channel has been terminated, we don't need to schedule an extra task.
|
||||||
|
&& deadlineCancellationExecutor != null) {
|
||||||
|
deadlineCancellationFuture = startDeadlineTimer(effectiveDeadline);
|
||||||
|
}
|
||||||
|
if (cancelListenersShouldBeRemoved) {
|
||||||
|
// Race detected! ClientStreamListener.closed may have been called before
|
||||||
|
// deadlineCancellationFuture was set / context listener added, thereby preventing the future
|
||||||
|
// and listener from being cancelled. Go ahead and cancel again, just to be sure it
|
||||||
|
// was cancelled.
|
||||||
|
removeContextListenerAndCancelDeadlineFuture();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyMethodConfig() {
|
||||||
|
MethodInfo info = callOptions.getOption(MethodInfo.KEY);
|
||||||
|
if (info == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (info.timeoutNanos != null) {
|
||||||
|
Deadline newDeadline = Deadline.after(info.timeoutNanos, TimeUnit.NANOSECONDS);
|
||||||
|
Deadline existingDeadline = callOptions.getDeadline();
|
||||||
|
// If the new deadline is sooner than the existing deadline, swap them.
|
||||||
|
if (existingDeadline == null || newDeadline.compareTo(existingDeadline) < 0) {
|
||||||
|
callOptions = callOptions.withDeadline(newDeadline);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (info.waitForReady != null) {
|
||||||
|
callOptions =
|
||||||
|
info.waitForReady ? callOptions.withWaitForReady() : callOptions.withoutWaitForReady();
|
||||||
|
}
|
||||||
|
if (info.maxInboundMessageSize != null) {
|
||||||
|
Integer existingLimit = callOptions.getMaxInboundMessageSize();
|
||||||
|
if (existingLimit != null) {
|
||||||
|
callOptions =
|
||||||
|
callOptions.withMaxInboundMessageSize(
|
||||||
|
Math.min(existingLimit, info.maxInboundMessageSize));
|
||||||
|
} else {
|
||||||
|
callOptions = callOptions.withMaxInboundMessageSize(info.maxInboundMessageSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (info.maxOutboundMessageSize != null) {
|
||||||
|
Integer existingLimit = callOptions.getMaxOutboundMessageSize();
|
||||||
|
if (existingLimit != null) {
|
||||||
|
callOptions =
|
||||||
|
callOptions.withMaxOutboundMessageSize(
|
||||||
|
Math.min(existingLimit, info.maxOutboundMessageSize));
|
||||||
|
} else {
|
||||||
|
callOptions = callOptions.withMaxOutboundMessageSize(info.maxOutboundMessageSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void logIfContextNarrowedTimeout(
|
||||||
|
Deadline effectiveDeadline,
|
||||||
|
@Nullable Deadline outerCallDeadline,
|
||||||
|
@Nullable Deadline callDeadline) {
|
||||||
|
if (!log.isLoggable(Level.FINE)
|
||||||
|
|| effectiveDeadline == null
|
||||||
|
|| !effectiveDeadline.equals(outerCallDeadline)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long effectiveTimeout = max(0, effectiveDeadline.timeRemaining(TimeUnit.NANOSECONDS));
|
||||||
|
StringBuilder builder =
|
||||||
|
new StringBuilder(
|
||||||
|
String.format(
|
||||||
|
"Call timeout set to '%d' ns, due to context deadline.", effectiveTimeout));
|
||||||
|
if (callDeadline == null) {
|
||||||
|
builder.append(" Explicit call timeout was not set.");
|
||||||
|
} else {
|
||||||
|
long callTimeout = callDeadline.timeRemaining(TimeUnit.NANOSECONDS);
|
||||||
|
builder.append(String.format(" Explicit call timeout was '%d' ns.", callTimeout));
|
||||||
|
}
|
||||||
|
|
||||||
|
log.fine(builder.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeContextListenerAndCancelDeadlineFuture() {
|
||||||
|
context.removeListener(cancellationListener);
|
||||||
|
ScheduledFuture<?> f = deadlineCancellationFuture;
|
||||||
|
if (f != null) {
|
||||||
|
f.cancel(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DeadlineTimer implements Runnable {
|
||||||
|
private final long remainingNanos;
|
||||||
|
|
||||||
|
DeadlineTimer(long remainingNanos) {
|
||||||
|
this.remainingNanos = remainingNanos;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
InsightBuilder insight = new InsightBuilder();
|
||||||
|
stream.appendTimeoutInsight(insight);
|
||||||
|
// DelayedStream.cancel() is safe to call from a thread that is different from where the
|
||||||
|
// stream is created.
|
||||||
|
long seconds = Math.abs(remainingNanos) / TimeUnit.SECONDS.toNanos(1);
|
||||||
|
long nanos = Math.abs(remainingNanos) % TimeUnit.SECONDS.toNanos(1);
|
||||||
|
|
||||||
|
StringBuilder buf = new StringBuilder();
|
||||||
|
buf.append("deadline exceeded after ");
|
||||||
|
if (remainingNanos < 0) {
|
||||||
|
buf.append('-');
|
||||||
|
}
|
||||||
|
buf.append(seconds);
|
||||||
|
buf.append(String.format(Locale.US, ".%09d", nanos));
|
||||||
|
buf.append("s. ");
|
||||||
|
buf.append(insight);
|
||||||
|
stream.cancel(DEADLINE_EXCEEDED.augmentDescription(buf.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ScheduledFuture<?> startDeadlineTimer(Deadline deadline) {
|
||||||
|
long remainingNanos = deadline.timeRemaining(TimeUnit.NANOSECONDS);
|
||||||
|
return deadlineCancellationExecutor.schedule(
|
||||||
|
new LogExceptionRunnable(new DeadlineTimer(remainingNanos)),
|
||||||
|
remainingNanos,
|
||||||
|
TimeUnit.NANOSECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private Deadline effectiveDeadline() {
|
||||||
|
// Call options and context are immutable, so we don't need to cache the deadline.
|
||||||
|
return min(callOptions.getDeadline(), context.getDeadline());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static Deadline min(@Nullable Deadline deadline0, @Nullable Deadline deadline1) {
|
||||||
|
if (deadline0 == null) {
|
||||||
|
return deadline1;
|
||||||
|
}
|
||||||
|
if (deadline1 == null) {
|
||||||
|
return deadline0;
|
||||||
|
}
|
||||||
|
return deadline0.minimum(deadline1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void request(int numMessages) {
|
||||||
|
PerfMark.startTask("ClientCall.request", tag);
|
||||||
|
try {
|
||||||
|
checkState(stream != null, "Not started");
|
||||||
|
checkArgument(numMessages >= 0, "Number requested must be non-negative");
|
||||||
|
stream.request(numMessages);
|
||||||
|
} finally {
|
||||||
|
PerfMark.stopTask("ClientCall.request", tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cancel(@Nullable String message, @Nullable Throwable cause) {
|
||||||
|
PerfMark.startTask("ClientCall.cancel", tag);
|
||||||
|
try {
|
||||||
|
cancelInternal(message, cause);
|
||||||
|
} finally {
|
||||||
|
PerfMark.stopTask("ClientCall.cancel", tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cancelInternal(@Nullable String message, @Nullable Throwable cause) {
|
||||||
|
if (message == null && cause == null) {
|
||||||
|
cause = new CancellationException("Cancelled without a message or cause");
|
||||||
|
log.log(Level.WARNING, "Cancelling without a message or cause is suboptimal", cause);
|
||||||
|
}
|
||||||
|
if (cancelCalled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cancelCalled = true;
|
||||||
|
try {
|
||||||
|
// Cancel is called in exception handling cases, so it may be the case that the
|
||||||
|
// stream was never successfully created or start has never been called.
|
||||||
|
if (stream != null) {
|
||||||
|
Status status = Status.CANCELLED;
|
||||||
|
if (message != null) {
|
||||||
|
status = status.withDescription(message);
|
||||||
|
} else {
|
||||||
|
status = status.withDescription("Call cancelled without message");
|
||||||
|
}
|
||||||
|
if (cause != null) {
|
||||||
|
status = status.withCause(cause);
|
||||||
|
}
|
||||||
|
stream.cancel(status);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
removeContextListenerAndCancelDeadlineFuture();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void halfClose() {
|
||||||
|
PerfMark.startTask("ClientCall.halfClose", tag);
|
||||||
|
try {
|
||||||
|
halfCloseInternal();
|
||||||
|
} finally {
|
||||||
|
PerfMark.stopTask("ClientCall.halfClose", tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void halfCloseInternal() {
|
||||||
|
checkState(stream != null, "Not started");
|
||||||
|
checkState(!cancelCalled, "call was cancelled");
|
||||||
|
checkState(!halfCloseCalled, "call already half-closed");
|
||||||
|
halfCloseCalled = true;
|
||||||
|
stream.halfClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendMessage(ReqT message) {
|
||||||
|
PerfMark.startTask("ClientCall.sendMessage", tag);
|
||||||
|
try {
|
||||||
|
sendMessageInternal(message);
|
||||||
|
} finally {
|
||||||
|
PerfMark.stopTask("ClientCall.sendMessage", tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendMessageInternal(ReqT message) {
|
||||||
|
checkState(stream != null, "Not started");
|
||||||
|
checkState(!cancelCalled, "call was cancelled");
|
||||||
|
checkState(!halfCloseCalled, "call was half-closed");
|
||||||
|
try {
|
||||||
|
if (stream instanceof RetriableStream) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
RetriableStream<ReqT> retriableStream = (RetriableStream<ReqT>) stream;
|
||||||
|
retriableStream.sendMessage(message);
|
||||||
|
} else {
|
||||||
|
stream.writeMessage(method.streamRequest(message));
|
||||||
|
}
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
stream.cancel(Status.CANCELLED.withCause(e).withDescription("Failed to stream message"));
|
||||||
|
return;
|
||||||
|
} catch (Error e) {
|
||||||
|
stream.cancel(Status.CANCELLED.withDescription("Client sendMessage() failed with Error"));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
// For unary requests, we don't flush since we know that halfClose should be coming soon. This
|
||||||
|
// allows us to piggy-back the END_STREAM=true on the last message frame without opening the
|
||||||
|
// possibility of broken applications forgetting to call halfClose without noticing.
|
||||||
|
if (!unaryRequest) {
|
||||||
|
stream.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setMessageCompression(boolean enabled) {
|
||||||
|
checkState(stream != null, "Not started");
|
||||||
|
stream.setMessageCompression(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isReady() {
|
||||||
|
return stream.isReady();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Attributes getAttributes() {
|
||||||
|
if (stream != null) {
|
||||||
|
return stream.getAttributes();
|
||||||
|
}
|
||||||
|
return Attributes.EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closeObserver(Listener<RespT> observer, Status status, Metadata trailers) {
|
||||||
|
observer.onClose(status, trailers);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return MoreObjects.toStringHelper(this).add("method", method).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ClientStreamListenerImpl implements ClientStreamListener {
|
||||||
|
private final Listener<RespT> observer;
|
||||||
|
private Status exceptionStatus;
|
||||||
|
|
||||||
|
public ClientStreamListenerImpl(Listener<RespT> observer) {
|
||||||
|
this.observer = checkNotNull(observer, "observer");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels call and schedules onClose() notification. May only be called from the application
|
||||||
|
* thread.
|
||||||
|
*/
|
||||||
|
private void exceptionThrown(Status status) {
|
||||||
|
// Since each RPC can have its own executor, we can only call onClose() when we are sure there
|
||||||
|
// will be no further callbacks. We set the status here and overwrite the onClose() details
|
||||||
|
// when it arrives.
|
||||||
|
exceptionStatus = status;
|
||||||
|
stream.cancel(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void headersRead(final Metadata headers) {
|
||||||
|
PerfMark.startTask("ClientStreamListener.headersRead", tag);
|
||||||
|
final Link link = PerfMark.linkOut();
|
||||||
|
|
||||||
|
final class HeadersRead extends ContextRunnable {
|
||||||
|
HeadersRead() {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void runInContext() {
|
||||||
|
PerfMark.startTask("ClientCall$Listener.headersRead", tag);
|
||||||
|
PerfMark.linkIn(link);
|
||||||
|
try {
|
||||||
|
runInternal();
|
||||||
|
} finally {
|
||||||
|
PerfMark.stopTask("ClientCall$Listener.headersRead", tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runInternal() {
|
||||||
|
if (exceptionStatus != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
observer.onHeaders(headers);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
exceptionThrown(
|
||||||
|
Status.CANCELLED.withCause(t).withDescription("Failed to read headers"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
callExecutor.execute(new HeadersRead());
|
||||||
|
} finally {
|
||||||
|
PerfMark.stopTask("ClientStreamListener.headersRead", tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void messagesAvailable(final MessageProducer producer) {
|
||||||
|
PerfMark.startTask("ClientStreamListener.messagesAvailable", tag);
|
||||||
|
final Link link = PerfMark.linkOut();
|
||||||
|
|
||||||
|
final class MessagesAvailable extends ContextRunnable {
|
||||||
|
MessagesAvailable() {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void runInContext() {
|
||||||
|
PerfMark.startTask("ClientCall$Listener.messagesAvailable", tag);
|
||||||
|
PerfMark.linkIn(link);
|
||||||
|
try {
|
||||||
|
runInternal();
|
||||||
|
} finally {
|
||||||
|
PerfMark.stopTask("ClientCall$Listener.messagesAvailable", tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runInternal() {
|
||||||
|
if (exceptionStatus != null) {
|
||||||
|
GrpcUtil.closeQuietly(producer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
InputStream message;
|
||||||
|
while ((message = producer.next()) != null) {
|
||||||
|
try {
|
||||||
|
observer.onMessage(method.parseResponse(message));
|
||||||
|
} catch (Throwable t) {
|
||||||
|
GrpcUtil.closeQuietly(message);
|
||||||
|
throw t;
|
||||||
|
}
|
||||||
|
message.close();
|
||||||
|
}
|
||||||
|
} catch (Throwable t) {
|
||||||
|
GrpcUtil.closeQuietly(producer);
|
||||||
|
exceptionThrown(
|
||||||
|
Status.CANCELLED.withCause(t).withDescription("Failed to read message."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
callExecutor.execute(new MessagesAvailable());
|
||||||
|
} finally {
|
||||||
|
PerfMark.stopTask("ClientStreamListener.messagesAvailable", tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void closed(Status status, Metadata trailers) {
|
||||||
|
closed(status, RpcProgress.PROCESSED, trailers);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void closed(Status status, RpcProgress rpcProgress, Metadata trailers) {
|
||||||
|
PerfMark.startTask("ClientStreamListener.closed", tag);
|
||||||
|
try {
|
||||||
|
closedInternal(status, rpcProgress, trailers);
|
||||||
|
} finally {
|
||||||
|
PerfMark.stopTask("ClientStreamListener.closed", tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closedInternal(
|
||||||
|
Status status, @SuppressWarnings("unused") RpcProgress rpcProgress, Metadata trailers) {
|
||||||
|
Deadline deadline = effectiveDeadline();
|
||||||
|
if (status.getCode() == Status.Code.CANCELLED && deadline != null) {
|
||||||
|
// When the server's deadline expires, it can only reset the stream with CANCEL and no
|
||||||
|
// description. Since our timer may be delayed in firing, we double-check the deadline and
|
||||||
|
// turn the failure into the likely more helpful DEADLINE_EXCEEDED status.
|
||||||
|
if (deadline.isExpired()) {
|
||||||
|
InsightBuilder insight = new InsightBuilder();
|
||||||
|
stream.appendTimeoutInsight(insight);
|
||||||
|
status =
|
||||||
|
DEADLINE_EXCEEDED.augmentDescription(
|
||||||
|
"ClientCall was cancelled at or after deadline. " + insight);
|
||||||
|
// Replace trailers to prevent mixing sources of status and trailers.
|
||||||
|
trailers = new Metadata();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final Status savedStatus = status;
|
||||||
|
final Metadata savedTrailers = trailers;
|
||||||
|
final Link link = PerfMark.linkOut();
|
||||||
|
final class StreamClosed extends ContextRunnable {
|
||||||
|
StreamClosed() {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void runInContext() {
|
||||||
|
PerfMark.startTask("ClientCall$Listener.onClose", tag);
|
||||||
|
PerfMark.linkIn(link);
|
||||||
|
try {
|
||||||
|
runInternal();
|
||||||
|
} finally {
|
||||||
|
PerfMark.stopTask("ClientCall$Listener.onClose", tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runInternal() {
|
||||||
|
Status status = savedStatus;
|
||||||
|
Metadata trailers = savedTrailers;
|
||||||
|
if (exceptionStatus != null) {
|
||||||
|
// Ideally exceptionStatus == savedStatus, as exceptionStatus was passed to cancel().
|
||||||
|
// However the cancel is racy and this closed() may have already been queued when the
|
||||||
|
// cancellation occurred. Since other calls like onMessage() will throw away data if
|
||||||
|
// exceptionStatus != null, it is semantically essential that we _not_ use a status
|
||||||
|
// provided by the server.
|
||||||
|
status = exceptionStatus;
|
||||||
|
// Replace trailers to prevent mixing sources of status and trailers.
|
||||||
|
trailers = new Metadata();
|
||||||
|
}
|
||||||
|
cancelListenersShouldBeRemoved = true;
|
||||||
|
try {
|
||||||
|
closeObserver(observer, status, trailers);
|
||||||
|
} finally {
|
||||||
|
removeContextListenerAndCancelDeadlineFuture();
|
||||||
|
channelCallsTracer.reportCallEnded(status.isOk());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callExecutor.execute(new StreamClosed());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReady() {
|
||||||
|
if (method.getType().clientSendsOneMessage()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PerfMark.startTask("ClientStreamListener.onReady", tag);
|
||||||
|
final Link link = PerfMark.linkOut();
|
||||||
|
|
||||||
|
final class StreamOnReady extends ContextRunnable {
|
||||||
|
StreamOnReady() {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void runInContext() {
|
||||||
|
PerfMark.startTask("ClientCall$Listener.onReady", tag);
|
||||||
|
PerfMark.linkIn(link);
|
||||||
|
try {
|
||||||
|
runInternal();
|
||||||
|
} finally {
|
||||||
|
PerfMark.stopTask("ClientCall$Listener.onReady", tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runInternal() {
|
||||||
|
if (exceptionStatus != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
observer.onReady();
|
||||||
|
} catch (Throwable t) {
|
||||||
|
exceptionThrown(
|
||||||
|
Status.CANCELLED.withCause(t).withDescription("Failed to call onReady."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
callExecutor.execute(new StreamOnReady());
|
||||||
|
} finally {
|
||||||
|
PerfMark.stopTask("ClientStreamListener.onReady", tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,774 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2014 The gRPC Authors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.grpc.netty;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkArgument;
|
||||||
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
import static com.google.common.base.Preconditions.checkState;
|
||||||
|
import static io.grpc.internal.GrpcUtil.DEFAULT_KEEPALIVE_TIMEOUT_NANOS;
|
||||||
|
import static io.grpc.internal.GrpcUtil.KEEPALIVE_TIME_NANOS_DISABLED;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||||
|
import io.grpc.Attributes;
|
||||||
|
import io.grpc.CallCredentials;
|
||||||
|
import io.grpc.ChannelCredentials;
|
||||||
|
import io.grpc.ChannelLogger;
|
||||||
|
import io.grpc.EquivalentAddressGroup;
|
||||||
|
import io.grpc.ExperimentalApi;
|
||||||
|
import io.grpc.HttpConnectProxiedSocketAddress;
|
||||||
|
import io.grpc.Internal;
|
||||||
|
import io.grpc.ManagedChannelBuilder;
|
||||||
|
import io.grpc.internal.AbstractManagedChannelImplBuilder;
|
||||||
|
import io.grpc.internal.AtomicBackoff;
|
||||||
|
import io.grpc.internal.ClientTransportFactory;
|
||||||
|
import io.grpc.internal.ConnectionClientTransport;
|
||||||
|
import io.grpc.internal.FixedObjectPool;
|
||||||
|
import io.grpc.internal.GrpcUtil;
|
||||||
|
import io.grpc.internal.KeepAliveManager;
|
||||||
|
import io.grpc.internal.ManagedChannelImplBuilder;
|
||||||
|
import io.grpc.internal.ManagedChannelImplBuilder.ChannelBuilderDefaultPortProvider;
|
||||||
|
import io.grpc.internal.ManagedChannelImplBuilder.ClientTransportFactoryBuilder;
|
||||||
|
import io.grpc.internal.ObjectPool;
|
||||||
|
import io.grpc.internal.SharedResourcePool;
|
||||||
|
import io.grpc.internal.TransportTracer;
|
||||||
|
import io.grpc.netty.ProtocolNegotiators.FromChannelCredentialsResult;
|
||||||
|
import io.netty.channel.Channel;
|
||||||
|
import io.netty.channel.ChannelFactory;
|
||||||
|
import io.netty.channel.ChannelOption;
|
||||||
|
import io.netty.channel.EventLoopGroup;
|
||||||
|
import io.netty.channel.ReflectiveChannelFactory;
|
||||||
|
import io.netty.channel.socket.nio.NioSocketChannel;
|
||||||
|
import io.netty.handler.ssl.SslContext;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.SocketAddress;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import javax.annotation.CheckReturnValue;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import javax.net.ssl.SSLException;
|
||||||
|
|
||||||
|
/** A builder to help simplify construction of channels using the Netty transport. */
|
||||||
|
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/1784")
|
||||||
|
@CanIgnoreReturnValue
|
||||||
|
public final class NettyChannelBuilder
|
||||||
|
extends AbstractManagedChannelImplBuilder<NettyChannelBuilder> {
|
||||||
|
|
||||||
|
// 1MiB.
|
||||||
|
public static final int DEFAULT_FLOW_CONTROL_WINDOW = 1024 * 1024;
|
||||||
|
private static final boolean DEFAULT_AUTO_FLOW_CONTROL;
|
||||||
|
|
||||||
|
private static final long AS_LARGE_AS_INFINITE = TimeUnit.DAYS.toNanos(1000L);
|
||||||
|
|
||||||
|
private static final ChannelFactory<? extends Channel> DEFAULT_CHANNEL_FACTORY =
|
||||||
|
new ReflectiveChannelFactory<>(Utils.DEFAULT_CLIENT_CHANNEL_TYPE);
|
||||||
|
private static final ObjectPool<? extends EventLoopGroup> DEFAULT_EVENT_LOOP_GROUP_POOL =
|
||||||
|
SharedResourcePool.forResource(Utils.DEFAULT_WORKER_EVENT_LOOP_GROUP);
|
||||||
|
|
||||||
|
static {
|
||||||
|
String autoFlowControl = System.getenv("GRPC_EXPERIMENTAL_AUTOFLOWCONTROL");
|
||||||
|
if (autoFlowControl == null) {
|
||||||
|
autoFlowControl = "true";
|
||||||
|
}
|
||||||
|
DEFAULT_AUTO_FLOW_CONTROL = Boolean.parseBoolean(autoFlowControl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ManagedChannelImplBuilder managedChannelImplBuilder;
|
||||||
|
private TransportTracer.Factory transportTracerFactory = TransportTracer.getDefaultFactory();
|
||||||
|
private final Map<ChannelOption<?>, Object> channelOptions = new HashMap<>();
|
||||||
|
private ChannelFactory<? extends Channel> channelFactory = DEFAULT_CHANNEL_FACTORY;
|
||||||
|
private ObjectPool<? extends EventLoopGroup> eventLoopGroupPool = DEFAULT_EVENT_LOOP_GROUP_POOL;
|
||||||
|
private boolean autoFlowControl = DEFAULT_AUTO_FLOW_CONTROL;
|
||||||
|
private int flowControlWindow = DEFAULT_FLOW_CONTROL_WINDOW;
|
||||||
|
private int maxInboundMessageSize = GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE;
|
||||||
|
private int maxHeaderListSize = GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE;
|
||||||
|
private long keepAliveTimeNanos = KEEPALIVE_TIME_NANOS_DISABLED;
|
||||||
|
private long keepAliveTimeoutNanos = DEFAULT_KEEPALIVE_TIMEOUT_NANOS;
|
||||||
|
private boolean keepAliveWithoutCalls;
|
||||||
|
private ProtocolNegotiator.ClientFactory protocolNegotiatorFactory =
|
||||||
|
new DefaultProtocolNegotiator();
|
||||||
|
private final boolean freezeProtocolNegotiatorFactory;
|
||||||
|
private LocalSocketPicker localSocketPicker;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true, indicates that the transport may use the GET method for RPCs, and may include the
|
||||||
|
* request body in the query params.
|
||||||
|
*/
|
||||||
|
private final boolean useGetForSafeMethods = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new builder with the given server address. This factory method is primarily intended
|
||||||
|
* for using Netty Channel types other than SocketChannel. {@link #forAddress(String, int)} should
|
||||||
|
* generally be preferred over this method, since that API permits delaying DNS lookups and
|
||||||
|
* noticing changes to DNS. If an unresolved InetSocketAddress is passed in, then it will remain
|
||||||
|
* unresolved.
|
||||||
|
*/
|
||||||
|
@CheckReturnValue
|
||||||
|
public static NettyChannelBuilder forAddress(SocketAddress serverAddress) {
|
||||||
|
return new NettyChannelBuilder(serverAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates a new builder with the given host and port. */
|
||||||
|
@CheckReturnValue
|
||||||
|
public static NettyChannelBuilder forAddress(String host, int port) {
|
||||||
|
return forTarget(GrpcUtil.authorityFromHostAndPort(host, port));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates a new builder with the given host and port. */
|
||||||
|
@CheckReturnValue
|
||||||
|
public static NettyChannelBuilder forAddress(String host, int port, ChannelCredentials creds) {
|
||||||
|
return forTarget(GrpcUtil.authorityFromHostAndPort(host, port), creds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new builder with the given target string that will be resolved by {@link
|
||||||
|
* io.grpc.NameResolver}.
|
||||||
|
*/
|
||||||
|
@CheckReturnValue
|
||||||
|
public static NettyChannelBuilder forTarget(String target) {
|
||||||
|
return new NettyChannelBuilder(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new builder with the given target string that will be resolved by {@link
|
||||||
|
* io.grpc.NameResolver}.
|
||||||
|
*/
|
||||||
|
@CheckReturnValue
|
||||||
|
public static NettyChannelBuilder forTarget(String target, ChannelCredentials creds) {
|
||||||
|
FromChannelCredentialsResult result = ProtocolNegotiators.from(creds);
|
||||||
|
if (result.error != null) {
|
||||||
|
throw new IllegalArgumentException(result.error);
|
||||||
|
}
|
||||||
|
return new NettyChannelBuilder(target, creds, result.callCredentials, result.negotiator);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class NettyChannelTransportFactoryBuilder implements ClientTransportFactoryBuilder {
|
||||||
|
@Override
|
||||||
|
public ClientTransportFactory buildClientTransportFactory() {
|
||||||
|
return buildTransportFactory();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class NettyChannelDefaultPortProvider implements ChannelBuilderDefaultPortProvider {
|
||||||
|
@Override
|
||||||
|
public int getDefaultPort() {
|
||||||
|
return protocolNegotiatorFactory.getDefaultPort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@CheckReturnValue
|
||||||
|
NettyChannelBuilder(String target) {
|
||||||
|
managedChannelImplBuilder =
|
||||||
|
new ManagedChannelImplBuilder(
|
||||||
|
target,
|
||||||
|
new NettyChannelTransportFactoryBuilder(),
|
||||||
|
new NettyChannelDefaultPortProvider());
|
||||||
|
this.freezeProtocolNegotiatorFactory = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
NettyChannelBuilder(
|
||||||
|
String target,
|
||||||
|
ChannelCredentials channelCreds,
|
||||||
|
CallCredentials callCreds,
|
||||||
|
ProtocolNegotiator.ClientFactory negotiator) {
|
||||||
|
managedChannelImplBuilder =
|
||||||
|
new ManagedChannelImplBuilder(
|
||||||
|
target,
|
||||||
|
channelCreds,
|
||||||
|
callCreds,
|
||||||
|
new NettyChannelTransportFactoryBuilder(),
|
||||||
|
new NettyChannelDefaultPortProvider());
|
||||||
|
this.protocolNegotiatorFactory = checkNotNull(negotiator, "negotiator");
|
||||||
|
this.freezeProtocolNegotiatorFactory = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@CheckReturnValue
|
||||||
|
NettyChannelBuilder(SocketAddress address) {
|
||||||
|
managedChannelImplBuilder =
|
||||||
|
new ManagedChannelImplBuilder(
|
||||||
|
address,
|
||||||
|
getAuthorityFromAddress(address),
|
||||||
|
new NettyChannelTransportFactoryBuilder(),
|
||||||
|
new NettyChannelDefaultPortProvider());
|
||||||
|
this.freezeProtocolNegotiatorFactory = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Internal
|
||||||
|
@Override
|
||||||
|
protected ManagedChannelBuilder<?> delegate() {
|
||||||
|
return managedChannelImplBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@CheckReturnValue
|
||||||
|
private static String getAuthorityFromAddress(SocketAddress address) {
|
||||||
|
if (address instanceof InetSocketAddress) {
|
||||||
|
InetSocketAddress inetAddress = (InetSocketAddress) address;
|
||||||
|
return GrpcUtil.authorityFromHostAndPort(inetAddress.getHostString(), inetAddress.getPort());
|
||||||
|
} else {
|
||||||
|
return address.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the channel type to use, by default we use {@code EpollSocketChannel} if available,
|
||||||
|
* otherwise using {@link NioSocketChannel}.
|
||||||
|
*
|
||||||
|
* <p>You either use this or {@link #channelFactory(io.netty.channel.ChannelFactory)} if your
|
||||||
|
* {@link Channel} implementation has no no-args constructor.
|
||||||
|
*
|
||||||
|
* <p>It's an optional parameter. If the user has not provided an Channel type or ChannelFactory
|
||||||
|
* when the channel is built, the builder will use the default one which is static.
|
||||||
|
*
|
||||||
|
* <p>You must also provide corresponding {@link #eventLoopGroup(EventLoopGroup)}. For example,
|
||||||
|
* {@link NioSocketChannel} must use {@link io.netty.channel.nio.NioEventLoopGroup}, otherwise
|
||||||
|
* your application won't start.
|
||||||
|
*/
|
||||||
|
public NettyChannelBuilder channelType(Class<? extends Channel> channelType) {
|
||||||
|
checkNotNull(channelType, "channelType");
|
||||||
|
return channelFactory(new ReflectiveChannelFactory<>(channelType));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the {@link ChannelFactory} to create {@link Channel} instances. This method is
|
||||||
|
* usually only used if the specific {@code Channel} requires complex logic which requires
|
||||||
|
* additional information to create the {@code Channel}. Otherwise, recommend to use {@link
|
||||||
|
* #channelType(Class)}.
|
||||||
|
*
|
||||||
|
* <p>It's an optional parameter. If the user has not provided an Channel type or ChannelFactory
|
||||||
|
* when the channel is built, the builder will use the default one which is static.
|
||||||
|
*
|
||||||
|
* <p>You must also provide corresponding {@link #eventLoopGroup(EventLoopGroup)}. For example,
|
||||||
|
* {@link NioSocketChannel} based {@link ChannelFactory} must use {@link
|
||||||
|
* io.netty.channel.nio.NioEventLoopGroup}, otherwise your application won't start.
|
||||||
|
*/
|
||||||
|
public NettyChannelBuilder channelFactory(ChannelFactory<? extends Channel> channelFactory) {
|
||||||
|
this.channelFactory = checkNotNull(channelFactory, "channelFactory");
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies a channel option. As the underlying channel as well as network implementation may
|
||||||
|
* ignore this value applications should consider it a hint.
|
||||||
|
*/
|
||||||
|
public <T> NettyChannelBuilder withOption(ChannelOption<T> option, T value) {
|
||||||
|
channelOptions.put(option, value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the negotiation type for the HTTP/2 connection.
|
||||||
|
*
|
||||||
|
* <p>Default: <code>TLS</code>
|
||||||
|
*/
|
||||||
|
public NettyChannelBuilder negotiationType(NegotiationType type) {
|
||||||
|
checkState(
|
||||||
|
!freezeProtocolNegotiatorFactory, "Cannot change security when using ChannelCredentials");
|
||||||
|
if (!(protocolNegotiatorFactory instanceof DefaultProtocolNegotiator)) {
|
||||||
|
// Do nothing for compatibility
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
((DefaultProtocolNegotiator) protocolNegotiatorFactory).negotiationType = type;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides an EventGroupLoop to be used by the netty transport.
|
||||||
|
*
|
||||||
|
* <p>It's an optional parameter. If the user has not provided an EventGroupLoop when the channel
|
||||||
|
* is built, the builder will use the default one which is static.
|
||||||
|
*
|
||||||
|
* <p>You must also provide corresponding {@link #channelType(Class)} or {@link
|
||||||
|
* #channelFactory(ChannelFactory)} corresponding to the given {@code EventLoopGroup}. For
|
||||||
|
* example, {@link io.netty.channel.nio.NioEventLoopGroup} requires {@link NioSocketChannel}
|
||||||
|
*
|
||||||
|
* <p>The channel won't take ownership of the given EventLoopGroup. It's caller's responsibility
|
||||||
|
* to shut it down when it's desired.
|
||||||
|
*/
|
||||||
|
public NettyChannelBuilder eventLoopGroup(@Nullable EventLoopGroup eventLoopGroup) {
|
||||||
|
if (eventLoopGroup != null) {
|
||||||
|
return eventLoopGroupPool(new FixedObjectPool<>(eventLoopGroup));
|
||||||
|
}
|
||||||
|
return eventLoopGroupPool(DEFAULT_EVENT_LOOP_GROUP_POOL);
|
||||||
|
}
|
||||||
|
|
||||||
|
NettyChannelBuilder eventLoopGroupPool(ObjectPool<? extends EventLoopGroup> eventLoopGroupPool) {
|
||||||
|
this.eventLoopGroupPool = checkNotNull(eventLoopGroupPool, "eventLoopGroupPool");
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSL/TLS context to use instead of the system default. It must have been configured with {@link
|
||||||
|
* GrpcSslContexts}, but options could have been overridden.
|
||||||
|
*/
|
||||||
|
public NettyChannelBuilder sslContext(SslContext sslContext) {
|
||||||
|
checkState(
|
||||||
|
!freezeProtocolNegotiatorFactory, "Cannot change security when using ChannelCredentials");
|
||||||
|
if (sslContext != null) {
|
||||||
|
checkArgument(sslContext.isClient(), "Server SSL context can not be used for client channel");
|
||||||
|
GrpcSslContexts.ensureAlpnAndH2Enabled(sslContext.applicationProtocolNegotiator());
|
||||||
|
}
|
||||||
|
if (!(protocolNegotiatorFactory instanceof DefaultProtocolNegotiator)) {
|
||||||
|
// Do nothing for compatibility
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
((DefaultProtocolNegotiator) protocolNegotiatorFactory).sslContext = sslContext;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the initial flow control window in bytes. Setting initial flow control window enables auto
|
||||||
|
* flow control tuning using bandwidth-delay product algorithm. To disable auto flow control
|
||||||
|
* tuning, use {@link #flowControlWindow(int)}. By default, auto flow control is enabled with
|
||||||
|
* initial flow control window size of {@link #DEFAULT_FLOW_CONTROL_WINDOW}.
|
||||||
|
*/
|
||||||
|
public NettyChannelBuilder initialFlowControlWindow(int initialFlowControlWindow) {
|
||||||
|
checkArgument(initialFlowControlWindow > 0, "initialFlowControlWindow must be positive");
|
||||||
|
this.flowControlWindow = initialFlowControlWindow;
|
||||||
|
this.autoFlowControl = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the flow control window in bytes. Setting flowControlWindow disables auto flow control
|
||||||
|
* tuning; use {@link #initialFlowControlWindow(int)} to enable auto flow control tuning. If not
|
||||||
|
* called, the default value is {@link #DEFAULT_FLOW_CONTROL_WINDOW}) with auto flow control
|
||||||
|
* tuning.
|
||||||
|
*/
|
||||||
|
public NettyChannelBuilder flowControlWindow(int flowControlWindow) {
|
||||||
|
checkArgument(flowControlWindow > 0, "flowControlWindow must be positive");
|
||||||
|
this.flowControlWindow = flowControlWindow;
|
||||||
|
this.autoFlowControl = false;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the maximum size of header list allowed to be received. This is cumulative size of the
|
||||||
|
* headers with some overhead, as defined for <a
|
||||||
|
* href="http://httpwg.org/specs/rfc7540.html#rfc.section.6.5.2">HTTP/2's
|
||||||
|
* SETTINGS_MAX_HEADER_LIST_SIZE</a>. The default is 8 KiB.
|
||||||
|
*
|
||||||
|
* @deprecated Use {@link #maxInboundMetadataSize} instead
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
public NettyChannelBuilder maxHeaderListSize(int maxHeaderListSize) {
|
||||||
|
return maxInboundMetadataSize(maxHeaderListSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the maximum size of metadata allowed to be received. This is cumulative size of the
|
||||||
|
* entries with some overhead, as defined for <a
|
||||||
|
* href="http://httpwg.org/specs/rfc7540.html#rfc.section.6.5.2">HTTP/2's
|
||||||
|
* SETTINGS_MAX_HEADER_LIST_SIZE</a>. The default is 8 KiB.
|
||||||
|
*
|
||||||
|
* @param bytes the maximum size of received metadata
|
||||||
|
* @return this
|
||||||
|
* @throws IllegalArgumentException if bytes is non-positive
|
||||||
|
* @since 1.17.0
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public NettyChannelBuilder maxInboundMetadataSize(int bytes) {
|
||||||
|
checkArgument(bytes > 0, "maxInboundMetadataSize must be > 0");
|
||||||
|
this.maxHeaderListSize = bytes;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Equivalent to using {@link #negotiationType(NegotiationType)} with {@code PLAINTEXT}. */
|
||||||
|
@Override
|
||||||
|
public NettyChannelBuilder usePlaintext() {
|
||||||
|
negotiationType(NegotiationType.PLAINTEXT);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Equivalent to using {@link #negotiationType(NegotiationType)} with {@code TLS}. */
|
||||||
|
@Override
|
||||||
|
public NettyChannelBuilder useTransportSecurity() {
|
||||||
|
negotiationType(NegotiationType.TLS);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* @since 1.3.0
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public NettyChannelBuilder keepAliveTime(long keepAliveTime, TimeUnit timeUnit) {
|
||||||
|
checkArgument(keepAliveTime > 0L, "keepalive time must be positive");
|
||||||
|
keepAliveTimeNanos = timeUnit.toNanos(keepAliveTime);
|
||||||
|
keepAliveTimeNanos = KeepAliveManager.clampKeepAliveTimeInNanos(keepAliveTimeNanos);
|
||||||
|
if (keepAliveTimeNanos >= AS_LARGE_AS_INFINITE) {
|
||||||
|
// Bump keepalive time to infinite. This disables keepalive.
|
||||||
|
keepAliveTimeNanos = KEEPALIVE_TIME_NANOS_DISABLED;
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* @since 1.3.0
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public NettyChannelBuilder keepAliveTimeout(long keepAliveTimeout, TimeUnit timeUnit) {
|
||||||
|
checkArgument(keepAliveTimeout > 0L, "keepalive timeout must be positive");
|
||||||
|
keepAliveTimeoutNanos = timeUnit.toNanos(keepAliveTimeout);
|
||||||
|
keepAliveTimeoutNanos = KeepAliveManager.clampKeepAliveTimeoutInNanos(keepAliveTimeoutNanos);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* @since 1.3.0
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public NettyChannelBuilder keepAliveWithoutCalls(boolean enable) {
|
||||||
|
keepAliveWithoutCalls = enable;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** If non-{@code null}, attempts to create connections bound to a local port. */
|
||||||
|
public NettyChannelBuilder localSocketPicker(@Nullable LocalSocketPicker localSocketPicker) {
|
||||||
|
this.localSocketPicker = localSocketPicker;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is meant to be overriden with a custom implementation of {@link
|
||||||
|
* #createSocketAddress}. The default implementation is a no-op.
|
||||||
|
*
|
||||||
|
* @since 1.16.0
|
||||||
|
*/
|
||||||
|
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/4917")
|
||||||
|
public static class LocalSocketPicker {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by gRPC to pick local socket to bind to. This may be called multiple times. Subclasses
|
||||||
|
* are expected to override this method.
|
||||||
|
*
|
||||||
|
* @param remoteAddress the remote address to connect to.
|
||||||
|
* @param attrs the Attributes present on the {@link io.grpc.EquivalentAddressGroup} associated
|
||||||
|
* with the address.
|
||||||
|
* @return a {@link SocketAddress} suitable for binding, or else {@code null}.
|
||||||
|
* @since 1.16.0
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public SocketAddress createSocketAddress(
|
||||||
|
SocketAddress remoteAddress, @EquivalentAddressGroup.Attr Attributes attrs) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the maximum message size allowed for a single gRPC frame. If an inbound messages larger
|
||||||
|
* than this limit is received it will not be processed and the RPC will fail with
|
||||||
|
* RESOURCE_EXHAUSTED.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public NettyChannelBuilder maxInboundMessageSize(int max) {
|
||||||
|
checkArgument(max >= 0, "negative max");
|
||||||
|
maxInboundMessageSize = max;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@CheckReturnValue
|
||||||
|
ClientTransportFactory buildTransportFactory() {
|
||||||
|
assertEventLoopAndChannelType();
|
||||||
|
|
||||||
|
ProtocolNegotiator negotiator = protocolNegotiatorFactory.newNegotiator();
|
||||||
|
return new NettyTransportFactory(
|
||||||
|
negotiator,
|
||||||
|
channelFactory,
|
||||||
|
channelOptions,
|
||||||
|
eventLoopGroupPool,
|
||||||
|
autoFlowControl,
|
||||||
|
flowControlWindow,
|
||||||
|
maxInboundMessageSize,
|
||||||
|
maxHeaderListSize,
|
||||||
|
keepAliveTimeNanos,
|
||||||
|
keepAliveTimeoutNanos,
|
||||||
|
keepAliveWithoutCalls,
|
||||||
|
transportTracerFactory,
|
||||||
|
localSocketPicker,
|
||||||
|
useGetForSafeMethods);
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
void assertEventLoopAndChannelType() {
|
||||||
|
boolean bothProvided =
|
||||||
|
channelFactory != DEFAULT_CHANNEL_FACTORY
|
||||||
|
&& eventLoopGroupPool != DEFAULT_EVENT_LOOP_GROUP_POOL;
|
||||||
|
boolean nonProvided =
|
||||||
|
channelFactory == DEFAULT_CHANNEL_FACTORY
|
||||||
|
&& eventLoopGroupPool == DEFAULT_EVENT_LOOP_GROUP_POOL;
|
||||||
|
checkState(
|
||||||
|
bothProvided || nonProvided,
|
||||||
|
"Both EventLoopGroup and ChannelType should be provided or neither should be");
|
||||||
|
}
|
||||||
|
|
||||||
|
@CheckReturnValue
|
||||||
|
int getDefaultPort() {
|
||||||
|
return protocolNegotiatorFactory.getDefaultPort();
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
@CheckReturnValue
|
||||||
|
static ProtocolNegotiator createProtocolNegotiatorByType(
|
||||||
|
NegotiationType negotiationType,
|
||||||
|
SslContext sslContext,
|
||||||
|
ObjectPool<? extends Executor> executorPool) {
|
||||||
|
switch (negotiationType) {
|
||||||
|
case PLAINTEXT:
|
||||||
|
return ProtocolNegotiators.plaintext();
|
||||||
|
case PLAINTEXT_UPGRADE:
|
||||||
|
return ProtocolNegotiators.plaintextUpgrade();
|
||||||
|
case TLS:
|
||||||
|
return ProtocolNegotiators.tls(sslContext, executorPool);
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unsupported negotiationType: " + negotiationType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NettyChannelBuilder disableCheckAuthority() {
|
||||||
|
this.managedChannelImplBuilder.disableCheckAuthority();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
NettyChannelBuilder enableCheckAuthority() {
|
||||||
|
this.managedChannelImplBuilder.enableCheckAuthority();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
void protocolNegotiatorFactory(ProtocolNegotiator.ClientFactory protocolNegotiatorFactory) {
|
||||||
|
checkState(
|
||||||
|
!freezeProtocolNegotiatorFactory, "Cannot change security when using ChannelCredentials");
|
||||||
|
this.protocolNegotiatorFactory =
|
||||||
|
checkNotNull(protocolNegotiatorFactory, "protocolNegotiatorFactory");
|
||||||
|
}
|
||||||
|
|
||||||
|
void setTracingEnabled(boolean value) {
|
||||||
|
this.managedChannelImplBuilder.setTracingEnabled(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setStatsEnabled(boolean value) {
|
||||||
|
this.managedChannelImplBuilder.setStatsEnabled(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setStatsRecordStartedRpcs(boolean value) {
|
||||||
|
this.managedChannelImplBuilder.setStatsRecordStartedRpcs(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setStatsRecordFinishedRpcs(boolean value) {
|
||||||
|
this.managedChannelImplBuilder.setStatsRecordFinishedRpcs(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setStatsRecordRealTimeMetrics(boolean value) {
|
||||||
|
this.managedChannelImplBuilder.setStatsRecordRealTimeMetrics(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
NettyChannelBuilder setTransportTracerFactory(TransportTracer.Factory transportTracerFactory) {
|
||||||
|
this.transportTracerFactory = transportTracerFactory;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class DefaultProtocolNegotiator implements ProtocolNegotiator.ClientFactory {
|
||||||
|
private NegotiationType negotiationType = NegotiationType.TLS;
|
||||||
|
private SslContext sslContext;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ProtocolNegotiator newNegotiator() {
|
||||||
|
SslContext localSslContext = sslContext;
|
||||||
|
if (negotiationType == NegotiationType.TLS && localSslContext == null) {
|
||||||
|
try {
|
||||||
|
localSslContext = GrpcSslContexts.forClient().build();
|
||||||
|
} catch (SSLException ex) {
|
||||||
|
throw new RuntimeException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return createProtocolNegotiatorByType(
|
||||||
|
negotiationType, localSslContext, managedChannelImplBuilder.getOffloadExecutorPool());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getDefaultPort() {
|
||||||
|
switch (negotiationType) {
|
||||||
|
case PLAINTEXT:
|
||||||
|
case PLAINTEXT_UPGRADE:
|
||||||
|
return GrpcUtil.DEFAULT_PORT_PLAINTEXT;
|
||||||
|
case TLS:
|
||||||
|
return GrpcUtil.DEFAULT_PORT_SSL;
|
||||||
|
default:
|
||||||
|
throw new AssertionError(negotiationType + " not handled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates Netty transports. Exposed for internal use, as it should be private. */
|
||||||
|
@CheckReturnValue
|
||||||
|
private static final class NettyTransportFactory implements ClientTransportFactory {
|
||||||
|
private final ProtocolNegotiator protocolNegotiator;
|
||||||
|
private final ChannelFactory<? extends Channel> channelFactory;
|
||||||
|
private final Map<ChannelOption<?>, ?> channelOptions;
|
||||||
|
private final ObjectPool<? extends EventLoopGroup> groupPool;
|
||||||
|
private final EventLoopGroup group;
|
||||||
|
private final boolean autoFlowControl;
|
||||||
|
private final int flowControlWindow;
|
||||||
|
private final int maxMessageSize;
|
||||||
|
private final int maxHeaderListSize;
|
||||||
|
private final long keepAliveTimeNanos;
|
||||||
|
private final AtomicBackoff keepAliveBackoff;
|
||||||
|
private final long keepAliveTimeoutNanos;
|
||||||
|
private final boolean keepAliveWithoutCalls;
|
||||||
|
private final TransportTracer.Factory transportTracerFactory;
|
||||||
|
private final LocalSocketPicker localSocketPicker;
|
||||||
|
private final boolean useGetForSafeMethods;
|
||||||
|
|
||||||
|
private boolean closed;
|
||||||
|
|
||||||
|
NettyTransportFactory(
|
||||||
|
ProtocolNegotiator protocolNegotiator,
|
||||||
|
ChannelFactory<? extends Channel> channelFactory,
|
||||||
|
Map<ChannelOption<?>, ?> channelOptions,
|
||||||
|
ObjectPool<? extends EventLoopGroup> groupPool,
|
||||||
|
boolean autoFlowControl,
|
||||||
|
int flowControlWindow,
|
||||||
|
int maxMessageSize,
|
||||||
|
int maxHeaderListSize,
|
||||||
|
long keepAliveTimeNanos,
|
||||||
|
long keepAliveTimeoutNanos,
|
||||||
|
boolean keepAliveWithoutCalls,
|
||||||
|
TransportTracer.Factory transportTracerFactory,
|
||||||
|
LocalSocketPicker localSocketPicker,
|
||||||
|
boolean useGetForSafeMethods) {
|
||||||
|
this.protocolNegotiator = checkNotNull(protocolNegotiator, "protocolNegotiator");
|
||||||
|
this.channelFactory = channelFactory;
|
||||||
|
this.channelOptions = new HashMap<ChannelOption<?>, Object>(channelOptions);
|
||||||
|
this.groupPool = groupPool;
|
||||||
|
this.group = groupPool.getObject();
|
||||||
|
this.autoFlowControl = autoFlowControl;
|
||||||
|
this.flowControlWindow = flowControlWindow;
|
||||||
|
this.maxMessageSize = maxMessageSize;
|
||||||
|
this.maxHeaderListSize = maxHeaderListSize;
|
||||||
|
this.keepAliveTimeNanos = keepAliveTimeNanos;
|
||||||
|
this.keepAliveBackoff = new AtomicBackoff("keepalive time nanos", keepAliveTimeNanos);
|
||||||
|
this.keepAliveTimeoutNanos = keepAliveTimeoutNanos;
|
||||||
|
this.keepAliveWithoutCalls = keepAliveWithoutCalls;
|
||||||
|
this.transportTracerFactory = transportTracerFactory;
|
||||||
|
this.localSocketPicker =
|
||||||
|
localSocketPicker != null ? localSocketPicker : new LocalSocketPicker();
|
||||||
|
this.useGetForSafeMethods = useGetForSafeMethods;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ConnectionClientTransport newClientTransport(
|
||||||
|
SocketAddress serverAddress, ClientTransportOptions options, ChannelLogger channelLogger) {
|
||||||
|
checkState(!closed, "The transport factory is closed.");
|
||||||
|
|
||||||
|
ProtocolNegotiator localNegotiator = protocolNegotiator;
|
||||||
|
HttpConnectProxiedSocketAddress proxiedAddr = options.getHttpConnectProxiedSocketAddress();
|
||||||
|
if (proxiedAddr != null) {
|
||||||
|
serverAddress = proxiedAddr.getTargetAddress();
|
||||||
|
localNegotiator =
|
||||||
|
ProtocolNegotiators.httpProxy(
|
||||||
|
proxiedAddr.getProxyAddress(),
|
||||||
|
proxiedAddr.getUsername(),
|
||||||
|
proxiedAddr.getPassword(),
|
||||||
|
protocolNegotiator);
|
||||||
|
}
|
||||||
|
|
||||||
|
final AtomicBackoff.State keepAliveTimeNanosState = keepAliveBackoff.getState();
|
||||||
|
Runnable tooManyPingsRunnable =
|
||||||
|
new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
keepAliveTimeNanosState.backoff();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO(carl-mastrangelo): Pass channelLogger in.
|
||||||
|
NettyClientTransport transport =
|
||||||
|
new NettyClientTransport(
|
||||||
|
serverAddress,
|
||||||
|
channelFactory,
|
||||||
|
channelOptions,
|
||||||
|
group,
|
||||||
|
localNegotiator,
|
||||||
|
autoFlowControl,
|
||||||
|
flowControlWindow,
|
||||||
|
maxMessageSize,
|
||||||
|
maxHeaderListSize,
|
||||||
|
keepAliveTimeNanosState.get(),
|
||||||
|
keepAliveTimeoutNanos,
|
||||||
|
keepAliveWithoutCalls,
|
||||||
|
options.getAuthority(),
|
||||||
|
options.getUserAgent(),
|
||||||
|
tooManyPingsRunnable,
|
||||||
|
transportTracerFactory.create(),
|
||||||
|
options.getEagAttributes(),
|
||||||
|
localSocketPicker,
|
||||||
|
channelLogger,
|
||||||
|
useGetForSafeMethods);
|
||||||
|
return transport;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ScheduledExecutorService getScheduledExecutorService() {
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SwapChannelCredentialsResult swapChannelCredentials(ChannelCredentials channelCreds) {
|
||||||
|
checkNotNull(channelCreds, "channelCreds");
|
||||||
|
FromChannelCredentialsResult result = ProtocolNegotiators.from(channelCreds);
|
||||||
|
if (result.error != null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
ClientTransportFactory factory =
|
||||||
|
new NettyTransportFactory(
|
||||||
|
result.negotiator.newNegotiator(),
|
||||||
|
channelFactory,
|
||||||
|
channelOptions,
|
||||||
|
groupPool,
|
||||||
|
autoFlowControl,
|
||||||
|
flowControlWindow,
|
||||||
|
maxMessageSize,
|
||||||
|
maxHeaderListSize,
|
||||||
|
keepAliveTimeNanos,
|
||||||
|
keepAliveTimeoutNanos,
|
||||||
|
keepAliveWithoutCalls,
|
||||||
|
transportTracerFactory,
|
||||||
|
localSocketPicker,
|
||||||
|
useGetForSafeMethods);
|
||||||
|
return new SwapChannelCredentialsResult(factory, result.callCredentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
if (closed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
closed = true;
|
||||||
|
|
||||||
|
protocolNegotiator.close();
|
||||||
|
groupPool.returnObject(group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,353 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2015 The gRPC Authors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.grpc.netty;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkArgument;
|
||||||
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
import static com.google.common.base.Preconditions.checkState;
|
||||||
|
import static io.netty.buffer.Unpooled.EMPTY_BUFFER;
|
||||||
|
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
import com.google.common.io.BaseEncoding;
|
||||||
|
import io.grpc.Attributes;
|
||||||
|
import io.grpc.CallOptions;
|
||||||
|
import io.grpc.InternalKnownTransport;
|
||||||
|
import io.grpc.InternalMethodDescriptor;
|
||||||
|
import io.grpc.Metadata;
|
||||||
|
import io.grpc.MethodDescriptor;
|
||||||
|
import io.grpc.Status;
|
||||||
|
import io.grpc.internal.AbstractClientStream;
|
||||||
|
import io.grpc.internal.Http2ClientStreamTransportState;
|
||||||
|
import io.grpc.internal.StatsTraceContext;
|
||||||
|
import io.grpc.internal.TransportTracer;
|
||||||
|
import io.grpc.internal.WritableBuffer;
|
||||||
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import io.netty.channel.Channel;
|
||||||
|
import io.netty.channel.ChannelFuture;
|
||||||
|
import io.netty.channel.ChannelFutureListener;
|
||||||
|
import io.netty.channel.EventLoop;
|
||||||
|
import io.netty.handler.codec.http2.Http2Headers;
|
||||||
|
import io.netty.handler.codec.http2.Http2Stream;
|
||||||
|
import io.netty.util.AsciiString;
|
||||||
|
import io.perfmark.PerfMark;
|
||||||
|
import io.perfmark.Tag;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
/** Client stream for a Netty transport. Must only be called from the sending application thread. */
|
||||||
|
class NettyClientStream extends AbstractClientStream {
|
||||||
|
private static final InternalMethodDescriptor methodDescriptorAccessor =
|
||||||
|
new InternalMethodDescriptor(
|
||||||
|
NettyClientTransport.class.getName().contains("grpc.netty.shaded")
|
||||||
|
? InternalKnownTransport.NETTY_SHADED
|
||||||
|
: InternalKnownTransport.NETTY);
|
||||||
|
|
||||||
|
private final Sink sink = new Sink();
|
||||||
|
private final TransportState state;
|
||||||
|
private final WriteQueue writeQueue;
|
||||||
|
private final MethodDescriptor<?, ?> method;
|
||||||
|
private AsciiString authority;
|
||||||
|
private final AsciiString scheme;
|
||||||
|
private final AsciiString userAgent;
|
||||||
|
|
||||||
|
NettyClientStream(
|
||||||
|
TransportState state,
|
||||||
|
MethodDescriptor<?, ?> method,
|
||||||
|
Metadata headers,
|
||||||
|
Channel channel,
|
||||||
|
AsciiString authority,
|
||||||
|
AsciiString scheme,
|
||||||
|
AsciiString userAgent,
|
||||||
|
StatsTraceContext statsTraceCtx,
|
||||||
|
TransportTracer transportTracer,
|
||||||
|
CallOptions callOptions,
|
||||||
|
boolean useGetForSafeMethods) {
|
||||||
|
super(
|
||||||
|
new NettyWritableBufferAllocator(channel.alloc()),
|
||||||
|
statsTraceCtx,
|
||||||
|
transportTracer,
|
||||||
|
headers,
|
||||||
|
callOptions,
|
||||||
|
useGetForSafeMethods && method.isSafe());
|
||||||
|
this.state = checkNotNull(state, "transportState");
|
||||||
|
this.writeQueue = state.handler.getWriteQueue();
|
||||||
|
this.method = checkNotNull(method, "method");
|
||||||
|
this.authority = checkNotNull(authority, "authority");
|
||||||
|
this.scheme = checkNotNull(scheme, "scheme");
|
||||||
|
this.userAgent = userAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected TransportState transportState() {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Sink abstractClientStreamSink() {
|
||||||
|
return sink;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAuthority(String authority) {
|
||||||
|
this.authority = AsciiString.of(checkNotNull(authority, "authority"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Attributes getAttributes() {
|
||||||
|
return state.handler.getAttributes();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Sink implements AbstractClientStream.Sink {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeHeaders(Metadata headers, byte[] requestPayload) {
|
||||||
|
PerfMark.startTask("NettyClientStream$Sink.writeHeaders");
|
||||||
|
try {
|
||||||
|
writeHeadersInternal(headers, requestPayload);
|
||||||
|
} finally {
|
||||||
|
PerfMark.stopTask("NettyClientStream$Sink.writeHeaders");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeHeadersInternal(Metadata headers, byte[] requestPayload) {
|
||||||
|
// Convert the headers into Netty HTTP/2 headers.
|
||||||
|
AsciiString defaultPath = (AsciiString) methodDescriptorAccessor.geRawMethodName(method);
|
||||||
|
if (defaultPath == null) {
|
||||||
|
defaultPath = new AsciiString("/" + method.getFullMethodName());
|
||||||
|
methodDescriptorAccessor.setRawMethodName(method, defaultPath);
|
||||||
|
}
|
||||||
|
boolean get = (requestPayload != null);
|
||||||
|
AsciiString httpMethod;
|
||||||
|
if (get) {
|
||||||
|
// Forge the query string
|
||||||
|
// TODO(ericgribkoff) Add the key back to the query string
|
||||||
|
defaultPath =
|
||||||
|
new AsciiString(defaultPath + "?" + BaseEncoding.base64().encode(requestPayload));
|
||||||
|
httpMethod = Utils.HTTP_GET_METHOD;
|
||||||
|
} else {
|
||||||
|
httpMethod = Utils.HTTP_METHOD;
|
||||||
|
}
|
||||||
|
Http2Headers http2Headers =
|
||||||
|
Utils.convertClientHeaders(
|
||||||
|
headers, scheme, defaultPath, authority, httpMethod, userAgent);
|
||||||
|
|
||||||
|
ChannelFutureListener failureListener =
|
||||||
|
new ChannelFutureListener() {
|
||||||
|
@Override
|
||||||
|
public void operationComplete(ChannelFuture future) throws Exception {
|
||||||
|
if (!future.isSuccess()) {
|
||||||
|
// Stream creation failed. Close the stream if not already closed.
|
||||||
|
// When the channel is shutdown, the lifecycle manager has a better view of the
|
||||||
|
// failure,
|
||||||
|
// especially before negotiation completes (because the negotiator commonly doesn't
|
||||||
|
// receive the execeptionCaught because NettyClientHandler does not propagate it).
|
||||||
|
Status s = transportState().handler.getLifecycleManager().getShutdownStatus();
|
||||||
|
if (s == null) {
|
||||||
|
s = transportState().statusFromFailedFuture(future);
|
||||||
|
}
|
||||||
|
transportState().transportReportStatus(s, true, new Metadata());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Write the command requesting the creation of the stream.
|
||||||
|
writeQueue
|
||||||
|
.enqueue(
|
||||||
|
new CreateStreamCommand(
|
||||||
|
http2Headers, transportState(), shouldBeCountedForInUse(), get),
|
||||||
|
!method.getType().clientSendsOneMessage() || get)
|
||||||
|
.addListener(failureListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeFrameInternal(
|
||||||
|
WritableBuffer frame, boolean endOfStream, boolean flush, final int numMessages) {
|
||||||
|
Preconditions.checkArgument(numMessages >= 0);
|
||||||
|
ByteBuf bytebuf =
|
||||||
|
frame == null ? EMPTY_BUFFER : ((NettyWritableBuffer) frame).bytebuf().touch();
|
||||||
|
final int numBytes = bytebuf.readableBytes();
|
||||||
|
if (numBytes > 0) {
|
||||||
|
// Add the bytes to outbound flow control.
|
||||||
|
onSendingBytes(numBytes);
|
||||||
|
writeQueue
|
||||||
|
.enqueue(new SendGrpcFrameCommand(transportState(), bytebuf, endOfStream), flush)
|
||||||
|
.addListener(
|
||||||
|
new ChannelFutureListener() {
|
||||||
|
@Override
|
||||||
|
public void operationComplete(ChannelFuture future) throws Exception {
|
||||||
|
// If the future succeeds when http2stream is null, the stream has been
|
||||||
|
// cancelled
|
||||||
|
// before it began and Netty is purging pending writes from the flow-controller.
|
||||||
|
if (future.isSuccess() && transportState().http2Stream() != null) {
|
||||||
|
// Remove the bytes from outbound flow control, optionally notifying
|
||||||
|
// the client that they can send more bytes.
|
||||||
|
transportState().onSentBytes(numBytes);
|
||||||
|
NettyClientStream.this.getTransportTracer().reportMessageSent(numMessages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// The frame is empty and will not impact outbound flow control. Just send it.
|
||||||
|
writeQueue.enqueue(new SendGrpcFrameCommand(transportState(), bytebuf, endOfStream), flush);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeFrame(
|
||||||
|
WritableBuffer frame, boolean endOfStream, boolean flush, int numMessages) {
|
||||||
|
PerfMark.startTask("NettyClientStream$Sink.writeFrame");
|
||||||
|
try {
|
||||||
|
writeFrameInternal(frame, endOfStream, flush, numMessages);
|
||||||
|
} finally {
|
||||||
|
PerfMark.stopTask("NettyClientStream$Sink.writeFrame");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cancel(Status status) {
|
||||||
|
PerfMark.startTask("NettyClientStream$Sink.cancel");
|
||||||
|
try {
|
||||||
|
writeQueue.enqueue(new CancelClientStreamCommand(transportState(), status), true);
|
||||||
|
} finally {
|
||||||
|
PerfMark.stopTask("NettyClientStream$Sink.cancel");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** This should only called from the transport thread. */
|
||||||
|
public abstract static class TransportState extends Http2ClientStreamTransportState
|
||||||
|
implements StreamIdHolder {
|
||||||
|
private static final int NON_EXISTENT_ID = -1;
|
||||||
|
|
||||||
|
private final String methodName;
|
||||||
|
private final NettyClientHandler handler;
|
||||||
|
private final EventLoop eventLoop;
|
||||||
|
private int id;
|
||||||
|
private Http2Stream http2Stream;
|
||||||
|
private Tag tag;
|
||||||
|
|
||||||
|
protected TransportState(
|
||||||
|
NettyClientHandler handler,
|
||||||
|
EventLoop eventLoop,
|
||||||
|
int maxMessageSize,
|
||||||
|
StatsTraceContext statsTraceCtx,
|
||||||
|
TransportTracer transportTracer,
|
||||||
|
String methodName) {
|
||||||
|
super(maxMessageSize, statsTraceCtx, transportTracer);
|
||||||
|
this.methodName = checkNotNull(methodName, "methodName");
|
||||||
|
this.handler = checkNotNull(handler, "handler");
|
||||||
|
this.eventLoop = checkNotNull(eventLoop, "eventLoop");
|
||||||
|
tag = PerfMark.createTag(methodName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int id() {
|
||||||
|
// id should be positive
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(int id) {
|
||||||
|
checkArgument(id > 0, "id must be positive %s", id);
|
||||||
|
checkState(this.id == 0, "id has been previously set: %s", this.id);
|
||||||
|
this.id = id;
|
||||||
|
this.tag = PerfMark.createTag(methodName, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks the stream state as if it had never existed. This can happen if the stream is cancelled
|
||||||
|
* after it is created, but before it has been started.
|
||||||
|
*/
|
||||||
|
void setNonExistent() {
|
||||||
|
checkState(this.id == 0, "Id has been previously set: %s", this.id);
|
||||||
|
this.id = NON_EXISTENT_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isNonExistent() {
|
||||||
|
return this.id == NON_EXISTENT_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the underlying Netty {@link Http2Stream} for this stream. This must be called in the
|
||||||
|
* context of the transport thread.
|
||||||
|
*/
|
||||||
|
public void setHttp2Stream(Http2Stream http2Stream) {
|
||||||
|
checkNotNull(http2Stream, "http2Stream");
|
||||||
|
checkState(this.http2Stream == null, "Can only set http2Stream once");
|
||||||
|
this.http2Stream = http2Stream;
|
||||||
|
|
||||||
|
// Now that the stream has actually been initialized, call the listener's onReady callback if
|
||||||
|
// appropriate.
|
||||||
|
onStreamAllocated();
|
||||||
|
getTransportTracer().reportLocalStreamStarted();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gets the underlying Netty {@link Http2Stream} for this stream. */
|
||||||
|
@Nullable
|
||||||
|
public Http2Stream http2Stream() {
|
||||||
|
return http2Stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intended to be overridden by NettyClientTransport, which has more information about failures.
|
||||||
|
* May only be called from event loop.
|
||||||
|
*/
|
||||||
|
protected abstract Status statusFromFailedFuture(ChannelFuture f);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void http2ProcessingFailed(Status status, boolean stopDelivery, Metadata trailers) {
|
||||||
|
transportReportStatus(status, stopDelivery, trailers);
|
||||||
|
handler.getWriteQueue().enqueue(new CancelClientStreamCommand(this, status), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void runOnTransportThread(final Runnable r) {
|
||||||
|
if (eventLoop.inEventLoop()) {
|
||||||
|
r.run();
|
||||||
|
} else {
|
||||||
|
eventLoop.execute(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void bytesRead(int processedBytes) {
|
||||||
|
handler.returnProcessedBytes(http2Stream, processedBytes);
|
||||||
|
handler.getWriteQueue().scheduleFlush();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deframeFailed(Throwable cause) {
|
||||||
|
http2ProcessingFailed(Status.fromThrowable(cause), true, new Metadata());
|
||||||
|
}
|
||||||
|
|
||||||
|
void transportHeadersReceived(Http2Headers headers, boolean endOfStream) {
|
||||||
|
if (endOfStream) {
|
||||||
|
if (!isOutboundClosed()) {
|
||||||
|
handler.getWriteQueue().enqueue(new CancelClientStreamCommand(this, null), true);
|
||||||
|
}
|
||||||
|
transportTrailersReceived(Utils.convertTrailers(headers));
|
||||||
|
} else {
|
||||||
|
transportHeadersReceived(Utils.convertHeaders(headers));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void transportDataReceived(ByteBuf frame, boolean endOfStream) {
|
||||||
|
transportDataReceived(new NettyReadableBuffer(frame.retain()), endOfStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final Tag tag() {
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,228 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2015 The gRPC Authors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.grpc.netty;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||||
|
import io.netty.channel.Channel;
|
||||||
|
import io.netty.channel.ChannelFuture;
|
||||||
|
import io.netty.channel.ChannelPromise;
|
||||||
|
import io.perfmark.Link;
|
||||||
|
import io.perfmark.PerfMark;
|
||||||
|
import java.util.Queue;
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
/** A queue of pending writes to a {@link Channel} that is flushed as a single unit. */
|
||||||
|
class WriteQueue {
|
||||||
|
|
||||||
|
// Dequeue in chunks, so we don't have to acquire the queue's log too often.
|
||||||
|
@VisibleForTesting static final int DEQUE_CHUNK_SIZE = 128;
|
||||||
|
|
||||||
|
/** {@link Runnable} used to schedule work onto the tail of the event loop. */
|
||||||
|
private final Runnable later =
|
||||||
|
new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final Channel channel;
|
||||||
|
private final Queue<QueuedCommand> queue;
|
||||||
|
private final AtomicBoolean scheduled = new AtomicBoolean();
|
||||||
|
|
||||||
|
public WriteQueue(Channel channel) {
|
||||||
|
this.channel = Preconditions.checkNotNull(channel, "channel");
|
||||||
|
queue = new ConcurrentLinkedQueue<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Schedule a flush on the channel. */
|
||||||
|
void scheduleFlush() {
|
||||||
|
if (scheduled.compareAndSet(false, true)) {
|
||||||
|
// Add the queue to the tail of the event loop so writes will be executed immediately
|
||||||
|
// inside the event loop. Note DO NOT do channel.write outside the event loop as
|
||||||
|
// it will not wake up immediately without a flush.
|
||||||
|
channel.eventLoop().execute(later);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueue a write command on the channel.
|
||||||
|
*
|
||||||
|
* @param command a write to be executed on the channel.
|
||||||
|
* @param flush true if a flush of the write should be schedule, false if a later call to enqueue
|
||||||
|
* will schedule the flush.
|
||||||
|
*/
|
||||||
|
@CanIgnoreReturnValue
|
||||||
|
ChannelFuture enqueue(QueuedCommand command, boolean flush) {
|
||||||
|
// Detect erroneous code that tries to reuse command objects.
|
||||||
|
Preconditions.checkArgument(command.promise() == null, "promise must not be set on command");
|
||||||
|
|
||||||
|
ChannelPromise promise = channel.newPromise();
|
||||||
|
command.promise(promise);
|
||||||
|
queue.add(command);
|
||||||
|
if (flush) {
|
||||||
|
scheduleFlush();
|
||||||
|
}
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueue the runnable. It is not safe for another thread to queue an Runnable directly to the
|
||||||
|
* event loop, because it will be out-of-order with writes. This method allows the Runnable to be
|
||||||
|
* processed in-order with writes.
|
||||||
|
*/
|
||||||
|
void enqueue(Runnable runnable, boolean flush) {
|
||||||
|
queue.add(new RunnableCommand(runnable));
|
||||||
|
if (flush) {
|
||||||
|
scheduleFlush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes enqueued work directly on the current thread. This can be used to trigger writes
|
||||||
|
* before performing additional reads. Must be called from the event loop. This method makes no
|
||||||
|
* guarantee that the work queue is empty when it returns.
|
||||||
|
*/
|
||||||
|
void drainNow() {
|
||||||
|
Preconditions.checkState(channel.eventLoop().inEventLoop(), "must be on the event loop");
|
||||||
|
if (queue.peek() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the queue of commands and dispatch them to the stream. This method is only called in
|
||||||
|
* the event loop
|
||||||
|
*/
|
||||||
|
private void flush() {
|
||||||
|
PerfMark.startTask("WriteQueue.periodicFlush");
|
||||||
|
try {
|
||||||
|
QueuedCommand cmd;
|
||||||
|
int i = 0;
|
||||||
|
boolean flushedOnce = false;
|
||||||
|
while ((cmd = queue.poll()) != null) {
|
||||||
|
cmd.run(channel);
|
||||||
|
if (++i == DEQUE_CHUNK_SIZE) {
|
||||||
|
i = 0;
|
||||||
|
// Flush each chunk so we are releasing buffers periodically. In theory this loop
|
||||||
|
// might never end as new events are continuously added to the queue, if we never
|
||||||
|
// flushed in that case we would be guaranteed to OOM.
|
||||||
|
PerfMark.startTask("WriteQueue.flush0");
|
||||||
|
try {
|
||||||
|
channel.flush();
|
||||||
|
} finally {
|
||||||
|
PerfMark.stopTask("WriteQueue.flush0");
|
||||||
|
}
|
||||||
|
flushedOnce = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Must flush at least once, even if there were no writes.
|
||||||
|
if (i != 0 || !flushedOnce) {
|
||||||
|
PerfMark.startTask("WriteQueue.flush1");
|
||||||
|
try {
|
||||||
|
channel.flush();
|
||||||
|
} finally {
|
||||||
|
PerfMark.stopTask("WriteQueue.flush1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
PerfMark.stopTask("WriteQueue.periodicFlush");
|
||||||
|
// Mark the write as done, if the queue is non-empty after marking trigger a new write.
|
||||||
|
scheduled.set(false);
|
||||||
|
if (!queue.isEmpty()) {
|
||||||
|
scheduleFlush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class RunnableCommand implements QueuedCommand {
|
||||||
|
private final Runnable runnable;
|
||||||
|
private final Link link;
|
||||||
|
|
||||||
|
public RunnableCommand(Runnable runnable) {
|
||||||
|
this.link = PerfMark.linkOut();
|
||||||
|
this.runnable = runnable;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void promise(ChannelPromise promise) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final ChannelPromise promise() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void run(Channel channel) {
|
||||||
|
runnable.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Link getLink() {
|
||||||
|
return link;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract static class AbstractQueuedCommand implements QueuedCommand {
|
||||||
|
|
||||||
|
private ChannelPromise promise;
|
||||||
|
private final Link link;
|
||||||
|
|
||||||
|
AbstractQueuedCommand() {
|
||||||
|
this.link = PerfMark.linkOut();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void promise(ChannelPromise promise) {
|
||||||
|
this.promise = promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final ChannelPromise promise() {
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void run(Channel channel) {
|
||||||
|
channel.write(this, promise);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Link getLink() {
|
||||||
|
return link;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Simple wrapper type around a command and its optional completion listener. */
|
||||||
|
interface QueuedCommand {
|
||||||
|
/** Returns the promise beeing notified of the success/failure of the write. */
|
||||||
|
ChannelPromise promise();
|
||||||
|
|
||||||
|
/** Sets the promise. */
|
||||||
|
void promise(ChannelPromise promise);
|
||||||
|
|
||||||
|
void run(Channel channel);
|
||||||
|
|
||||||
|
Link getLink();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,765 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2014 The gRPC Authors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.grpc.stub;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
import static com.google.common.base.Preconditions.checkState;
|
||||||
|
|
||||||
|
import com.google.common.base.MoreObjects;
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
import com.google.common.util.concurrent.AbstractFuture;
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
import io.grpc.CallOptions;
|
||||||
|
import io.grpc.Channel;
|
||||||
|
import io.grpc.ClientCall;
|
||||||
|
import io.grpc.Metadata;
|
||||||
|
import io.grpc.MethodDescriptor;
|
||||||
|
import io.grpc.Status;
|
||||||
|
import io.grpc.StatusException;
|
||||||
|
import io.grpc.StatusRuntimeException;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
|
import java.util.concurrent.BlockingQueue;
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.locks.LockSupport;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility functions for processing different call idioms. We have one-to-one correspondence between
|
||||||
|
* utilities in this class and the potential signatures in a generated stub class so that the
|
||||||
|
* runtime can vary behavior without requiring regeneration of the stub.
|
||||||
|
*/
|
||||||
|
public final class ClientCalls {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(ClientCalls.class.getName());
|
||||||
|
|
||||||
|
// Prevent instantiation
|
||||||
|
private ClientCalls() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a unary call with a response {@link StreamObserver}. The {@code call} should not be
|
||||||
|
* already started. After calling this method, {@code call} should no longer be used.
|
||||||
|
*
|
||||||
|
* <p>If the provided {@code responseObserver} is an instance of {@link ClientResponseObserver},
|
||||||
|
* {@code beforeStart()} will be called.
|
||||||
|
*/
|
||||||
|
public static <ReqT, RespT> void asyncUnaryCall(
|
||||||
|
ClientCall<ReqT, RespT> call, ReqT req, StreamObserver<RespT> responseObserver) {
|
||||||
|
asyncUnaryRequestCall(call, req, responseObserver, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a server-streaming call with a response {@link StreamObserver}. The {@code call}
|
||||||
|
* should not be already started. After calling this method, {@code call} should no longer be
|
||||||
|
* used.
|
||||||
|
*
|
||||||
|
* <p>If the provided {@code responseObserver} is an instance of {@link ClientResponseObserver},
|
||||||
|
* {@code beforeStart()} will be called.
|
||||||
|
*/
|
||||||
|
public static <ReqT, RespT> void asyncServerStreamingCall(
|
||||||
|
ClientCall<ReqT, RespT> call, ReqT req, StreamObserver<RespT> responseObserver) {
|
||||||
|
asyncUnaryRequestCall(call, req, responseObserver, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a client-streaming call returning a {@link StreamObserver} for the request messages.
|
||||||
|
* The {@code call} should not be already started. After calling this method, {@code call} should
|
||||||
|
* no longer be used.
|
||||||
|
*
|
||||||
|
* <p>If the provided {@code responseObserver} is an instance of {@link ClientResponseObserver},
|
||||||
|
* {@code beforeStart()} will be called.
|
||||||
|
*
|
||||||
|
* @return request stream observer. It will extend {@link ClientCallStreamObserver}
|
||||||
|
*/
|
||||||
|
public static <ReqT, RespT> StreamObserver<ReqT> asyncClientStreamingCall(
|
||||||
|
ClientCall<ReqT, RespT> call, StreamObserver<RespT> responseObserver) {
|
||||||
|
return asyncStreamingRequestCall(call, responseObserver, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a bidirectional-streaming call. The {@code call} should not be already started. After
|
||||||
|
* calling this method, {@code call} should no longer be used.
|
||||||
|
*
|
||||||
|
* <p>If the provided {@code responseObserver} is an instance of {@link ClientResponseObserver},
|
||||||
|
* {@code beforeStart()} will be called.
|
||||||
|
*
|
||||||
|
* @return request stream observer. It will extend {@link ClientCallStreamObserver}
|
||||||
|
*/
|
||||||
|
public static <ReqT, RespT> StreamObserver<ReqT> asyncBidiStreamingCall(
|
||||||
|
ClientCall<ReqT, RespT> call, StreamObserver<RespT> responseObserver) {
|
||||||
|
return asyncStreamingRequestCall(call, responseObserver, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a unary call and blocks on the response. The {@code call} should not be already
|
||||||
|
* started. After calling this method, {@code call} should no longer be used.
|
||||||
|
*
|
||||||
|
* @return the single response message.
|
||||||
|
* @throws StatusRuntimeException on error
|
||||||
|
*/
|
||||||
|
public static <ReqT, RespT> RespT blockingUnaryCall(ClientCall<ReqT, RespT> call, ReqT req) {
|
||||||
|
try {
|
||||||
|
return getUnchecked(futureUnaryCall(call, req));
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
throw cancelThrow(call, e);
|
||||||
|
} catch (Error e) {
|
||||||
|
throw cancelThrow(call, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a unary call and blocks on the response. The {@code call} should not be already
|
||||||
|
* started. After calling this method, {@code call} should no longer be used.
|
||||||
|
*
|
||||||
|
* @return the single response message.
|
||||||
|
* @throws StatusRuntimeException on error
|
||||||
|
*/
|
||||||
|
public static <ReqT, RespT> RespT blockingUnaryCall(
|
||||||
|
Channel channel, MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, ReqT req) {
|
||||||
|
ThreadlessExecutor executor = new ThreadlessExecutor();
|
||||||
|
boolean interrupt = false;
|
||||||
|
ClientCall<ReqT, RespT> call =
|
||||||
|
channel.newCall(
|
||||||
|
method,
|
||||||
|
callOptions
|
||||||
|
.withOption(ClientCalls.STUB_TYPE_OPTION, StubType.BLOCKING)
|
||||||
|
.withExecutor(executor));
|
||||||
|
try {
|
||||||
|
ListenableFuture<RespT> responseFuture = futureUnaryCall(call, req);
|
||||||
|
while (!responseFuture.isDone()) {
|
||||||
|
try {
|
||||||
|
executor.waitAndDrain();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
interrupt = true;
|
||||||
|
call.cancel("Thread interrupted", e);
|
||||||
|
// Now wait for onClose() to be called, so interceptors can clean up
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return getUnchecked(responseFuture);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
// Something very bad happened. All bets are off; it may be dangerous to wait for onClose().
|
||||||
|
throw cancelThrow(call, e);
|
||||||
|
} catch (Error e) {
|
||||||
|
// Something very bad happened. All bets are off; it may be dangerous to wait for onClose().
|
||||||
|
throw cancelThrow(call, e);
|
||||||
|
} finally {
|
||||||
|
if (interrupt) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a server-streaming call returning a blocking {@link Iterator} over the response
|
||||||
|
* stream. The {@code call} should not be already started. After calling this method, {@code call}
|
||||||
|
* should no longer be used.
|
||||||
|
*
|
||||||
|
* <p>The returned iterator may throw {@link StatusRuntimeException} on error.
|
||||||
|
*
|
||||||
|
* @return an iterator over the response stream.
|
||||||
|
*/
|
||||||
|
// TODO(louiscryan): Not clear if we want to use this idiom for 'simple' stubs.
|
||||||
|
public static <ReqT, RespT> Iterator<RespT> blockingServerStreamingCall(
|
||||||
|
ClientCall<ReqT, RespT> call, ReqT req) {
|
||||||
|
BlockingResponseStream<RespT> result = new BlockingResponseStream<>(call);
|
||||||
|
asyncUnaryRequestCall(call, req, result.listener());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a server-streaming call returning a blocking {@link Iterator} over the response
|
||||||
|
* stream. The {@code call} should not be already started. After calling this method, {@code call}
|
||||||
|
* should no longer be used.
|
||||||
|
*
|
||||||
|
* <p>The returned iterator may throw {@link StatusRuntimeException} on error.
|
||||||
|
*
|
||||||
|
* @return an iterator over the response stream.
|
||||||
|
*/
|
||||||
|
// TODO(louiscryan): Not clear if we want to use this idiom for 'simple' stubs.
|
||||||
|
public static <ReqT, RespT> Iterator<RespT> blockingServerStreamingCall(
|
||||||
|
Channel channel, MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, ReqT req) {
|
||||||
|
ThreadlessExecutor executor = new ThreadlessExecutor();
|
||||||
|
ClientCall<ReqT, RespT> call =
|
||||||
|
channel.newCall(
|
||||||
|
method,
|
||||||
|
callOptions
|
||||||
|
.withOption(ClientCalls.STUB_TYPE_OPTION, StubType.BLOCKING)
|
||||||
|
.withExecutor(executor));
|
||||||
|
BlockingResponseStream<RespT> result = new BlockingResponseStream<>(call, executor);
|
||||||
|
asyncUnaryRequestCall(call, req, result.listener());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a unary call and returns a {@link ListenableFuture} to the response. The {@code call}
|
||||||
|
* should not be already started. After calling this method, {@code call} should no longer be
|
||||||
|
* used.
|
||||||
|
*
|
||||||
|
* @return a future for the single response message.
|
||||||
|
*/
|
||||||
|
public static <ReqT, RespT> ListenableFuture<RespT> futureUnaryCall(
|
||||||
|
ClientCall<ReqT, RespT> call, ReqT req) {
|
||||||
|
GrpcFuture<RespT> responseFuture = new GrpcFuture<>(call);
|
||||||
|
asyncUnaryRequestCall(call, req, new UnaryStreamToFuture<>(responseFuture));
|
||||||
|
return responseFuture;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the result of calling {@link Future#get()} interruptibly on a task known not to throw a
|
||||||
|
* checked exception.
|
||||||
|
*
|
||||||
|
* <p>If interrupted, the interrupt is restored before throwing an exception..
|
||||||
|
*
|
||||||
|
* @throws java.util.concurrent.CancellationException if {@code get} throws a {@code
|
||||||
|
* CancellationException}.
|
||||||
|
* @throws io.grpc.StatusRuntimeException if {@code get} throws an {@link ExecutionException} or
|
||||||
|
* an {@link InterruptedException}.
|
||||||
|
*/
|
||||||
|
private static <V> V getUnchecked(Future<V> future) {
|
||||||
|
try {
|
||||||
|
return future.get();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw Status.CANCELLED
|
||||||
|
.withDescription("Thread interrupted")
|
||||||
|
.withCause(e)
|
||||||
|
.asRuntimeException();
|
||||||
|
} catch (ExecutionException e) {
|
||||||
|
throw toStatusRuntimeException(e.getCause());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps the given {@link Throwable} in a {@link StatusRuntimeException}. If it contains an
|
||||||
|
* embedded {@link StatusException} or {@link StatusRuntimeException}, the returned exception will
|
||||||
|
* contain the embedded trailers and status, with the given exception as the cause. Otherwise, an
|
||||||
|
* exception will be generated from an {@link Status#UNKNOWN} status.
|
||||||
|
*/
|
||||||
|
private static StatusRuntimeException toStatusRuntimeException(Throwable t) {
|
||||||
|
Throwable cause = checkNotNull(t, "t");
|
||||||
|
while (cause != null) {
|
||||||
|
// If we have an embedded status, use it and replace the cause
|
||||||
|
if (cause instanceof StatusException) {
|
||||||
|
StatusException se = (StatusException) cause;
|
||||||
|
return new StatusRuntimeException(se.getStatus(), se.getTrailers());
|
||||||
|
} else if (cause instanceof StatusRuntimeException) {
|
||||||
|
StatusRuntimeException se = (StatusRuntimeException) cause;
|
||||||
|
return new StatusRuntimeException(se.getStatus(), se.getTrailers());
|
||||||
|
}
|
||||||
|
cause = cause.getCause();
|
||||||
|
}
|
||||||
|
return Status.UNKNOWN.withDescription("unexpected exception").withCause(t).asRuntimeException();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels a call, and throws the exception.
|
||||||
|
*
|
||||||
|
* @param t must be a RuntimeException or Error
|
||||||
|
*/
|
||||||
|
private static RuntimeException cancelThrow(ClientCall<?, ?> call, Throwable t) {
|
||||||
|
try {
|
||||||
|
call.cancel(null, t);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
assert e instanceof RuntimeException || e instanceof Error;
|
||||||
|
logger.log(Level.SEVERE, "RuntimeException encountered while closing call", e);
|
||||||
|
}
|
||||||
|
if (t instanceof RuntimeException) {
|
||||||
|
throw (RuntimeException) t;
|
||||||
|
} else if (t instanceof Error) {
|
||||||
|
throw (Error) t;
|
||||||
|
}
|
||||||
|
// should be impossible
|
||||||
|
throw new AssertionError(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <ReqT, RespT> void asyncUnaryRequestCall(
|
||||||
|
ClientCall<ReqT, RespT> call,
|
||||||
|
ReqT req,
|
||||||
|
StreamObserver<RespT> responseObserver,
|
||||||
|
boolean streamingResponse) {
|
||||||
|
asyncUnaryRequestCall(
|
||||||
|
call,
|
||||||
|
req,
|
||||||
|
new StreamObserverToCallListenerAdapter<>(
|
||||||
|
responseObserver, new CallToStreamObserverAdapter<>(call, streamingResponse)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <ReqT, RespT> void asyncUnaryRequestCall(
|
||||||
|
ClientCall<ReqT, RespT> call, ReqT req, StartableListener<RespT> responseListener) {
|
||||||
|
startCall(call, responseListener);
|
||||||
|
try {
|
||||||
|
call.sendMessage(req);
|
||||||
|
call.halfClose();
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
throw cancelThrow(call, e);
|
||||||
|
} catch (Error e) {
|
||||||
|
throw cancelThrow(call, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <ReqT, RespT> StreamObserver<ReqT> asyncStreamingRequestCall(
|
||||||
|
ClientCall<ReqT, RespT> call,
|
||||||
|
StreamObserver<RespT> responseObserver,
|
||||||
|
boolean streamingResponse) {
|
||||||
|
CallToStreamObserverAdapter<ReqT> adapter =
|
||||||
|
new CallToStreamObserverAdapter<>(call, streamingResponse);
|
||||||
|
startCall(call, new StreamObserverToCallListenerAdapter<>(responseObserver, adapter));
|
||||||
|
return adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <ReqT, RespT> void startCall(
|
||||||
|
ClientCall<ReqT, RespT> call, StartableListener<RespT> responseListener) {
|
||||||
|
call.start(responseListener, new Metadata());
|
||||||
|
responseListener.onStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
private abstract static class StartableListener<T> extends ClientCall.Listener<T> {
|
||||||
|
abstract void onStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class CallToStreamObserverAdapter<T> extends ClientCallStreamObserver<T> {
|
||||||
|
private boolean frozen;
|
||||||
|
private final ClientCall<T, ?> call;
|
||||||
|
private final boolean streamingResponse;
|
||||||
|
private Runnable onReadyHandler;
|
||||||
|
private int initialRequest = 1;
|
||||||
|
private boolean autoRequestEnabled = true;
|
||||||
|
private boolean aborted = false;
|
||||||
|
private boolean completed = false;
|
||||||
|
|
||||||
|
// Non private to avoid synthetic class
|
||||||
|
CallToStreamObserverAdapter(ClientCall<T, ?> call, boolean streamingResponse) {
|
||||||
|
this.call = call;
|
||||||
|
this.streamingResponse = streamingResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void freeze() {
|
||||||
|
this.frozen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNext(T value) {
|
||||||
|
checkState(!aborted, "Stream was terminated by error, no further calls are allowed");
|
||||||
|
checkState(!completed, "Stream is already completed, no further calls are allowed");
|
||||||
|
call.sendMessage(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(Throwable t) {
|
||||||
|
call.cancel("Cancelled by client with StreamObserver.onError()", t);
|
||||||
|
aborted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCompleted() {
|
||||||
|
call.halfClose();
|
||||||
|
completed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isReady() {
|
||||||
|
return call.isReady();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setOnReadyHandler(Runnable onReadyHandler) {
|
||||||
|
if (frozen) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Cannot alter onReadyHandler after call started. Use ClientResponseObserver");
|
||||||
|
}
|
||||||
|
this.onReadyHandler = onReadyHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
|
@Override
|
||||||
|
public void disableAutoInboundFlowControl() {
|
||||||
|
disableAutoRequestWithInitial(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void disableAutoRequestWithInitial(int request) {
|
||||||
|
if (frozen) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Cannot disable auto flow control after call started. Use ClientResponseObserver");
|
||||||
|
}
|
||||||
|
Preconditions.checkArgument(request >= 0, "Initial requests must be non-negative");
|
||||||
|
initialRequest = request;
|
||||||
|
autoRequestEnabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void request(int count) {
|
||||||
|
if (!streamingResponse && count == 1) {
|
||||||
|
// Initially ask for two responses from flow-control so that if a misbehaving server
|
||||||
|
// sends more than one responses, we can catch it and fail it in the listener.
|
||||||
|
call.request(2);
|
||||||
|
} else {
|
||||||
|
call.request(count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setMessageCompression(boolean enable) {
|
||||||
|
call.setMessageCompression(enable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cancel(@Nullable String message, @Nullable Throwable cause) {
|
||||||
|
call.cancel(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class StreamObserverToCallListenerAdapter<ReqT, RespT>
|
||||||
|
extends StartableListener<RespT> {
|
||||||
|
private final StreamObserver<RespT> observer;
|
||||||
|
private final CallToStreamObserverAdapter<ReqT> adapter;
|
||||||
|
private boolean firstResponseReceived;
|
||||||
|
|
||||||
|
// Non private to avoid synthetic class
|
||||||
|
StreamObserverToCallListenerAdapter(
|
||||||
|
StreamObserver<RespT> observer, CallToStreamObserverAdapter<ReqT> adapter) {
|
||||||
|
this.observer = observer;
|
||||||
|
this.adapter = adapter;
|
||||||
|
if (observer instanceof ClientResponseObserver) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
ClientResponseObserver<ReqT, RespT> clientResponseObserver =
|
||||||
|
(ClientResponseObserver<ReqT, RespT>) observer;
|
||||||
|
clientResponseObserver.beforeStart(adapter);
|
||||||
|
}
|
||||||
|
adapter.freeze();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onHeaders(Metadata headers) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(RespT message) {
|
||||||
|
if (firstResponseReceived && !adapter.streamingResponse) {
|
||||||
|
throw Status.INTERNAL
|
||||||
|
.withDescription("More than one responses received for unary or client-streaming call")
|
||||||
|
.asRuntimeException();
|
||||||
|
}
|
||||||
|
firstResponseReceived = true;
|
||||||
|
observer.onNext(message);
|
||||||
|
|
||||||
|
if (adapter.streamingResponse && adapter.autoRequestEnabled) {
|
||||||
|
// Request delivery of the next inbound message.
|
||||||
|
adapter.request(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClose(Status status, Metadata trailers) {
|
||||||
|
if (status.isOk()) {
|
||||||
|
observer.onCompleted();
|
||||||
|
} else {
|
||||||
|
observer.onError(status.asRuntimeException(trailers));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReady() {
|
||||||
|
if (adapter.onReadyHandler != null) {
|
||||||
|
adapter.onReadyHandler.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void onStart() {
|
||||||
|
if (adapter.initialRequest > 0) {
|
||||||
|
adapter.request(adapter.initialRequest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Completes a {@link GrpcFuture} using {@link StreamObserver} events. */
|
||||||
|
private static final class UnaryStreamToFuture<RespT> extends StartableListener<RespT> {
|
||||||
|
private final GrpcFuture<RespT> responseFuture;
|
||||||
|
private RespT value;
|
||||||
|
|
||||||
|
// Non private to avoid synthetic class
|
||||||
|
UnaryStreamToFuture(GrpcFuture<RespT> responseFuture) {
|
||||||
|
this.responseFuture = responseFuture;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onHeaders(Metadata headers) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(RespT value) {
|
||||||
|
if (this.value != null) {
|
||||||
|
throw Status.INTERNAL
|
||||||
|
.withDescription("More than one value received for unary call")
|
||||||
|
.asRuntimeException();
|
||||||
|
}
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClose(Status status, Metadata trailers) {
|
||||||
|
if (status.isOk()) {
|
||||||
|
if (value == null) {
|
||||||
|
// No value received so mark the future as an error
|
||||||
|
responseFuture.setException(
|
||||||
|
Status.INTERNAL
|
||||||
|
.withDescription("No value received for unary call")
|
||||||
|
.asRuntimeException(trailers));
|
||||||
|
}
|
||||||
|
responseFuture.set(value);
|
||||||
|
} else {
|
||||||
|
responseFuture.setException(status.asRuntimeException(trailers));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void onStart() {
|
||||||
|
responseFuture.call.request(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class GrpcFuture<RespT> extends AbstractFuture<RespT> {
|
||||||
|
private final ClientCall<?, RespT> call;
|
||||||
|
|
||||||
|
// Non private to avoid synthetic class
|
||||||
|
GrpcFuture(ClientCall<?, RespT> call) {
|
||||||
|
this.call = call;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void interruptTask() {
|
||||||
|
call.cancel("GrpcFuture was cancelled", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean set(@Nullable RespT resp) {
|
||||||
|
return super.set(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean setException(Throwable throwable) {
|
||||||
|
return super.setException(throwable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("MissingOverride") // Add @Override once Java 6 support is dropped
|
||||||
|
protected String pendingToString() {
|
||||||
|
return MoreObjects.toStringHelper(this).add("clientCall", call).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert events on a {@link io.grpc.ClientCall.Listener} into a blocking {@link Iterator}.
|
||||||
|
*
|
||||||
|
* <p>The class is not thread-safe, but it does permit {@link ClientCall.Listener} calls in a
|
||||||
|
* separate thread from {@link Iterator} calls.
|
||||||
|
*/
|
||||||
|
// TODO(ejona86): determine how to allow ClientCall.cancel() in case of application error.
|
||||||
|
private static final class BlockingResponseStream<T> implements Iterator<T> {
|
||||||
|
// Due to flow control, only needs to hold up to 3 items: 2 for value, 1 for close.
|
||||||
|
// (2 for value, not 1, because of early request() in next())
|
||||||
|
private final BlockingQueue<Object> buffer = new ArrayBlockingQueue<>(3);
|
||||||
|
private final StartableListener<T> listener = new QueuingListener();
|
||||||
|
private final ClientCall<?, T> call;
|
||||||
|
/** May be null. */
|
||||||
|
private final ThreadlessExecutor threadless;
|
||||||
|
// Only accessed when iterating.
|
||||||
|
private Object last;
|
||||||
|
|
||||||
|
// Non private to avoid synthetic class
|
||||||
|
BlockingResponseStream(ClientCall<?, T> call) {
|
||||||
|
this(call, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non private to avoid synthetic class
|
||||||
|
BlockingResponseStream(ClientCall<?, T> call, ThreadlessExecutor threadless) {
|
||||||
|
this.call = call;
|
||||||
|
this.threadless = threadless;
|
||||||
|
}
|
||||||
|
|
||||||
|
StartableListener<T> listener() {
|
||||||
|
return listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object waitForNext() {
|
||||||
|
boolean interrupt = false;
|
||||||
|
try {
|
||||||
|
if (threadless == null) {
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
return buffer.take();
|
||||||
|
} catch (InterruptedException ie) {
|
||||||
|
interrupt = true;
|
||||||
|
call.cancel("Thread interrupted", ie);
|
||||||
|
// Now wait for onClose() to be called, to guarantee BlockingQueue doesn't fill
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Object next;
|
||||||
|
while ((next = buffer.poll()) == null) {
|
||||||
|
try {
|
||||||
|
threadless.waitAndDrain();
|
||||||
|
} catch (InterruptedException ie) {
|
||||||
|
interrupt = true;
|
||||||
|
call.cancel("Thread interrupted", ie);
|
||||||
|
// Now wait for onClose() to be called, so interceptors can clean up
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (interrupt) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasNext() {
|
||||||
|
while (last == null) {
|
||||||
|
// Will block here indefinitely waiting for content. RPC timeouts defend against permanent
|
||||||
|
// hangs here as the call will become closed.
|
||||||
|
last = waitForNext();
|
||||||
|
}
|
||||||
|
if (last instanceof StatusRuntimeException) {
|
||||||
|
// Rethrow the exception with a new stacktrace.
|
||||||
|
StatusRuntimeException e = (StatusRuntimeException) last;
|
||||||
|
throw e.getStatus().asRuntimeException(e.getTrailers());
|
||||||
|
}
|
||||||
|
return last != this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T next() {
|
||||||
|
// Eagerly call request(1) so it can be processing the next message while we wait for the
|
||||||
|
// current one, which reduces latency for the next message. With MigratingThreadDeframer and
|
||||||
|
// if the data has already been recieved, every other message can be delivered instantly. This
|
||||||
|
// can be run after hasNext(), but just would be slower.
|
||||||
|
if (!(last instanceof StatusRuntimeException) && last != this) {
|
||||||
|
call.request(1);
|
||||||
|
}
|
||||||
|
if (!hasNext()) {
|
||||||
|
throw new NoSuchElementException();
|
||||||
|
}
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
T tmp = (T) last;
|
||||||
|
last = null;
|
||||||
|
return tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void remove() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class QueuingListener extends StartableListener<T> {
|
||||||
|
// Non private to avoid synthetic class
|
||||||
|
QueuingListener() {}
|
||||||
|
|
||||||
|
private boolean done = false;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onHeaders(Metadata headers) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(T value) {
|
||||||
|
Preconditions.checkState(!done, "ClientCall already closed");
|
||||||
|
buffer.add(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClose(Status status, Metadata trailers) {
|
||||||
|
Preconditions.checkState(!done, "ClientCall already closed");
|
||||||
|
if (status.isOk()) {
|
||||||
|
buffer.add(BlockingResponseStream.this);
|
||||||
|
} else {
|
||||||
|
buffer.add(status.asRuntimeException(trailers));
|
||||||
|
}
|
||||||
|
done = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void onStart() {
|
||||||
|
call.request(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
private static final class ThreadlessExecutor extends ConcurrentLinkedQueue<Runnable>
|
||||||
|
implements Executor {
|
||||||
|
private static final Logger log = Logger.getLogger(ThreadlessExecutor.class.getName());
|
||||||
|
|
||||||
|
private volatile Thread waiter;
|
||||||
|
|
||||||
|
// Non private to avoid synthetic class
|
||||||
|
ThreadlessExecutor() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits until there is a Runnable, then executes it and all queued Runnables after it. Must
|
||||||
|
* only be called by one thread at a time.
|
||||||
|
*/
|
||||||
|
public void waitAndDrain() throws InterruptedException {
|
||||||
|
throwIfInterrupted();
|
||||||
|
Runnable runnable = poll();
|
||||||
|
if (runnable == null) {
|
||||||
|
waiter = Thread.currentThread();
|
||||||
|
try {
|
||||||
|
while ((runnable = poll()) == null) {
|
||||||
|
LockSupport.park(this);
|
||||||
|
throwIfInterrupted();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
waiter = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
try {
|
||||||
|
runnable.run();
|
||||||
|
} catch (Throwable t) {
|
||||||
|
log.log(Level.WARNING, "Runnable threw exception", t);
|
||||||
|
}
|
||||||
|
} while ((runnable = poll()) != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void throwIfInterrupted() throws InterruptedException {
|
||||||
|
if (Thread.interrupted()) {
|
||||||
|
throw new InterruptedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(Runnable runnable) {
|
||||||
|
add(runnable);
|
||||||
|
LockSupport.unpark(waiter); // no-op if null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum StubType {
|
||||||
|
BLOCKING,
|
||||||
|
FUTURE,
|
||||||
|
ASYNC
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Internal {@link CallOptions.Key} to indicate stub types. */
|
||||||
|
static final CallOptions.Key<StubType> STUB_TYPE_OPTION =
|
||||||
|
CallOptions.Key.create("internal-stub-type");
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,546 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012 The Netty Project
|
||||||
|
*
|
||||||
|
* The Netty Project licenses this file to you under the Apache License,
|
||||||
|
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package io.netty.channel.socket.nio;
|
||||||
|
|
||||||
|
import static io.netty.channel.internal.ChannelUtils.MAX_BYTES_PER_GATHERING_WRITE_ATTEMPTED_LOW_THRESHOLD;
|
||||||
|
|
||||||
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import io.netty.channel.Channel;
|
||||||
|
import io.netty.channel.ChannelException;
|
||||||
|
import io.netty.channel.ChannelFuture;
|
||||||
|
import io.netty.channel.ChannelFutureListener;
|
||||||
|
import io.netty.channel.ChannelOption;
|
||||||
|
import io.netty.channel.ChannelOutboundBuffer;
|
||||||
|
import io.netty.channel.ChannelPromise;
|
||||||
|
import io.netty.channel.EventLoop;
|
||||||
|
import io.netty.channel.FileRegion;
|
||||||
|
import io.netty.channel.RecvByteBufAllocator;
|
||||||
|
import io.netty.channel.nio.AbstractNioByteChannel;
|
||||||
|
import io.netty.channel.socket.DefaultSocketChannelConfig;
|
||||||
|
import io.netty.channel.socket.ServerSocketChannel;
|
||||||
|
import io.netty.channel.socket.SocketChannelConfig;
|
||||||
|
import io.netty.util.concurrent.GlobalEventExecutor;
|
||||||
|
import io.netty.util.internal.PlatformDependent;
|
||||||
|
import io.netty.util.internal.SocketUtils;
|
||||||
|
import io.netty.util.internal.SuppressJava6Requirement;
|
||||||
|
import io.netty.util.internal.UnstableApi;
|
||||||
|
import io.netty.util.internal.logging.InternalLogger;
|
||||||
|
import io.netty.util.internal.logging.InternalLoggerFactory;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.net.SocketAddress;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.channels.SelectionKey;
|
||||||
|
import java.nio.channels.SocketChannel;
|
||||||
|
import java.nio.channels.spi.SelectorProvider;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
|
/** {@link io.netty.channel.socket.SocketChannel} which uses NIO selector based implementation. */
|
||||||
|
public class NioSocketChannel extends AbstractNioByteChannel
|
||||||
|
implements io.netty.channel.socket.SocketChannel {
|
||||||
|
private static final InternalLogger logger =
|
||||||
|
InternalLoggerFactory.getInstance(NioSocketChannel.class);
|
||||||
|
private static final SelectorProvider DEFAULT_SELECTOR_PROVIDER = SelectorProvider.provider();
|
||||||
|
|
||||||
|
private static SocketChannel newSocket(SelectorProvider provider) {
|
||||||
|
try {
|
||||||
|
/**
|
||||||
|
* Use the {@link SelectorProvider} to open {@link SocketChannel} and so remove condition in
|
||||||
|
* {@link SelectorProvider#provider()} which is called by each SocketChannel.open() otherwise.
|
||||||
|
*
|
||||||
|
* <p>See <a href="https://github.com/netty/netty/issues/2308">#2308</a>.
|
||||||
|
*/
|
||||||
|
return provider.openSocketChannel();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ChannelException("Failed to open a socket.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final SocketChannelConfig config;
|
||||||
|
|
||||||
|
/** Create a new instance */
|
||||||
|
public NioSocketChannel() {
|
||||||
|
this(DEFAULT_SELECTOR_PROVIDER);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a new instance using the given {@link SelectorProvider}. */
|
||||||
|
public NioSocketChannel(SelectorProvider provider) {
|
||||||
|
this(newSocket(provider));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a new instance using the given {@link SocketChannel}. */
|
||||||
|
public NioSocketChannel(SocketChannel socket) {
|
||||||
|
this(null, socket);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new instance
|
||||||
|
*
|
||||||
|
* @param parent the {@link Channel} which created this instance or {@code null} if it was created
|
||||||
|
* by the user
|
||||||
|
* @param socket the {@link SocketChannel} which will be used
|
||||||
|
*/
|
||||||
|
public NioSocketChannel(Channel parent, SocketChannel socket) {
|
||||||
|
super(parent, socket);
|
||||||
|
config = new NioSocketChannelConfig(this, socket.socket());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ServerSocketChannel parent() {
|
||||||
|
return (ServerSocketChannel) super.parent();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SocketChannelConfig config() {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected SocketChannel javaChannel() {
|
||||||
|
return (SocketChannel) super.javaChannel();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isActive() {
|
||||||
|
SocketChannel ch = javaChannel();
|
||||||
|
return ch.isOpen() && ch.isConnected();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isOutputShutdown() {
|
||||||
|
return javaChannel().socket().isOutputShutdown() || !isActive();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isInputShutdown() {
|
||||||
|
return javaChannel().socket().isInputShutdown() || !isActive();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isShutdown() {
|
||||||
|
Socket socket = javaChannel().socket();
|
||||||
|
return socket.isInputShutdown() && socket.isOutputShutdown() || !isActive();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InetSocketAddress localAddress() {
|
||||||
|
return (InetSocketAddress) super.localAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InetSocketAddress remoteAddress() {
|
||||||
|
return (InetSocketAddress) super.remoteAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressJava6Requirement(reason = "Usage guarded by java version check")
|
||||||
|
@UnstableApi
|
||||||
|
@Override
|
||||||
|
protected final void doShutdownOutput() throws Exception {
|
||||||
|
if (PlatformDependent.javaVersion() >= 7) {
|
||||||
|
javaChannel().shutdownOutput();
|
||||||
|
} else {
|
||||||
|
javaChannel().socket().shutdownOutput();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChannelFuture shutdownOutput() {
|
||||||
|
return shutdownOutput(newPromise());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChannelFuture shutdownOutput(final ChannelPromise promise) {
|
||||||
|
final EventLoop loop = eventLoop();
|
||||||
|
if (loop.inEventLoop()) {
|
||||||
|
((AbstractUnsafe) unsafe()).shutdownOutput(promise);
|
||||||
|
} else {
|
||||||
|
loop.execute(
|
||||||
|
new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
((AbstractUnsafe) unsafe()).shutdownOutput(promise);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChannelFuture shutdownInput() {
|
||||||
|
return shutdownInput(newPromise());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isInputShutdown0() {
|
||||||
|
return isInputShutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChannelFuture shutdownInput(final ChannelPromise promise) {
|
||||||
|
EventLoop loop = eventLoop();
|
||||||
|
if (loop.inEventLoop()) {
|
||||||
|
shutdownInput0(promise);
|
||||||
|
} else {
|
||||||
|
loop.execute(
|
||||||
|
new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
shutdownInput0(promise);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChannelFuture shutdown() {
|
||||||
|
return shutdown(newPromise());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChannelFuture shutdown(final ChannelPromise promise) {
|
||||||
|
ChannelFuture shutdownOutputFuture = shutdownOutput();
|
||||||
|
if (shutdownOutputFuture.isDone()) {
|
||||||
|
shutdownOutputDone(shutdownOutputFuture, promise);
|
||||||
|
} else {
|
||||||
|
shutdownOutputFuture.addListener(
|
||||||
|
new ChannelFutureListener() {
|
||||||
|
@Override
|
||||||
|
public void operationComplete(final ChannelFuture shutdownOutputFuture)
|
||||||
|
throws Exception {
|
||||||
|
shutdownOutputDone(shutdownOutputFuture, promise);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void shutdownOutputDone(
|
||||||
|
final ChannelFuture shutdownOutputFuture, final ChannelPromise promise) {
|
||||||
|
ChannelFuture shutdownInputFuture = shutdownInput();
|
||||||
|
if (shutdownInputFuture.isDone()) {
|
||||||
|
shutdownDone(shutdownOutputFuture, shutdownInputFuture, promise);
|
||||||
|
} else {
|
||||||
|
shutdownInputFuture.addListener(
|
||||||
|
new ChannelFutureListener() {
|
||||||
|
@Override
|
||||||
|
public void operationComplete(ChannelFuture shutdownInputFuture) throws Exception {
|
||||||
|
shutdownDone(shutdownOutputFuture, shutdownInputFuture, promise);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void shutdownDone(
|
||||||
|
ChannelFuture shutdownOutputFuture,
|
||||||
|
ChannelFuture shutdownInputFuture,
|
||||||
|
ChannelPromise promise) {
|
||||||
|
Throwable shutdownOutputCause = shutdownOutputFuture.cause();
|
||||||
|
Throwable shutdownInputCause = shutdownInputFuture.cause();
|
||||||
|
if (shutdownOutputCause != null) {
|
||||||
|
if (shutdownInputCause != null) {
|
||||||
|
logger.debug(
|
||||||
|
"Exception suppressed because a previous exception occurred.", shutdownInputCause);
|
||||||
|
}
|
||||||
|
promise.setFailure(shutdownOutputCause);
|
||||||
|
} else if (shutdownInputCause != null) {
|
||||||
|
promise.setFailure(shutdownInputCause);
|
||||||
|
} else {
|
||||||
|
promise.setSuccess();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void shutdownInput0(final ChannelPromise promise) {
|
||||||
|
try {
|
||||||
|
shutdownInput0();
|
||||||
|
promise.setSuccess();
|
||||||
|
} catch (Throwable t) {
|
||||||
|
promise.setFailure(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressJava6Requirement(reason = "Usage guarded by java version check")
|
||||||
|
private void shutdownInput0() throws Exception {
|
||||||
|
if (PlatformDependent.javaVersion() >= 7) {
|
||||||
|
javaChannel().shutdownInput();
|
||||||
|
} else {
|
||||||
|
javaChannel().socket().shutdownInput();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected SocketAddress localAddress0() {
|
||||||
|
return javaChannel().socket().getLocalSocketAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected SocketAddress remoteAddress0() {
|
||||||
|
return javaChannel().socket().getRemoteSocketAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doBind(SocketAddress localAddress) throws Exception {
|
||||||
|
doBind0(localAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doBind0(SocketAddress localAddress) throws Exception {
|
||||||
|
if (PlatformDependent.javaVersion() >= 7) {
|
||||||
|
SocketUtils.bind(javaChannel(), localAddress);
|
||||||
|
} else {
|
||||||
|
SocketUtils.bind(javaChannel().socket(), localAddress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean doConnect(SocketAddress remoteAddress, SocketAddress localAddress)
|
||||||
|
throws Exception {
|
||||||
|
if (localAddress != null) {
|
||||||
|
doBind0(localAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean success = false;
|
||||||
|
try {
|
||||||
|
boolean connected = SocketUtils.connect(javaChannel(), remoteAddress);
|
||||||
|
if (!connected) {
|
||||||
|
selectionKey().interestOps(SelectionKey.OP_CONNECT);
|
||||||
|
}
|
||||||
|
success = true;
|
||||||
|
return connected;
|
||||||
|
} finally {
|
||||||
|
if (!success) {
|
||||||
|
doClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFinishConnect() throws Exception {
|
||||||
|
if (!javaChannel().finishConnect()) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doDisconnect() throws Exception {
|
||||||
|
doClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doClose() throws Exception {
|
||||||
|
super.doClose();
|
||||||
|
javaChannel().close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int doReadBytes(ByteBuf byteBuf) throws Exception {
|
||||||
|
final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
|
||||||
|
allocHandle.attemptedBytesRead(byteBuf.writableBytes());
|
||||||
|
return byteBuf.writeBytes(javaChannel(), allocHandle.attemptedBytesRead());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int doWriteBytes(ByteBuf buf) throws Exception {
|
||||||
|
final int expectedWrittenBytes = buf.readableBytes();
|
||||||
|
return buf.readBytes(javaChannel(), expectedWrittenBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected long doWriteFileRegion(FileRegion region) throws Exception {
|
||||||
|
final long position = region.transferred();
|
||||||
|
return region.transferTo(javaChannel(), position);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void adjustMaxBytesPerGatheringWrite(
|
||||||
|
int attempted, int written, int oldMaxBytesPerGatheringWrite) {
|
||||||
|
// By default we track the SO_SNDBUF when ever it is explicitly set. However some OSes may
|
||||||
|
// dynamically change
|
||||||
|
// SO_SNDBUF (and other characteristics that determine how much data can be written at once) so
|
||||||
|
// we should try
|
||||||
|
// make a best effort to adjust as OS behavior changes.
|
||||||
|
if (attempted == written) {
|
||||||
|
if (attempted << 1 > oldMaxBytesPerGatheringWrite) {
|
||||||
|
((NioSocketChannelConfig) config).setMaxBytesPerGatheringWrite(attempted << 1);
|
||||||
|
}
|
||||||
|
} else if (attempted > MAX_BYTES_PER_GATHERING_WRITE_ATTEMPTED_LOW_THRESHOLD
|
||||||
|
&& written < attempted >>> 1) {
|
||||||
|
((NioSocketChannelConfig) config).setMaxBytesPerGatheringWrite(attempted >>> 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doWrite(ChannelOutboundBuffer in) throws Exception {
|
||||||
|
SocketChannel ch = javaChannel();
|
||||||
|
int writeSpinCount = config().getWriteSpinCount();
|
||||||
|
do {
|
||||||
|
if (in.isEmpty()) {
|
||||||
|
// All written so clear OP_WRITE
|
||||||
|
clearOpWrite();
|
||||||
|
// Directly return here so incompleteWrite(...) is not called.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the pending writes are made of ByteBufs only.
|
||||||
|
int maxBytesPerGatheringWrite =
|
||||||
|
((NioSocketChannelConfig) config).getMaxBytesPerGatheringWrite();
|
||||||
|
ByteBuffer[] nioBuffers = in.nioBuffers(1024, maxBytesPerGatheringWrite);
|
||||||
|
int nioBufferCnt = in.nioBufferCount();
|
||||||
|
|
||||||
|
// Always use nioBuffers() to workaround data-corruption.
|
||||||
|
// See https://github.com/netty/netty/issues/2761
|
||||||
|
switch (nioBufferCnt) {
|
||||||
|
case 0:
|
||||||
|
// We have something else beside ByteBuffers to write so fallback to normal writes.
|
||||||
|
writeSpinCount -= doWrite0(in);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
{
|
||||||
|
// Only one ByteBuf so use non-gathering write
|
||||||
|
// Zero length buffers are not added to nioBuffers by ChannelOutboundBuffer, so there is
|
||||||
|
// no need
|
||||||
|
// to check if the total size of all the buffers is non-zero.
|
||||||
|
ByteBuffer buffer = nioBuffers[0];
|
||||||
|
int attemptedBytes = buffer.remaining();
|
||||||
|
final int localWrittenBytes = ch.write(buffer);
|
||||||
|
if (localWrittenBytes <= 0) {
|
||||||
|
incompleteWrite(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
adjustMaxBytesPerGatheringWrite(
|
||||||
|
attemptedBytes, localWrittenBytes, maxBytesPerGatheringWrite);
|
||||||
|
in.removeBytes(localWrittenBytes);
|
||||||
|
--writeSpinCount;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
// Zero length buffers are not added to nioBuffers by ChannelOutboundBuffer, so there is
|
||||||
|
// no need
|
||||||
|
// to check if the total size of all the buffers is non-zero.
|
||||||
|
// We limit the max amount to int above so cast is safe
|
||||||
|
long attemptedBytes = in.nioBufferSize();
|
||||||
|
final long localWrittenBytes = ch.write(nioBuffers, 0, nioBufferCnt);
|
||||||
|
if (localWrittenBytes <= 0) {
|
||||||
|
incompleteWrite(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Casting to int is safe because we limit the total amount of data in the nioBuffers to
|
||||||
|
// int above.
|
||||||
|
adjustMaxBytesPerGatheringWrite(
|
||||||
|
(int) attemptedBytes, (int) localWrittenBytes, maxBytesPerGatheringWrite);
|
||||||
|
in.removeBytes(localWrittenBytes);
|
||||||
|
--writeSpinCount;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (writeSpinCount > 0);
|
||||||
|
|
||||||
|
incompleteWrite(writeSpinCount < 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected AbstractNioUnsafe newUnsafe() {
|
||||||
|
return new NioSocketChannelUnsafe();
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class NioSocketChannelUnsafe extends NioByteUnsafe {
|
||||||
|
@Override
|
||||||
|
protected Executor prepareToClose() {
|
||||||
|
try {
|
||||||
|
if (javaChannel().isOpen() && config().getSoLinger() > 0) {
|
||||||
|
// We need to cancel this key of the channel so we may not end up in a eventloop spin
|
||||||
|
// because we try to read or write until the actual close happens which may be later due
|
||||||
|
// SO_LINGER handling.
|
||||||
|
// See https://github.com/netty/netty/issues/4449
|
||||||
|
doDeregister();
|
||||||
|
return GlobalEventExecutor.INSTANCE;
|
||||||
|
}
|
||||||
|
} catch (Throwable ignore) {
|
||||||
|
// Ignore the error as the underlying channel may be closed in the meantime and so
|
||||||
|
// getSoLinger() may produce an exception. In this case we just return null.
|
||||||
|
// See https://github.com/netty/netty/issues/4449
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class NioSocketChannelConfig extends DefaultSocketChannelConfig {
|
||||||
|
private volatile int maxBytesPerGatheringWrite = Integer.MAX_VALUE;
|
||||||
|
|
||||||
|
private NioSocketChannelConfig(NioSocketChannel channel, Socket javaSocket) {
|
||||||
|
super(channel, javaSocket);
|
||||||
|
calculateMaxBytesPerGatheringWrite();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void autoReadCleared() {
|
||||||
|
clearReadPending();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NioSocketChannelConfig setSendBufferSize(int sendBufferSize) {
|
||||||
|
super.setSendBufferSize(sendBufferSize);
|
||||||
|
calculateMaxBytesPerGatheringWrite();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> boolean setOption(ChannelOption<T> option, T value) {
|
||||||
|
if (PlatformDependent.javaVersion() >= 7 && option instanceof NioChannelOption) {
|
||||||
|
return NioChannelOption.setOption(jdkChannel(), (NioChannelOption<T>) option, value);
|
||||||
|
}
|
||||||
|
return super.setOption(option, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T getOption(ChannelOption<T> option) {
|
||||||
|
if (PlatformDependent.javaVersion() >= 7 && option instanceof NioChannelOption) {
|
||||||
|
return NioChannelOption.getOption(jdkChannel(), (NioChannelOption<T>) option);
|
||||||
|
}
|
||||||
|
return super.getOption(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<ChannelOption<?>, Object> getOptions() {
|
||||||
|
if (PlatformDependent.javaVersion() >= 7) {
|
||||||
|
return getOptions(super.getOptions(), NioChannelOption.getOptions(jdkChannel()));
|
||||||
|
}
|
||||||
|
return super.getOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setMaxBytesPerGatheringWrite(int maxBytesPerGatheringWrite) {
|
||||||
|
this.maxBytesPerGatheringWrite = maxBytesPerGatheringWrite;
|
||||||
|
}
|
||||||
|
|
||||||
|
int getMaxBytesPerGatheringWrite() {
|
||||||
|
return maxBytesPerGatheringWrite;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void calculateMaxBytesPerGatheringWrite() {
|
||||||
|
// Multiply by 2 to give some extra space in case the OS can process write data faster than we
|
||||||
|
// can provide.
|
||||||
|
int newSendBufferSize = getSendBufferSize() << 1;
|
||||||
|
if (newSendBufferSize > 0) {
|
||||||
|
setMaxBytesPerGatheringWrite(newSendBufferSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SocketChannel jdkChannel() {
|
||||||
|
return ((NioSocketChannel) channel).javaChannel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,708 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2014 The Netty Project
|
||||||
|
*
|
||||||
|
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License. You may obtain a
|
||||||
|
* copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||||
|
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||||
|
* or implied. See the License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.netty.handler.codec.http2;
|
||||||
|
|
||||||
|
import static io.netty.buffer.Unpooled.directBuffer;
|
||||||
|
import static io.netty.buffer.Unpooled.unreleasableBuffer;
|
||||||
|
import static io.netty.handler.codec.http2.Http2CodecUtil.CONTINUATION_FRAME_HEADER_LENGTH;
|
||||||
|
import static io.netty.handler.codec.http2.Http2CodecUtil.DATA_FRAME_HEADER_LENGTH;
|
||||||
|
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_MAX_FRAME_SIZE;
|
||||||
|
import static io.netty.handler.codec.http2.Http2CodecUtil.FRAME_HEADER_LENGTH;
|
||||||
|
import static io.netty.handler.codec.http2.Http2CodecUtil.GO_AWAY_FRAME_HEADER_LENGTH;
|
||||||
|
import static io.netty.handler.codec.http2.Http2CodecUtil.HEADERS_FRAME_HEADER_LENGTH;
|
||||||
|
import static io.netty.handler.codec.http2.Http2CodecUtil.INT_FIELD_LENGTH;
|
||||||
|
import static io.netty.handler.codec.http2.Http2CodecUtil.MAX_UNSIGNED_BYTE;
|
||||||
|
import static io.netty.handler.codec.http2.Http2CodecUtil.MAX_UNSIGNED_INT;
|
||||||
|
import static io.netty.handler.codec.http2.Http2CodecUtil.MAX_WEIGHT;
|
||||||
|
import static io.netty.handler.codec.http2.Http2CodecUtil.MIN_WEIGHT;
|
||||||
|
import static io.netty.handler.codec.http2.Http2CodecUtil.PING_FRAME_PAYLOAD_LENGTH;
|
||||||
|
import static io.netty.handler.codec.http2.Http2CodecUtil.PRIORITY_ENTRY_LENGTH;
|
||||||
|
import static io.netty.handler.codec.http2.Http2CodecUtil.PRIORITY_FRAME_LENGTH;
|
||||||
|
import static io.netty.handler.codec.http2.Http2CodecUtil.PUSH_PROMISE_FRAME_HEADER_LENGTH;
|
||||||
|
import static io.netty.handler.codec.http2.Http2CodecUtil.RST_STREAM_FRAME_LENGTH;
|
||||||
|
import static io.netty.handler.codec.http2.Http2CodecUtil.SETTING_ENTRY_LENGTH;
|
||||||
|
import static io.netty.handler.codec.http2.Http2CodecUtil.WINDOW_UPDATE_FRAME_LENGTH;
|
||||||
|
import static io.netty.handler.codec.http2.Http2CodecUtil.isMaxFrameSizeValid;
|
||||||
|
import static io.netty.handler.codec.http2.Http2CodecUtil.verifyPadding;
|
||||||
|
import static io.netty.handler.codec.http2.Http2CodecUtil.writeFrameHeaderInternal;
|
||||||
|
import static io.netty.handler.codec.http2.Http2Error.FRAME_SIZE_ERROR;
|
||||||
|
import static io.netty.handler.codec.http2.Http2Exception.connectionError;
|
||||||
|
import static io.netty.handler.codec.http2.Http2FrameTypes.CONTINUATION;
|
||||||
|
import static io.netty.handler.codec.http2.Http2FrameTypes.DATA;
|
||||||
|
import static io.netty.handler.codec.http2.Http2FrameTypes.GO_AWAY;
|
||||||
|
import static io.netty.handler.codec.http2.Http2FrameTypes.HEADERS;
|
||||||
|
import static io.netty.handler.codec.http2.Http2FrameTypes.PING;
|
||||||
|
import static io.netty.handler.codec.http2.Http2FrameTypes.PRIORITY;
|
||||||
|
import static io.netty.handler.codec.http2.Http2FrameTypes.PUSH_PROMISE;
|
||||||
|
import static io.netty.handler.codec.http2.Http2FrameTypes.RST_STREAM;
|
||||||
|
import static io.netty.handler.codec.http2.Http2FrameTypes.SETTINGS;
|
||||||
|
import static io.netty.handler.codec.http2.Http2FrameTypes.WINDOW_UPDATE;
|
||||||
|
import static io.netty.util.internal.ObjectUtil.checkNotNull;
|
||||||
|
import static io.netty.util.internal.ObjectUtil.checkPositive;
|
||||||
|
import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
|
||||||
|
import static java.lang.Math.max;
|
||||||
|
import static java.lang.Math.min;
|
||||||
|
|
||||||
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import io.netty.channel.ChannelFuture;
|
||||||
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
|
import io.netty.channel.ChannelPromise;
|
||||||
|
import io.netty.handler.codec.http2.Http2CodecUtil.SimpleChannelPromiseAggregator;
|
||||||
|
import io.netty.handler.codec.http2.Http2FrameWriter.Configuration;
|
||||||
|
import io.netty.handler.codec.http2.Http2HeadersEncoder.SensitivityDetector;
|
||||||
|
import io.netty.util.internal.PlatformDependent;
|
||||||
|
import io.netty.util.internal.UnstableApi;
|
||||||
|
|
||||||
|
/** A {@link Http2FrameWriter} that supports all frame types defined by the HTTP/2 specification. */
|
||||||
|
@UnstableApi
|
||||||
|
public class DefaultHttp2FrameWriter
|
||||||
|
implements Http2FrameWriter, Http2FrameSizePolicy, Configuration {
|
||||||
|
private static final String STREAM_ID = "Stream ID";
|
||||||
|
private static final String STREAM_DEPENDENCY = "Stream Dependency";
|
||||||
|
/**
|
||||||
|
* This buffer is allocated to the maximum size of the padding field, and filled with zeros. When
|
||||||
|
* padding is needed it can be taken as a slice of this buffer. Users should call {@link
|
||||||
|
* ByteBuf#retain()} before using their slice.
|
||||||
|
*/
|
||||||
|
private static final ByteBuf ZERO_BUFFER =
|
||||||
|
unreleasableBuffer(directBuffer(MAX_UNSIGNED_BYTE).writeZero(MAX_UNSIGNED_BYTE)).asReadOnly();
|
||||||
|
|
||||||
|
private final Http2HeadersEncoder headersEncoder;
|
||||||
|
private int maxFrameSize;
|
||||||
|
|
||||||
|
public DefaultHttp2FrameWriter() {
|
||||||
|
this(new DefaultHttp2HeadersEncoder());
|
||||||
|
}
|
||||||
|
|
||||||
|
public DefaultHttp2FrameWriter(SensitivityDetector headersSensitivityDetector) {
|
||||||
|
this(new DefaultHttp2HeadersEncoder(headersSensitivityDetector));
|
||||||
|
}
|
||||||
|
|
||||||
|
public DefaultHttp2FrameWriter(
|
||||||
|
SensitivityDetector headersSensitivityDetector, boolean ignoreMaxHeaderListSize) {
|
||||||
|
this(new DefaultHttp2HeadersEncoder(headersSensitivityDetector, ignoreMaxHeaderListSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
public DefaultHttp2FrameWriter(Http2HeadersEncoder headersEncoder) {
|
||||||
|
this.headersEncoder = headersEncoder;
|
||||||
|
maxFrameSize = DEFAULT_MAX_FRAME_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Configuration configuration() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Http2HeadersEncoder.Configuration headersConfiguration() {
|
||||||
|
return headersEncoder.configuration();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Http2FrameSizePolicy frameSizePolicy() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void maxFrameSize(int max) throws Http2Exception {
|
||||||
|
if (!isMaxFrameSizeValid(max)) {
|
||||||
|
throw connectionError(
|
||||||
|
FRAME_SIZE_ERROR, "Invalid MAX_FRAME_SIZE specified in sent settings: %d", max);
|
||||||
|
}
|
||||||
|
maxFrameSize = max;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int maxFrameSize() {
|
||||||
|
return maxFrameSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChannelFuture writeData(
|
||||||
|
ChannelHandlerContext ctx,
|
||||||
|
int streamId,
|
||||||
|
ByteBuf data,
|
||||||
|
int padding,
|
||||||
|
boolean endStream,
|
||||||
|
ChannelPromise promise) {
|
||||||
|
final SimpleChannelPromiseAggregator promiseAggregator =
|
||||||
|
new SimpleChannelPromiseAggregator(promise, ctx.channel(), ctx.executor());
|
||||||
|
ByteBuf frameHeader = null;
|
||||||
|
try {
|
||||||
|
verifyStreamId(streamId, STREAM_ID);
|
||||||
|
verifyPadding(padding);
|
||||||
|
|
||||||
|
int remainingData = data.readableBytes();
|
||||||
|
Http2Flags flags = new Http2Flags();
|
||||||
|
flags.endOfStream(false);
|
||||||
|
flags.paddingPresent(false);
|
||||||
|
// Fast path to write frames of payload size maxFrameSize first.
|
||||||
|
if (remainingData > maxFrameSize) {
|
||||||
|
frameHeader = ctx.alloc().buffer(FRAME_HEADER_LENGTH);
|
||||||
|
writeFrameHeaderInternal(frameHeader, maxFrameSize, DATA, flags, streamId);
|
||||||
|
do {
|
||||||
|
// Write the header.
|
||||||
|
ctx.write(frameHeader.retainedSlice(), promiseAggregator.newPromise());
|
||||||
|
|
||||||
|
// Write the payload.
|
||||||
|
ctx.write(data.readRetainedSlice(maxFrameSize), promiseAggregator.newPromise());
|
||||||
|
|
||||||
|
remainingData -= maxFrameSize;
|
||||||
|
// Stop iterating if remainingData == maxFrameSize so we can take care of reference counts
|
||||||
|
// below.
|
||||||
|
} while (remainingData > maxFrameSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (padding == 0) {
|
||||||
|
// Write the header.
|
||||||
|
if (frameHeader != null) {
|
||||||
|
frameHeader.release();
|
||||||
|
frameHeader = null;
|
||||||
|
}
|
||||||
|
ByteBuf frameHeader2 = ctx.alloc().buffer(FRAME_HEADER_LENGTH);
|
||||||
|
flags.endOfStream(endStream);
|
||||||
|
writeFrameHeaderInternal(frameHeader2, remainingData, DATA, flags, streamId);
|
||||||
|
ctx.write(frameHeader2, promiseAggregator.newPromise());
|
||||||
|
|
||||||
|
// Write the payload.
|
||||||
|
ByteBuf lastFrame = data.readSlice(remainingData);
|
||||||
|
data = null;
|
||||||
|
ctx.write(lastFrame, promiseAggregator.newPromise());
|
||||||
|
} else {
|
||||||
|
if (remainingData != maxFrameSize) {
|
||||||
|
if (frameHeader != null) {
|
||||||
|
frameHeader.release();
|
||||||
|
frameHeader = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
remainingData -= maxFrameSize;
|
||||||
|
// Write the header.
|
||||||
|
ByteBuf lastFrame;
|
||||||
|
if (frameHeader == null) {
|
||||||
|
lastFrame = ctx.alloc().buffer(FRAME_HEADER_LENGTH);
|
||||||
|
writeFrameHeaderInternal(lastFrame, maxFrameSize, DATA, flags, streamId);
|
||||||
|
} else {
|
||||||
|
lastFrame = frameHeader.slice();
|
||||||
|
frameHeader = null;
|
||||||
|
}
|
||||||
|
ctx.write(lastFrame, promiseAggregator.newPromise());
|
||||||
|
|
||||||
|
// Write the payload.
|
||||||
|
lastFrame = data.readableBytes() != maxFrameSize ? data.readSlice(maxFrameSize) : data;
|
||||||
|
data = null;
|
||||||
|
ctx.write(lastFrame, promiseAggregator.newPromise());
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
int frameDataBytes = min(remainingData, maxFrameSize);
|
||||||
|
int framePaddingBytes = min(padding, max(0, (maxFrameSize - 1) - frameDataBytes));
|
||||||
|
|
||||||
|
// Decrement the remaining counters.
|
||||||
|
padding -= framePaddingBytes;
|
||||||
|
remainingData -= frameDataBytes;
|
||||||
|
|
||||||
|
// Write the header.
|
||||||
|
ByteBuf frameHeader2 = ctx.alloc().buffer(DATA_FRAME_HEADER_LENGTH);
|
||||||
|
flags.endOfStream(endStream && remainingData == 0 && padding == 0);
|
||||||
|
flags.paddingPresent(framePaddingBytes > 0);
|
||||||
|
writeFrameHeaderInternal(
|
||||||
|
frameHeader2, framePaddingBytes + frameDataBytes, DATA, flags, streamId);
|
||||||
|
writePaddingLength(frameHeader2, framePaddingBytes);
|
||||||
|
ctx.write(frameHeader2, promiseAggregator.newPromise());
|
||||||
|
|
||||||
|
// Write the payload.
|
||||||
|
if (frameDataBytes != 0) {
|
||||||
|
if (remainingData == 0) {
|
||||||
|
ByteBuf lastFrame = data.readSlice(frameDataBytes);
|
||||||
|
data = null;
|
||||||
|
ctx.write(lastFrame, promiseAggregator.newPromise());
|
||||||
|
} else {
|
||||||
|
ctx.write(data.readRetainedSlice(frameDataBytes), promiseAggregator.newPromise());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Write the frame padding.
|
||||||
|
if (paddingBytes(framePaddingBytes) > 0) {
|
||||||
|
ctx.write(
|
||||||
|
ZERO_BUFFER.slice(0, paddingBytes(framePaddingBytes)),
|
||||||
|
promiseAggregator.newPromise());
|
||||||
|
}
|
||||||
|
} while (remainingData != 0 || padding != 0);
|
||||||
|
}
|
||||||
|
} catch (Throwable cause) {
|
||||||
|
if (frameHeader != null) {
|
||||||
|
frameHeader.release();
|
||||||
|
}
|
||||||
|
// Use a try/finally here in case the data has been released before calling this method. This
|
||||||
|
// is not
|
||||||
|
// necessary above because we internally allocate frameHeader.
|
||||||
|
try {
|
||||||
|
if (data != null) {
|
||||||
|
data.release();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
promiseAggregator.setFailure(cause);
|
||||||
|
promiseAggregator.doneAllocatingPromises();
|
||||||
|
}
|
||||||
|
return promiseAggregator;
|
||||||
|
}
|
||||||
|
return promiseAggregator.doneAllocatingPromises();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChannelFuture writeHeaders(
|
||||||
|
ChannelHandlerContext ctx,
|
||||||
|
int streamId,
|
||||||
|
Http2Headers headers,
|
||||||
|
int padding,
|
||||||
|
boolean endStream,
|
||||||
|
ChannelPromise promise) {
|
||||||
|
return writeHeadersInternal(
|
||||||
|
ctx, streamId, headers, padding, endStream, false, 0, (short) 0, false, promise);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChannelFuture writeHeaders(
|
||||||
|
ChannelHandlerContext ctx,
|
||||||
|
int streamId,
|
||||||
|
Http2Headers headers,
|
||||||
|
int streamDependency,
|
||||||
|
short weight,
|
||||||
|
boolean exclusive,
|
||||||
|
int padding,
|
||||||
|
boolean endStream,
|
||||||
|
ChannelPromise promise) {
|
||||||
|
return writeHeadersInternal(
|
||||||
|
ctx,
|
||||||
|
streamId,
|
||||||
|
headers,
|
||||||
|
padding,
|
||||||
|
endStream,
|
||||||
|
true,
|
||||||
|
streamDependency,
|
||||||
|
weight,
|
||||||
|
exclusive,
|
||||||
|
promise);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChannelFuture writePriority(
|
||||||
|
ChannelHandlerContext ctx,
|
||||||
|
int streamId,
|
||||||
|
int streamDependency,
|
||||||
|
short weight,
|
||||||
|
boolean exclusive,
|
||||||
|
ChannelPromise promise) {
|
||||||
|
try {
|
||||||
|
verifyStreamId(streamId, STREAM_ID);
|
||||||
|
verifyStreamOrConnectionId(streamDependency, STREAM_DEPENDENCY);
|
||||||
|
verifyWeight(weight);
|
||||||
|
|
||||||
|
ByteBuf buf = ctx.alloc().buffer(PRIORITY_FRAME_LENGTH);
|
||||||
|
writeFrameHeaderInternal(buf, PRIORITY_ENTRY_LENGTH, PRIORITY, new Http2Flags(), streamId);
|
||||||
|
buf.writeInt(exclusive ? (int) (0x80000000L | streamDependency) : streamDependency);
|
||||||
|
// Adjust the weight so that it fits into a single byte on the wire.
|
||||||
|
buf.writeByte(weight - 1);
|
||||||
|
return ctx.write(buf, promise);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
return promise.setFailure(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChannelFuture writeRstStream(
|
||||||
|
ChannelHandlerContext ctx, int streamId, long errorCode, ChannelPromise promise) {
|
||||||
|
try {
|
||||||
|
verifyStreamId(streamId, STREAM_ID);
|
||||||
|
verifyErrorCode(errorCode);
|
||||||
|
|
||||||
|
ByteBuf buf = ctx.alloc().buffer(RST_STREAM_FRAME_LENGTH);
|
||||||
|
writeFrameHeaderInternal(buf, INT_FIELD_LENGTH, RST_STREAM, new Http2Flags(), streamId);
|
||||||
|
buf.writeInt((int) errorCode);
|
||||||
|
return ctx.write(buf, promise);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
return promise.setFailure(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChannelFuture writeSettings(
|
||||||
|
ChannelHandlerContext ctx, Http2Settings settings, ChannelPromise promise) {
|
||||||
|
try {
|
||||||
|
checkNotNull(settings, "settings");
|
||||||
|
int payloadLength = SETTING_ENTRY_LENGTH * settings.size();
|
||||||
|
ByteBuf buf =
|
||||||
|
ctx.alloc().buffer(FRAME_HEADER_LENGTH + settings.size() * SETTING_ENTRY_LENGTH);
|
||||||
|
writeFrameHeaderInternal(buf, payloadLength, SETTINGS, new Http2Flags(), 0);
|
||||||
|
for (Http2Settings.PrimitiveEntry<Long> entry : settings.entries()) {
|
||||||
|
buf.writeChar(entry.key());
|
||||||
|
buf.writeInt(entry.value().intValue());
|
||||||
|
}
|
||||||
|
return ctx.write(buf, promise);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
return promise.setFailure(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChannelFuture writeSettingsAck(ChannelHandlerContext ctx, ChannelPromise promise) {
|
||||||
|
try {
|
||||||
|
ByteBuf buf = ctx.alloc().buffer(FRAME_HEADER_LENGTH);
|
||||||
|
writeFrameHeaderInternal(buf, 0, SETTINGS, new Http2Flags().ack(true), 0);
|
||||||
|
return ctx.write(buf, promise);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
return promise.setFailure(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChannelFuture writePing(
|
||||||
|
ChannelHandlerContext ctx, boolean ack, long data, ChannelPromise promise) {
|
||||||
|
Http2Flags flags = ack ? new Http2Flags().ack(true) : new Http2Flags();
|
||||||
|
ByteBuf buf = ctx.alloc().buffer(FRAME_HEADER_LENGTH + PING_FRAME_PAYLOAD_LENGTH);
|
||||||
|
// Assume nothing below will throw until buf is written. That way we don't have to take care of
|
||||||
|
// ownership
|
||||||
|
// in the catch block.
|
||||||
|
writeFrameHeaderInternal(buf, PING_FRAME_PAYLOAD_LENGTH, PING, flags, 0);
|
||||||
|
buf.writeLong(data);
|
||||||
|
return ctx.write(buf, promise);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChannelFuture writePushPromise(
|
||||||
|
ChannelHandlerContext ctx,
|
||||||
|
int streamId,
|
||||||
|
int promisedStreamId,
|
||||||
|
Http2Headers headers,
|
||||||
|
int padding,
|
||||||
|
ChannelPromise promise) {
|
||||||
|
ByteBuf headerBlock = null;
|
||||||
|
SimpleChannelPromiseAggregator promiseAggregator =
|
||||||
|
new SimpleChannelPromiseAggregator(promise, ctx.channel(), ctx.executor());
|
||||||
|
try {
|
||||||
|
verifyStreamId(streamId, STREAM_ID);
|
||||||
|
verifyStreamId(promisedStreamId, "Promised Stream ID");
|
||||||
|
verifyPadding(padding);
|
||||||
|
|
||||||
|
// Encode the entire header block into an intermediate buffer.
|
||||||
|
headerBlock = ctx.alloc().buffer();
|
||||||
|
headersEncoder.encodeHeaders(streamId, headers, headerBlock);
|
||||||
|
|
||||||
|
// Read the first fragment (possibly everything).
|
||||||
|
Http2Flags flags = new Http2Flags().paddingPresent(padding > 0);
|
||||||
|
// INT_FIELD_LENGTH is for the length of the promisedStreamId
|
||||||
|
int nonFragmentLength = INT_FIELD_LENGTH + padding;
|
||||||
|
int maxFragmentLength = maxFrameSize - nonFragmentLength;
|
||||||
|
ByteBuf fragment =
|
||||||
|
headerBlock.readRetainedSlice(min(headerBlock.readableBytes(), maxFragmentLength));
|
||||||
|
|
||||||
|
flags.endOfHeaders(!headerBlock.isReadable());
|
||||||
|
|
||||||
|
int payloadLength = fragment.readableBytes() + nonFragmentLength;
|
||||||
|
ByteBuf buf = ctx.alloc().buffer(PUSH_PROMISE_FRAME_HEADER_LENGTH);
|
||||||
|
writeFrameHeaderInternal(buf, payloadLength, PUSH_PROMISE, flags, streamId);
|
||||||
|
writePaddingLength(buf, padding);
|
||||||
|
|
||||||
|
// Write out the promised stream ID.
|
||||||
|
buf.writeInt(promisedStreamId);
|
||||||
|
ctx.write(buf, promiseAggregator.newPromise());
|
||||||
|
|
||||||
|
// Write the first fragment.
|
||||||
|
ctx.write(fragment, promiseAggregator.newPromise());
|
||||||
|
|
||||||
|
// Write out the padding, if any.
|
||||||
|
if (paddingBytes(padding) > 0) {
|
||||||
|
ctx.write(ZERO_BUFFER.slice(0, paddingBytes(padding)), promiseAggregator.newPromise());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!flags.endOfHeaders()) {
|
||||||
|
writeContinuationFrames(ctx, streamId, headerBlock, promiseAggregator);
|
||||||
|
}
|
||||||
|
} catch (Http2Exception e) {
|
||||||
|
promiseAggregator.setFailure(e);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
promiseAggregator.setFailure(t);
|
||||||
|
promiseAggregator.doneAllocatingPromises();
|
||||||
|
PlatformDependent.throwException(t);
|
||||||
|
} finally {
|
||||||
|
if (headerBlock != null) {
|
||||||
|
headerBlock.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return promiseAggregator.doneAllocatingPromises();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChannelFuture writeGoAway(
|
||||||
|
ChannelHandlerContext ctx,
|
||||||
|
int lastStreamId,
|
||||||
|
long errorCode,
|
||||||
|
ByteBuf debugData,
|
||||||
|
ChannelPromise promise) {
|
||||||
|
SimpleChannelPromiseAggregator promiseAggregator =
|
||||||
|
new SimpleChannelPromiseAggregator(promise, ctx.channel(), ctx.executor());
|
||||||
|
try {
|
||||||
|
verifyStreamOrConnectionId(lastStreamId, "Last Stream ID");
|
||||||
|
verifyErrorCode(errorCode);
|
||||||
|
|
||||||
|
int payloadLength = 8 + debugData.readableBytes();
|
||||||
|
ByteBuf buf = ctx.alloc().buffer(GO_AWAY_FRAME_HEADER_LENGTH);
|
||||||
|
// Assume nothing below will throw until buf is written. That way we don't have to take care
|
||||||
|
// of ownership
|
||||||
|
// in the catch block.
|
||||||
|
writeFrameHeaderInternal(buf, payloadLength, GO_AWAY, new Http2Flags(), 0);
|
||||||
|
buf.writeInt(lastStreamId);
|
||||||
|
buf.writeInt((int) errorCode);
|
||||||
|
ctx.write(buf, promiseAggregator.newPromise());
|
||||||
|
} catch (Throwable t) {
|
||||||
|
try {
|
||||||
|
debugData.release();
|
||||||
|
} finally {
|
||||||
|
promiseAggregator.setFailure(t);
|
||||||
|
promiseAggregator.doneAllocatingPromises();
|
||||||
|
}
|
||||||
|
return promiseAggregator;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
ctx.write(debugData, promiseAggregator.newPromise());
|
||||||
|
} catch (Throwable t) {
|
||||||
|
promiseAggregator.setFailure(t);
|
||||||
|
}
|
||||||
|
return promiseAggregator.doneAllocatingPromises();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChannelFuture writeWindowUpdate(
|
||||||
|
ChannelHandlerContext ctx, int streamId, int windowSizeIncrement, ChannelPromise promise) {
|
||||||
|
try {
|
||||||
|
verifyStreamOrConnectionId(streamId, STREAM_ID);
|
||||||
|
verifyWindowSizeIncrement(windowSizeIncrement);
|
||||||
|
|
||||||
|
ByteBuf buf = ctx.alloc().buffer(WINDOW_UPDATE_FRAME_LENGTH);
|
||||||
|
writeFrameHeaderInternal(buf, INT_FIELD_LENGTH, WINDOW_UPDATE, new Http2Flags(), streamId);
|
||||||
|
buf.writeInt(windowSizeIncrement);
|
||||||
|
return ctx.write(buf, promise);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
return promise.setFailure(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChannelFuture writeFrame(
|
||||||
|
ChannelHandlerContext ctx,
|
||||||
|
byte frameType,
|
||||||
|
int streamId,
|
||||||
|
Http2Flags flags,
|
||||||
|
ByteBuf payload,
|
||||||
|
ChannelPromise promise) {
|
||||||
|
SimpleChannelPromiseAggregator promiseAggregator =
|
||||||
|
new SimpleChannelPromiseAggregator(promise, ctx.channel(), ctx.executor());
|
||||||
|
try {
|
||||||
|
verifyStreamOrConnectionId(streamId, STREAM_ID);
|
||||||
|
ByteBuf buf = ctx.alloc().buffer(FRAME_HEADER_LENGTH);
|
||||||
|
// Assume nothing below will throw until buf is written. That way we don't have to take care
|
||||||
|
// of ownership
|
||||||
|
// in the catch block.
|
||||||
|
writeFrameHeaderInternal(buf, payload.readableBytes(), frameType, flags, streamId);
|
||||||
|
ctx.write(buf, promiseAggregator.newPromise());
|
||||||
|
} catch (Throwable t) {
|
||||||
|
try {
|
||||||
|
payload.release();
|
||||||
|
} finally {
|
||||||
|
promiseAggregator.setFailure(t);
|
||||||
|
promiseAggregator.doneAllocatingPromises();
|
||||||
|
}
|
||||||
|
return promiseAggregator;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
ctx.write(payload, promiseAggregator.newPromise());
|
||||||
|
} catch (Throwable t) {
|
||||||
|
promiseAggregator.setFailure(t);
|
||||||
|
}
|
||||||
|
return promiseAggregator.doneAllocatingPromises();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChannelFuture writeHeadersInternal(
|
||||||
|
ChannelHandlerContext ctx,
|
||||||
|
int streamId,
|
||||||
|
Http2Headers headers,
|
||||||
|
int padding,
|
||||||
|
boolean endStream,
|
||||||
|
boolean hasPriority,
|
||||||
|
int streamDependency,
|
||||||
|
short weight,
|
||||||
|
boolean exclusive,
|
||||||
|
ChannelPromise promise) {
|
||||||
|
ByteBuf headerBlock = null;
|
||||||
|
SimpleChannelPromiseAggregator promiseAggregator =
|
||||||
|
new SimpleChannelPromiseAggregator(promise, ctx.channel(), ctx.executor());
|
||||||
|
try {
|
||||||
|
verifyStreamId(streamId, STREAM_ID);
|
||||||
|
if (hasPriority) {
|
||||||
|
verifyStreamOrConnectionId(streamDependency, STREAM_DEPENDENCY);
|
||||||
|
verifyPadding(padding);
|
||||||
|
verifyWeight(weight);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode the entire header block.
|
||||||
|
headerBlock = ctx.alloc().buffer();
|
||||||
|
headersEncoder.encodeHeaders(streamId, headers, headerBlock);
|
||||||
|
|
||||||
|
Http2Flags flags =
|
||||||
|
new Http2Flags()
|
||||||
|
.endOfStream(endStream)
|
||||||
|
.priorityPresent(hasPriority)
|
||||||
|
.paddingPresent(padding > 0);
|
||||||
|
|
||||||
|
// Read the first fragment (possibly everything).
|
||||||
|
int nonFragmentBytes = padding + flags.getNumPriorityBytes();
|
||||||
|
int maxFragmentLength = maxFrameSize - nonFragmentBytes;
|
||||||
|
ByteBuf fragment =
|
||||||
|
headerBlock.readRetainedSlice(min(headerBlock.readableBytes(), maxFragmentLength));
|
||||||
|
|
||||||
|
// Set the end of headers flag for the first frame.
|
||||||
|
flags.endOfHeaders(!headerBlock.isReadable());
|
||||||
|
|
||||||
|
int payloadLength = fragment.readableBytes() + nonFragmentBytes;
|
||||||
|
ByteBuf buf = ctx.alloc().buffer(HEADERS_FRAME_HEADER_LENGTH);
|
||||||
|
writeFrameHeaderInternal(buf, payloadLength, HEADERS, flags, streamId);
|
||||||
|
writePaddingLength(buf, padding);
|
||||||
|
|
||||||
|
if (hasPriority) {
|
||||||
|
buf.writeInt(exclusive ? (int) (0x80000000L | streamDependency) : streamDependency);
|
||||||
|
|
||||||
|
// Adjust the weight so that it fits into a single byte on the wire.
|
||||||
|
buf.writeByte(weight - 1);
|
||||||
|
}
|
||||||
|
ctx.write(buf, promiseAggregator.newPromise());
|
||||||
|
|
||||||
|
// Write the first fragment.
|
||||||
|
ctx.write(fragment, promiseAggregator.newPromise());
|
||||||
|
|
||||||
|
// Write out the padding, if any.
|
||||||
|
if (paddingBytes(padding) > 0) {
|
||||||
|
ctx.write(ZERO_BUFFER.slice(0, paddingBytes(padding)), promiseAggregator.newPromise());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!flags.endOfHeaders()) {
|
||||||
|
writeContinuationFrames(ctx, streamId, headerBlock, promiseAggregator);
|
||||||
|
}
|
||||||
|
} catch (Http2Exception e) {
|
||||||
|
promiseAggregator.setFailure(e);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
promiseAggregator.setFailure(t);
|
||||||
|
promiseAggregator.doneAllocatingPromises();
|
||||||
|
PlatformDependent.throwException(t);
|
||||||
|
} finally {
|
||||||
|
if (headerBlock != null) {
|
||||||
|
headerBlock.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return promiseAggregator.doneAllocatingPromises();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes as many continuation frames as needed until {@code padding} and {@code headerBlock} are
|
||||||
|
* consumed.
|
||||||
|
*/
|
||||||
|
private ChannelFuture writeContinuationFrames(
|
||||||
|
ChannelHandlerContext ctx,
|
||||||
|
int streamId,
|
||||||
|
ByteBuf headerBlock,
|
||||||
|
SimpleChannelPromiseAggregator promiseAggregator) {
|
||||||
|
Http2Flags flags = new Http2Flags();
|
||||||
|
|
||||||
|
if (headerBlock.isReadable()) {
|
||||||
|
// The frame header (and padding) only changes on the last frame, so allocate it once and
|
||||||
|
// re-use
|
||||||
|
int fragmentReadableBytes = min(headerBlock.readableBytes(), maxFrameSize);
|
||||||
|
ByteBuf buf = ctx.alloc().buffer(CONTINUATION_FRAME_HEADER_LENGTH);
|
||||||
|
writeFrameHeaderInternal(buf, fragmentReadableBytes, CONTINUATION, flags, streamId);
|
||||||
|
|
||||||
|
do {
|
||||||
|
fragmentReadableBytes = min(headerBlock.readableBytes(), maxFrameSize);
|
||||||
|
ByteBuf fragment = headerBlock.readRetainedSlice(fragmentReadableBytes);
|
||||||
|
|
||||||
|
if (headerBlock.isReadable()) {
|
||||||
|
ctx.write(buf.retain(), promiseAggregator.newPromise());
|
||||||
|
} else {
|
||||||
|
// The frame header is different for the last frame, so re-allocate and release the old
|
||||||
|
// buffer
|
||||||
|
flags = flags.endOfHeaders(true);
|
||||||
|
buf.release();
|
||||||
|
buf = ctx.alloc().buffer(CONTINUATION_FRAME_HEADER_LENGTH);
|
||||||
|
writeFrameHeaderInternal(buf, fragmentReadableBytes, CONTINUATION, flags, streamId);
|
||||||
|
ctx.write(buf, promiseAggregator.newPromise());
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.write(fragment, promiseAggregator.newPromise());
|
||||||
|
|
||||||
|
} while (headerBlock.isReadable());
|
||||||
|
}
|
||||||
|
return promiseAggregator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the number of padding bytes that should be appended to the end of a frame. */
|
||||||
|
private static int paddingBytes(int padding) {
|
||||||
|
// The padding parameter contains the 1 byte pad length field as well as the trailing padding
|
||||||
|
// bytes.
|
||||||
|
// Subtract 1, so to only get the number of padding bytes that need to be appended to the end of
|
||||||
|
// a frame.
|
||||||
|
return padding - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writePaddingLength(ByteBuf buf, int padding) {
|
||||||
|
if (padding > 0) {
|
||||||
|
// It is assumed that the padding length has been bounds checked before this
|
||||||
|
// Minus 1, as the pad length field is included in the padding parameter and is 1 byte wide.
|
||||||
|
buf.writeByte(padding - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void verifyStreamId(int streamId, String argumentName) {
|
||||||
|
checkPositive(streamId, argumentName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void verifyStreamOrConnectionId(int streamId, String argumentName) {
|
||||||
|
checkPositiveOrZero(streamId, argumentName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void verifyWeight(short weight) {
|
||||||
|
if (weight < MIN_WEIGHT || weight > MAX_WEIGHT) {
|
||||||
|
throw new IllegalArgumentException("Invalid weight: " + weight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void verifyErrorCode(long errorCode) {
|
||||||
|
if (errorCode < 0 || errorCode > MAX_UNSIGNED_INT) {
|
||||||
|
throw new IllegalArgumentException("Invalid errorCode: " + errorCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void verifyWindowSizeIncrement(int windowSizeIncrement) {
|
||||||
|
checkPositiveOrZero(windowSizeIncrement, "windowSizeIncrement");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void verifyPingPayload(ByteBuf data) {
|
||||||
|
if (data == null || data.readableBytes() != PING_FRAME_PAYLOAD_LENGTH) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Opaque data must be " + PING_FRAME_PAYLOAD_LENGTH + " bytes");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,779 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2014 The Netty Project
|
||||||
|
*
|
||||||
|
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License. You may obtain a
|
||||||
|
* copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||||
|
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||||
|
* or implied. See the License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package io.netty.handler.codec.http2;
|
||||||
|
|
||||||
|
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_WINDOW_SIZE;
|
||||||
|
import static io.netty.handler.codec.http2.Http2CodecUtil.MAX_WEIGHT;
|
||||||
|
import static io.netty.handler.codec.http2.Http2CodecUtil.MIN_WEIGHT;
|
||||||
|
import static io.netty.handler.codec.http2.Http2Error.FLOW_CONTROL_ERROR;
|
||||||
|
import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR;
|
||||||
|
import static io.netty.handler.codec.http2.Http2Error.STREAM_CLOSED;
|
||||||
|
import static io.netty.handler.codec.http2.Http2Exception.streamError;
|
||||||
|
import static io.netty.handler.codec.http2.Http2Stream.State.HALF_CLOSED_LOCAL;
|
||||||
|
import static io.netty.util.internal.ObjectUtil.checkNotNull;
|
||||||
|
import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
|
||||||
|
import static java.lang.Math.max;
|
||||||
|
import static java.lang.Math.min;
|
||||||
|
|
||||||
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
|
import io.netty.util.internal.UnstableApi;
|
||||||
|
import io.netty.util.internal.logging.InternalLogger;
|
||||||
|
import io.netty.util.internal.logging.InternalLoggerFactory;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.Deque;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basic implementation of {@link Http2RemoteFlowController}.
|
||||||
|
*
|
||||||
|
* <p>This class is <strong>NOT</strong> thread safe. The assumption is all methods must be invoked
|
||||||
|
* from a single thread. Typically this thread is the event loop thread for the {@link
|
||||||
|
* ChannelHandlerContext} managed by this class.
|
||||||
|
*/
|
||||||
|
@UnstableApi
|
||||||
|
public class DefaultHttp2RemoteFlowController implements Http2RemoteFlowController {
|
||||||
|
private static final InternalLogger logger =
|
||||||
|
InternalLoggerFactory.getInstance(DefaultHttp2RemoteFlowController.class);
|
||||||
|
private static final int MIN_WRITABLE_CHUNK = 32 * 1024;
|
||||||
|
private final Http2Connection connection;
|
||||||
|
private final Http2Connection.PropertyKey stateKey;
|
||||||
|
private final StreamByteDistributor streamByteDistributor;
|
||||||
|
private final FlowState connectionState;
|
||||||
|
private int initialWindowSize = DEFAULT_WINDOW_SIZE;
|
||||||
|
private WritabilityMonitor monitor;
|
||||||
|
private ChannelHandlerContext ctx;
|
||||||
|
|
||||||
|
public DefaultHttp2RemoteFlowController(Http2Connection connection) {
|
||||||
|
this(connection, (Listener) null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DefaultHttp2RemoteFlowController(
|
||||||
|
Http2Connection connection, StreamByteDistributor streamByteDistributor) {
|
||||||
|
this(connection, streamByteDistributor, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DefaultHttp2RemoteFlowController(Http2Connection connection, final Listener listener) {
|
||||||
|
this(connection, new WeightedFairQueueByteDistributor(connection), listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DefaultHttp2RemoteFlowController(
|
||||||
|
Http2Connection connection,
|
||||||
|
StreamByteDistributor streamByteDistributor,
|
||||||
|
final Listener listener) {
|
||||||
|
this.connection = checkNotNull(connection, "connection");
|
||||||
|
this.streamByteDistributor = checkNotNull(streamByteDistributor, "streamWriteDistributor");
|
||||||
|
|
||||||
|
// Add a flow state for the connection.
|
||||||
|
stateKey = connection.newKey();
|
||||||
|
connectionState = new FlowState(connection.connectionStream());
|
||||||
|
connection.connectionStream().setProperty(stateKey, connectionState);
|
||||||
|
|
||||||
|
// Monitor may depend upon connectionState, and so initialize after connectionState
|
||||||
|
listener(listener);
|
||||||
|
monitor.windowSize(connectionState, initialWindowSize);
|
||||||
|
|
||||||
|
// Register for notification of new streams.
|
||||||
|
connection.addListener(
|
||||||
|
new Http2ConnectionAdapter() {
|
||||||
|
@Override
|
||||||
|
public void onStreamAdded(Http2Stream stream) {
|
||||||
|
// If the stream state is not open then the stream is not yet eligible for flow
|
||||||
|
// controlled frames and
|
||||||
|
// only requires the ReducedFlowState. Otherwise the full amount of memory is required.
|
||||||
|
stream.setProperty(stateKey, new FlowState(stream));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStreamActive(Http2Stream stream) {
|
||||||
|
// If the object was previously created, but later activated then we have to ensure the
|
||||||
|
// proper
|
||||||
|
// initialWindowSize is used.
|
||||||
|
monitor.windowSize(state(stream), initialWindowSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStreamClosed(Http2Stream stream) {
|
||||||
|
// Any pending frames can never be written, cancel and
|
||||||
|
// write errors for any pending frames.
|
||||||
|
state(stream).cancel(STREAM_CLOSED, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStreamHalfClosed(Http2Stream stream) {
|
||||||
|
if (HALF_CLOSED_LOCAL == stream.state()) {
|
||||||
|
/**
|
||||||
|
* When this method is called there should not be any pending frames left if the API
|
||||||
|
* is used correctly. However, it is possible that a erroneous application can sneak
|
||||||
|
* in a frame even after having already written a frame with the END_STREAM flag set,
|
||||||
|
* as the stream state might not transition immediately to HALF_CLOSED_LOCAL / CLOSED
|
||||||
|
* due to flow control delaying the write.
|
||||||
|
*
|
||||||
|
* <p>This is to cancel any such illegal writes.
|
||||||
|
*/
|
||||||
|
state(stream).cancel(STREAM_CLOSED, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>Any queued {@link FlowControlled} objects will be sent.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void channelHandlerContext(ChannelHandlerContext ctx) throws Http2Exception {
|
||||||
|
this.ctx = checkNotNull(ctx, "ctx");
|
||||||
|
|
||||||
|
// Writing the pending bytes will not check writability change and instead a writability change
|
||||||
|
// notification
|
||||||
|
// to be provided by an explicit call.
|
||||||
|
channelWritabilityChanged();
|
||||||
|
|
||||||
|
// Don't worry about cleaning up queued frames here if ctx is null. It is expected that all
|
||||||
|
// streams will be
|
||||||
|
// closed and the queue cleanup will occur when the stream state transitions occur.
|
||||||
|
|
||||||
|
// If any frames have been queued up, we should send them now that we have a channel context.
|
||||||
|
if (isChannelWritable()) {
|
||||||
|
writePendingBytes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChannelHandlerContext channelHandlerContext() {
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialWindowSize(int newWindowSize) throws Http2Exception {
|
||||||
|
assert ctx == null || ctx.executor().inEventLoop();
|
||||||
|
monitor.initialWindowSize(newWindowSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int initialWindowSize() {
|
||||||
|
return initialWindowSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int windowSize(Http2Stream stream) {
|
||||||
|
return state(stream).windowSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isWritable(Http2Stream stream) {
|
||||||
|
return monitor.isWritable(state(stream));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void channelWritabilityChanged() throws Http2Exception {
|
||||||
|
monitor.channelWritabilityChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateDependencyTree(
|
||||||
|
int childStreamId, int parentStreamId, short weight, boolean exclusive) {
|
||||||
|
// It is assumed there are all validated at a higher level. For example in the Http2FrameReader.
|
||||||
|
assert weight >= MIN_WEIGHT && weight <= MAX_WEIGHT : "Invalid weight";
|
||||||
|
assert childStreamId != parentStreamId : "A stream cannot depend on itself";
|
||||||
|
assert childStreamId > 0 && parentStreamId >= 0
|
||||||
|
: "childStreamId must be > 0. parentStreamId must be >= 0.";
|
||||||
|
|
||||||
|
streamByteDistributor.updateDependencyTree(childStreamId, parentStreamId, weight, exclusive);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isChannelWritable() {
|
||||||
|
return ctx != null && isChannelWritable0();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isChannelWritable0() {
|
||||||
|
return ctx.channel().isWritable();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void listener(Listener listener) {
|
||||||
|
monitor =
|
||||||
|
listener == null ? new WritabilityMonitor() : new ListenerWritabilityMonitor(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void incrementWindowSize(Http2Stream stream, int delta) throws Http2Exception {
|
||||||
|
assert ctx == null || ctx.executor().inEventLoop();
|
||||||
|
monitor.incrementWindowSize(state(stream), delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addFlowControlled(Http2Stream stream, FlowControlled frame) {
|
||||||
|
// The context can be null assuming the frame will be queued and send later when the context is
|
||||||
|
// set.
|
||||||
|
assert ctx == null || ctx.executor().inEventLoop();
|
||||||
|
checkNotNull(frame, "frame");
|
||||||
|
try {
|
||||||
|
monitor.enqueueFrame(state(stream), frame);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
frame.error(ctx, t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasFlowControlled(Http2Stream stream) {
|
||||||
|
return state(stream).hasFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
private FlowState state(Http2Stream stream) {
|
||||||
|
return (FlowState) stream.getProperty(stateKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the flow control window for the entire connection. */
|
||||||
|
private int connectionWindowSize() {
|
||||||
|
return connectionState.windowSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int minUsableChannelBytes() {
|
||||||
|
// The current allocation algorithm values "fairness" and doesn't give any consideration to
|
||||||
|
// "goodput". It
|
||||||
|
// is possible that 1 byte will be allocated to many streams. In an effort to try to make
|
||||||
|
// "goodput"
|
||||||
|
// reasonable with the current allocation algorithm we have this "cheap" check up front to
|
||||||
|
// ensure there is
|
||||||
|
// an "adequate" amount of connection window before allocation is attempted. This is not
|
||||||
|
// foolproof as if the
|
||||||
|
// number of streams is >= this minimal number then we may still have the issue, but the idea is
|
||||||
|
// to narrow the
|
||||||
|
// circumstances in which this can happen without rewriting the allocation algorithm.
|
||||||
|
return max(ctx.channel().config().getWriteBufferLowWaterMark(), MIN_WRITABLE_CHUNK);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int maxUsableChannelBytes() {
|
||||||
|
// If the channel isWritable, allow at least minUsableChannelBytes.
|
||||||
|
int channelWritableBytes = (int) min(Integer.MAX_VALUE, ctx.channel().bytesBeforeUnwritable());
|
||||||
|
int usableBytes =
|
||||||
|
channelWritableBytes > 0 ? max(channelWritableBytes, minUsableChannelBytes()) : 0;
|
||||||
|
|
||||||
|
// Clip the usable bytes by the connection window.
|
||||||
|
return min(connectionState.windowSize(), usableBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The amount of bytes that can be supported by underlying {@link io.netty.channel.Channel}
|
||||||
|
* without queuing "too-much".
|
||||||
|
*/
|
||||||
|
private int writableBytes() {
|
||||||
|
return min(connectionWindowSize(), maxUsableChannelBytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writePendingBytes() throws Http2Exception {
|
||||||
|
monitor.writePendingBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The remote flow control state for a single stream. */
|
||||||
|
private final class FlowState implements StreamByteDistributor.StreamState {
|
||||||
|
private final Http2Stream stream;
|
||||||
|
private final Deque<FlowControlled> pendingWriteQueue;
|
||||||
|
private int window;
|
||||||
|
private long pendingBytes;
|
||||||
|
private boolean markedWritable;
|
||||||
|
|
||||||
|
/** Set to true while a frame is being written, false otherwise. */
|
||||||
|
private boolean writing;
|
||||||
|
/** Set to true if cancel() was called. */
|
||||||
|
private boolean cancelled;
|
||||||
|
|
||||||
|
FlowState(Http2Stream stream) {
|
||||||
|
this.stream = stream;
|
||||||
|
pendingWriteQueue = new ArrayDeque<FlowControlled>(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the stream associated with this object is writable.
|
||||||
|
*
|
||||||
|
* @return {@code true} if the stream associated with this object is writable.
|
||||||
|
*/
|
||||||
|
boolean isWritable() {
|
||||||
|
return windowSize() > pendingBytes() && !cancelled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The stream this state is associated with. */
|
||||||
|
@Override
|
||||||
|
public Http2Stream stream() {
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the parameter from the last call to {@link #markedWritability(boolean)}. */
|
||||||
|
boolean markedWritability() {
|
||||||
|
return markedWritable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Save the state of writability. */
|
||||||
|
void markedWritability(boolean isWritable) {
|
||||||
|
this.markedWritable = isWritable;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int windowSize() {
|
||||||
|
return window;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reset the window size for this stream. */
|
||||||
|
void windowSize(int initialWindowSize) {
|
||||||
|
window = initialWindowSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write the allocated bytes for this stream.
|
||||||
|
*
|
||||||
|
* @return the number of bytes written for a stream or {@code -1} if no write occurred.
|
||||||
|
*/
|
||||||
|
int writeAllocatedBytes(int allocated) {
|
||||||
|
final int initialAllocated = allocated;
|
||||||
|
int writtenBytes;
|
||||||
|
// In case an exception is thrown we want to remember it and pass it to cancel(Throwable).
|
||||||
|
Throwable cause = null;
|
||||||
|
FlowControlled frame;
|
||||||
|
try {
|
||||||
|
assert !writing;
|
||||||
|
writing = true;
|
||||||
|
|
||||||
|
// Write the remainder of frames that we are allowed to
|
||||||
|
boolean writeOccurred = false;
|
||||||
|
while (!cancelled && (frame = peek()) != null) {
|
||||||
|
int maxBytes = min(allocated, writableWindow());
|
||||||
|
if (maxBytes <= 0 && frame.size() > 0) {
|
||||||
|
// The frame still has data, but the amount of allocated bytes has been exhausted.
|
||||||
|
// Don't write needless empty frames.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
writeOccurred = true;
|
||||||
|
int initialFrameSize = frame.size();
|
||||||
|
try {
|
||||||
|
frame.write(ctx, max(0, maxBytes));
|
||||||
|
if (frame.size() == 0) {
|
||||||
|
// This frame has been fully written, remove this frame and notify it.
|
||||||
|
// Since we remove this frame first, we're guaranteed that its error
|
||||||
|
// method will not be called when we call cancel.
|
||||||
|
pendingWriteQueue.remove();
|
||||||
|
frame.writeComplete();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// Decrement allocated by how much was actually written.
|
||||||
|
allocated -= initialFrameSize - frame.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!writeOccurred) {
|
||||||
|
// Either there was no frame, or the amount of allocated bytes has been exhausted.
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Throwable t) {
|
||||||
|
// Mark the state as cancelled, we'll clear the pending queue via cancel() below.
|
||||||
|
cancelled = true;
|
||||||
|
cause = t;
|
||||||
|
} finally {
|
||||||
|
writing = false;
|
||||||
|
// Make sure we always decrement the flow control windows
|
||||||
|
// by the bytes written.
|
||||||
|
writtenBytes = initialAllocated - allocated;
|
||||||
|
|
||||||
|
decrementPendingBytes(writtenBytes, false);
|
||||||
|
decrementFlowControlWindow(writtenBytes);
|
||||||
|
|
||||||
|
// If a cancellation occurred while writing, call cancel again to
|
||||||
|
// clear and error all of the pending writes.
|
||||||
|
if (cancelled) {
|
||||||
|
cancel(INTERNAL_ERROR, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return writtenBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increments the flow control window for this stream by the given delta and returns the new
|
||||||
|
* value.
|
||||||
|
*/
|
||||||
|
int incrementStreamWindow(int delta) throws Http2Exception {
|
||||||
|
if (delta > 0 && Integer.MAX_VALUE - delta < window) {
|
||||||
|
throw streamError(
|
||||||
|
stream.id(), FLOW_CONTROL_ERROR, "Window size overflow for stream: %d", stream.id());
|
||||||
|
}
|
||||||
|
window += delta;
|
||||||
|
|
||||||
|
streamByteDistributor.updateStreamableBytes(this);
|
||||||
|
return window;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the maximum writable window (minimum of the stream and connection windows). */
|
||||||
|
private int writableWindow() {
|
||||||
|
return min(window, connectionWindowSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long pendingBytes() {
|
||||||
|
return pendingBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Adds the {@code frame} to the pending queue and increments the pending byte count. */
|
||||||
|
void enqueueFrame(FlowControlled frame) {
|
||||||
|
FlowControlled last = pendingWriteQueue.peekLast();
|
||||||
|
if (last == null) {
|
||||||
|
enqueueFrameWithoutMerge(frame);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int lastSize = last.size();
|
||||||
|
if (last.merge(ctx, frame)) {
|
||||||
|
incrementPendingBytes(last.size() - lastSize, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
enqueueFrameWithoutMerge(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void enqueueFrameWithoutMerge(FlowControlled frame) {
|
||||||
|
pendingWriteQueue.offer(frame);
|
||||||
|
// This must be called after adding to the queue in order so that hasFrame() is
|
||||||
|
// updated before updating the stream state.
|
||||||
|
incrementPendingBytes(frame.size(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasFrame() {
|
||||||
|
return !pendingWriteQueue.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the head of the pending queue, or {@code null} if empty. */
|
||||||
|
private FlowControlled peek() {
|
||||||
|
return pendingWriteQueue.peek();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the pending queue and writes errors for each remaining frame.
|
||||||
|
*
|
||||||
|
* @param error the {@link Http2Error} to use.
|
||||||
|
* @param cause the {@link Throwable} that caused this method to be invoked.
|
||||||
|
*/
|
||||||
|
void cancel(Http2Error error, Throwable cause) {
|
||||||
|
cancelled = true;
|
||||||
|
// Ensure that the queue can't be modified while we are writing.
|
||||||
|
if (writing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FlowControlled frame = pendingWriteQueue.poll();
|
||||||
|
if (frame != null) {
|
||||||
|
// Only create exception once and reuse to reduce overhead of filling in the stacktrace.
|
||||||
|
final Http2Exception exception =
|
||||||
|
streamError(stream.id(), error, cause, "Stream closed before write could take place");
|
||||||
|
do {
|
||||||
|
writeError(frame, exception);
|
||||||
|
frame = pendingWriteQueue.poll();
|
||||||
|
} while (frame != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
streamByteDistributor.updateStreamableBytes(this);
|
||||||
|
|
||||||
|
monitor.stateCancelled(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increments the number of pending bytes for this node and optionally updates the {@link
|
||||||
|
* StreamByteDistributor}.
|
||||||
|
*/
|
||||||
|
private void incrementPendingBytes(int numBytes, boolean updateStreamableBytes) {
|
||||||
|
pendingBytes += numBytes;
|
||||||
|
monitor.incrementPendingBytes(numBytes);
|
||||||
|
if (updateStreamableBytes) {
|
||||||
|
streamByteDistributor.updateStreamableBytes(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this frame is in the pending queue, decrements the number of pending bytes for the stream.
|
||||||
|
*/
|
||||||
|
private void decrementPendingBytes(int bytes, boolean updateStreamableBytes) {
|
||||||
|
incrementPendingBytes(-bytes, updateStreamableBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Decrement the per stream and connection flow control window by {@code bytes}. */
|
||||||
|
private void decrementFlowControlWindow(int bytes) {
|
||||||
|
try {
|
||||||
|
int negativeBytes = -bytes;
|
||||||
|
connectionState.incrementStreamWindow(negativeBytes);
|
||||||
|
incrementStreamWindow(negativeBytes);
|
||||||
|
} catch (Http2Exception e) {
|
||||||
|
// Should never get here since we're decrementing.
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Invalid window state when writing frame: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discards this {@link FlowControlled}, writing an error. If this frame is in the pending
|
||||||
|
* queue, the unwritten bytes are removed from this branch of the priority tree.
|
||||||
|
*/
|
||||||
|
private void writeError(FlowControlled frame, Http2Exception cause) {
|
||||||
|
assert ctx != null;
|
||||||
|
decrementPendingBytes(frame.size(), true);
|
||||||
|
frame.error(ctx, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Abstract class which provides common functionality for writability monitor implementations. */
|
||||||
|
private class WritabilityMonitor implements StreamByteDistributor.Writer {
|
||||||
|
private boolean inWritePendingBytes;
|
||||||
|
private long totalPendingBytes;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void write(Http2Stream stream, int numBytes) {
|
||||||
|
state(stream).writeAllocatedBytes(numBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the writability of the underlying channel changes.
|
||||||
|
*
|
||||||
|
* @throws Http2Exception If a write occurs and an exception happens in the write operation.
|
||||||
|
*/
|
||||||
|
void channelWritabilityChange() throws Http2Exception {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the state is cancelled.
|
||||||
|
*
|
||||||
|
* @param state the state that was cancelled.
|
||||||
|
*/
|
||||||
|
void stateCancelled(FlowState state) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the initial window size for {@code state}.
|
||||||
|
*
|
||||||
|
* @param state the state to change the initial window size for.
|
||||||
|
* @param initialWindowSize the size of the window in bytes.
|
||||||
|
*/
|
||||||
|
void windowSize(FlowState state, int initialWindowSize) {
|
||||||
|
state.windowSize(initialWindowSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increment the window size for a particular stream.
|
||||||
|
*
|
||||||
|
* @param state the state associated with the stream whose window is being incremented.
|
||||||
|
* @param delta The amount to increment by.
|
||||||
|
* @throws Http2Exception If this operation overflows the window for {@code state}.
|
||||||
|
*/
|
||||||
|
void incrementWindowSize(FlowState state, int delta) throws Http2Exception {
|
||||||
|
state.incrementStreamWindow(delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a frame to be sent via flow control.
|
||||||
|
*
|
||||||
|
* @param state The state associated with the stream which the {@code frame} is associated with.
|
||||||
|
* @param frame the frame to enqueue.
|
||||||
|
* @throws Http2Exception If a writability error occurs.
|
||||||
|
*/
|
||||||
|
void enqueueFrame(FlowState state, FlowControlled frame) throws Http2Exception {
|
||||||
|
state.enqueueFrame(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increment the total amount of pending bytes for all streams. When any stream's pending bytes
|
||||||
|
* changes method should be called.
|
||||||
|
*
|
||||||
|
* @param delta The amount to increment by.
|
||||||
|
*/
|
||||||
|
final void incrementPendingBytes(int delta) {
|
||||||
|
totalPendingBytes += delta;
|
||||||
|
|
||||||
|
// Notification of writibilty change should be delayed until the end of the top level event.
|
||||||
|
// This is to ensure the flow controller is more consistent state before calling external
|
||||||
|
// listener methods.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the stream associated with {@code state} is writable.
|
||||||
|
*
|
||||||
|
* @param state The state which is associated with the stream to test writability for.
|
||||||
|
* @return {@code true} if {@link FlowState#stream()} is writable. {@code false} otherwise.
|
||||||
|
*/
|
||||||
|
final boolean isWritable(FlowState state) {
|
||||||
|
return isWritableConnection() && state.isWritable();
|
||||||
|
}
|
||||||
|
|
||||||
|
final void writePendingBytes() throws Http2Exception {
|
||||||
|
// Reentry is not permitted during the byte distribution process. It may lead to undesirable
|
||||||
|
// distribution of
|
||||||
|
// bytes and even infinite loops. We protect against reentry and make sure each call has an
|
||||||
|
// opportunity to
|
||||||
|
// cause a distribution to occur. This may be useful for example if the channel's writability
|
||||||
|
// changes from
|
||||||
|
// Writable -> Not Writable (because we are writing) -> Writable (because the user flushed to
|
||||||
|
// make more room
|
||||||
|
// in the channel outbound buffer).
|
||||||
|
if (inWritePendingBytes) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
inWritePendingBytes = true;
|
||||||
|
try {
|
||||||
|
int bytesToWrite = writableBytes();
|
||||||
|
// Make sure we always write at least once, regardless if we have bytesToWrite or not.
|
||||||
|
// This ensures that zero-length frames will always be written.
|
||||||
|
for (; ; ) {
|
||||||
|
if (!streamByteDistributor.distribute(bytesToWrite, this)
|
||||||
|
|| (bytesToWrite = writableBytes()) <= 0
|
||||||
|
|| !isChannelWritable0()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
inWritePendingBytes = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void initialWindowSize(int newWindowSize) throws Http2Exception {
|
||||||
|
checkPositiveOrZero(newWindowSize, "newWindowSize");
|
||||||
|
|
||||||
|
final int delta = newWindowSize - initialWindowSize;
|
||||||
|
initialWindowSize = newWindowSize;
|
||||||
|
connection.forEachActiveStream(
|
||||||
|
new Http2StreamVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(Http2Stream stream) throws Http2Exception {
|
||||||
|
state(stream).incrementStreamWindow(delta);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (delta > 0 && isChannelWritable()) {
|
||||||
|
// The window size increased, send any pending frames for all streams.
|
||||||
|
writePendingBytes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final boolean isWritableConnection() {
|
||||||
|
return connectionState.windowSize() - totalPendingBytes > 0 && isChannelWritable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writability of a {@code stream} is calculated using the following:
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* Connection Window - Total Queued Bytes > 0 &&
|
||||||
|
* Stream Window - Bytes Queued for Stream > 0 &&
|
||||||
|
* isChannelWritable()
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
private final class ListenerWritabilityMonitor extends WritabilityMonitor
|
||||||
|
implements Http2StreamVisitor {
|
||||||
|
private final Listener listener;
|
||||||
|
|
||||||
|
ListenerWritabilityMonitor(Listener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(Http2Stream stream) throws Http2Exception {
|
||||||
|
FlowState state = state(stream);
|
||||||
|
if (isWritable(state) != state.markedWritability()) {
|
||||||
|
notifyWritabilityChanged(state);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void windowSize(FlowState state, int initialWindowSize) {
|
||||||
|
super.windowSize(state, initialWindowSize);
|
||||||
|
try {
|
||||||
|
checkStateWritability(state);
|
||||||
|
} catch (Http2Exception e) {
|
||||||
|
throw new RuntimeException("Caught unexpected exception from window", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void incrementWindowSize(FlowState state, int delta) throws Http2Exception {
|
||||||
|
super.incrementWindowSize(state, delta);
|
||||||
|
checkStateWritability(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void initialWindowSize(int newWindowSize) throws Http2Exception {
|
||||||
|
super.initialWindowSize(newWindowSize);
|
||||||
|
if (isWritableConnection()) {
|
||||||
|
// If the write operation does not occur we still need to check all streams because they
|
||||||
|
// may have transitioned from writable to not writable.
|
||||||
|
checkAllWritabilityChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void enqueueFrame(FlowState state, FlowControlled frame) throws Http2Exception {
|
||||||
|
super.enqueueFrame(state, frame);
|
||||||
|
checkConnectionThenStreamWritabilityChanged(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void stateCancelled(FlowState state) {
|
||||||
|
try {
|
||||||
|
checkConnectionThenStreamWritabilityChanged(state);
|
||||||
|
} catch (Http2Exception e) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Caught unexpected exception from checkAllWritabilityChanged", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void channelWritabilityChange() throws Http2Exception {
|
||||||
|
if (connectionState.markedWritability() != isChannelWritable()) {
|
||||||
|
checkAllWritabilityChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkStateWritability(FlowState state) throws Http2Exception {
|
||||||
|
if (isWritable(state) != state.markedWritability()) {
|
||||||
|
if (state == connectionState) {
|
||||||
|
checkAllWritabilityChanged();
|
||||||
|
} else {
|
||||||
|
notifyWritabilityChanged(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyWritabilityChanged(FlowState state) {
|
||||||
|
state.markedWritability(!state.markedWritability());
|
||||||
|
try {
|
||||||
|
listener.writabilityChanged(state.stream);
|
||||||
|
} catch (Throwable cause) {
|
||||||
|
logger.error("Caught Throwable from listener.writabilityChanged", cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkConnectionThenStreamWritabilityChanged(FlowState state)
|
||||||
|
throws Http2Exception {
|
||||||
|
// It is possible that the connection window and/or the individual stream writability could
|
||||||
|
// change.
|
||||||
|
if (isWritableConnection() != connectionState.markedWritability()) {
|
||||||
|
checkAllWritabilityChanged();
|
||||||
|
} else if (isWritable(state) != state.markedWritability()) {
|
||||||
|
notifyWritabilityChanged(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkAllWritabilityChanged() throws Http2Exception {
|
||||||
|
// Make sure we mark that we have notified as a result of this change.
|
||||||
|
connectionState.markedWritability(isWritableConnection());
|
||||||
|
connection.forEachActiveStream(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,176 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2014 The Netty Project
|
||||||
|
*
|
||||||
|
* The Netty Project licenses this file to you under the Apache License,
|
||||||
|
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package io.netty.handler.codec.http2;
|
||||||
|
|
||||||
|
import static io.netty.handler.codec.http2.Http2FrameLogger.Direction.OUTBOUND;
|
||||||
|
import static io.netty.util.internal.ObjectUtil.checkNotNull;
|
||||||
|
|
||||||
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import io.netty.channel.ChannelFuture;
|
||||||
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
|
import io.netty.channel.ChannelPromise;
|
||||||
|
import io.netty.util.internal.UnstableApi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decorator around a {@link Http2FrameWriter} that logs all outbound frames before calling the
|
||||||
|
* writer.
|
||||||
|
*/
|
||||||
|
@UnstableApi
|
||||||
|
public class Http2OutboundFrameLogger implements Http2FrameWriter {
|
||||||
|
private final Http2FrameWriter writer;
|
||||||
|
private final Http2FrameLogger logger;
|
||||||
|
|
||||||
|
public Http2OutboundFrameLogger(Http2FrameWriter writer, Http2FrameLogger logger) {
|
||||||
|
this.writer = checkNotNull(writer, "writer");
|
||||||
|
this.logger = checkNotNull(logger, "logger");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChannelFuture writeData(
|
||||||
|
ChannelHandlerContext ctx,
|
||||||
|
int streamId,
|
||||||
|
ByteBuf data,
|
||||||
|
int padding,
|
||||||
|
boolean endStream,
|
||||||
|
ChannelPromise promise) {
|
||||||
|
logger.logData(OUTBOUND, ctx, streamId, data, padding, endStream);
|
||||||
|
return writer.writeData(ctx, streamId, data, padding, endStream, promise);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChannelFuture writeHeaders(
|
||||||
|
ChannelHandlerContext ctx,
|
||||||
|
int streamId,
|
||||||
|
Http2Headers headers,
|
||||||
|
int padding,
|
||||||
|
boolean endStream,
|
||||||
|
ChannelPromise promise) {
|
||||||
|
logger.logHeaders(OUTBOUND, ctx, streamId, headers, padding, endStream);
|
||||||
|
return writer.writeHeaders(ctx, streamId, headers, padding, endStream, promise);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChannelFuture writeHeaders(
|
||||||
|
ChannelHandlerContext ctx,
|
||||||
|
int streamId,
|
||||||
|
Http2Headers headers,
|
||||||
|
int streamDependency,
|
||||||
|
short weight,
|
||||||
|
boolean exclusive,
|
||||||
|
int padding,
|
||||||
|
boolean endStream,
|
||||||
|
ChannelPromise promise) {
|
||||||
|
logger.logHeaders(
|
||||||
|
OUTBOUND, ctx, streamId, headers, streamDependency, weight, exclusive, padding, endStream);
|
||||||
|
return writer.writeHeaders(
|
||||||
|
ctx, streamId, headers, streamDependency, weight, exclusive, padding, endStream, promise);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChannelFuture writePriority(
|
||||||
|
ChannelHandlerContext ctx,
|
||||||
|
int streamId,
|
||||||
|
int streamDependency,
|
||||||
|
short weight,
|
||||||
|
boolean exclusive,
|
||||||
|
ChannelPromise promise) {
|
||||||
|
logger.logPriority(OUTBOUND, ctx, streamId, streamDependency, weight, exclusive);
|
||||||
|
return writer.writePriority(ctx, streamId, streamDependency, weight, exclusive, promise);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChannelFuture writeRstStream(
|
||||||
|
ChannelHandlerContext ctx, int streamId, long errorCode, ChannelPromise promise) {
|
||||||
|
logger.logRstStream(OUTBOUND, ctx, streamId, errorCode);
|
||||||
|
return writer.writeRstStream(ctx, streamId, errorCode, promise);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChannelFuture writeSettings(
|
||||||
|
ChannelHandlerContext ctx, Http2Settings settings, ChannelPromise promise) {
|
||||||
|
logger.logSettings(OUTBOUND, ctx, settings);
|
||||||
|
return writer.writeSettings(ctx, settings, promise);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChannelFuture writeSettingsAck(ChannelHandlerContext ctx, ChannelPromise promise) {
|
||||||
|
logger.logSettingsAck(OUTBOUND, ctx);
|
||||||
|
return writer.writeSettingsAck(ctx, promise);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChannelFuture writePing(
|
||||||
|
ChannelHandlerContext ctx, boolean ack, long data, ChannelPromise promise) {
|
||||||
|
if (ack) {
|
||||||
|
logger.logPingAck(OUTBOUND, ctx, data);
|
||||||
|
} else {
|
||||||
|
logger.logPing(OUTBOUND, ctx, data);
|
||||||
|
}
|
||||||
|
return writer.writePing(ctx, ack, data, promise);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChannelFuture writePushPromise(
|
||||||
|
ChannelHandlerContext ctx,
|
||||||
|
int streamId,
|
||||||
|
int promisedStreamId,
|
||||||
|
Http2Headers headers,
|
||||||
|
int padding,
|
||||||
|
ChannelPromise promise) {
|
||||||
|
logger.logPushPromise(OUTBOUND, ctx, streamId, promisedStreamId, headers, padding);
|
||||||
|
return writer.writePushPromise(ctx, streamId, promisedStreamId, headers, padding, promise);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChannelFuture writeGoAway(
|
||||||
|
ChannelHandlerContext ctx,
|
||||||
|
int lastStreamId,
|
||||||
|
long errorCode,
|
||||||
|
ByteBuf debugData,
|
||||||
|
ChannelPromise promise) {
|
||||||
|
logger.logGoAway(OUTBOUND, ctx, lastStreamId, errorCode, debugData);
|
||||||
|
return writer.writeGoAway(ctx, lastStreamId, errorCode, debugData, promise);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChannelFuture writeWindowUpdate(
|
||||||
|
ChannelHandlerContext ctx, int streamId, int windowSizeIncrement, ChannelPromise promise) {
|
||||||
|
logger.logWindowsUpdate(OUTBOUND, ctx, streamId, windowSizeIncrement);
|
||||||
|
return writer.writeWindowUpdate(ctx, streamId, windowSizeIncrement, promise);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChannelFuture writeFrame(
|
||||||
|
ChannelHandlerContext ctx,
|
||||||
|
byte frameType,
|
||||||
|
int streamId,
|
||||||
|
Http2Flags flags,
|
||||||
|
ByteBuf payload,
|
||||||
|
ChannelPromise promise) {
|
||||||
|
logger.logUnknownFrame(OUTBOUND, ctx, frameType, streamId, flags, payload);
|
||||||
|
return writer.writeFrame(ctx, frameType, streamId, flags, payload, promise);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
writer.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Configuration configuration() {
|
||||||
|
return writer.configuration();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue