nl.topicus.jdbc.shaded.io.grpc.internal.ClientCallImpl Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of spanner-jdbc Show documentation
Show all versions of spanner-jdbc Show documentation
JDBC Driver for Google Cloud Spanner
/*
* Copyright 2014, Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
*
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package nl.topicus.jdbc.shaded.io.grpc.internal;
import static nl.topicus.jdbc.shaded.com.google.nl.topicus.jdbc.shaded.com.on.base.Preconditions.checkArgument;
import static nl.topicus.jdbc.shaded.com.google.nl.topicus.jdbc.shaded.com.on.base.Preconditions.checkNotNull;
import static nl.topicus.jdbc.shaded.com.google.nl.topicus.jdbc.shaded.com.on.base.Preconditions.checkState;
import static nl.topicus.jdbc.shaded.com.google.nl.topicus.jdbc.shaded.com.on.util.concurrent.MoreExecutors.directExecutor;
import static nl.topicus.jdbc.shaded.io.grpc.Contexts.statusFromCancelled;
import static nl.topicus.jdbc.shaded.io.grpc.Status.DEADLINE_EXCEEDED;
import static nl.topicus.jdbc.shaded.io.grpc.internal.GrpcUtil.MESSAGE_ACCEPT_ENCODING_KEY;
import static nl.topicus.jdbc.shaded.io.grpc.internal.GrpcUtil.MESSAGE_ENCODING_KEY;
import static nl.topicus.jdbc.shaded.io.grpc.internal.GrpcUtil.TIMEOUT_KEY;
import static java.lang.Math.max;
import nl.topicus.jdbc.shaded.com.google.nl.topicus.jdbc.shaded.com.on.annotations.VisibleForTesting;
import nl.topicus.jdbc.shaded.com.google.nl.topicus.jdbc.shaded.com.on.base.Preconditions;
import nl.topicus.jdbc.shaded.io.grpc.Attributes;
import nl.topicus.jdbc.shaded.io.grpc.CallOptions;
import nl.topicus.jdbc.shaded.io.grpc.ClientCall;
import nl.topicus.jdbc.shaded.io.grpc.Codec;
import nl.topicus.jdbc.shaded.io.grpc.Compressor;
import nl.topicus.jdbc.shaded.io.grpc.CompressorRegistry;
import nl.topicus.jdbc.shaded.io.grpc.Context;
import nl.topicus.jdbc.shaded.io.grpc.Deadline;
import nl.topicus.jdbc.shaded.io.grpc.Decompressor;
import nl.topicus.jdbc.shaded.io.grpc.DecompressorRegistry;
import nl.topicus.jdbc.shaded.io.grpc.InternalDecompressorRegistry;
import nl.topicus.jdbc.shaded.io.grpc.LoadBalancer.PickSubchannelArgs;
import nl.topicus.jdbc.shaded.io.grpc.Metadata;
import nl.topicus.jdbc.shaded.io.grpc.MethodDescriptor;
import nl.topicus.jdbc.shaded.io.grpc.MethodDescriptor.MethodType;
import nl.topicus.jdbc.shaded.io.grpc.Status;
import java.nl.topicus.jdbc.shaded.io.InputStream;
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 nl.topicus.jdbc.shaded.javax.annotation.Nullable;
/**
* Implementation of {@link ClientCall}.
*/
final class ClientCallImpl extends ClientCall
implements Context.CancellationListener {
private static final Logger log = Logger.getLogger(ClientCallImpl.class.getName());
private final MethodDescriptor method;
private final Executor callExecutor;
private final Context context;
private volatile ScheduledFuture> deadlineCancellationFuture;
private final boolean unaryRequest;
private final CallOptions callOptions;
private ClientStream stream;
private volatile boolean cancelListenersShouldBeRemoved;
private boolean cancelCalled;
private boolean halfCloseCalled;
private final ClientTransportProvider clientTransportProvider;
private ScheduledExecutorService deadlineCancellationExecutor;
private DecompressorRegistry decompressorRegistry = DecompressorRegistry.getDefaultInstance();
private CompressorRegistry nl.topicus.jdbc.shaded.com.ressorRegistry = CompressorRegistry.getDefaultInstance();
ClientCallImpl(
MethodDescriptor method, Executor executor, CallOptions callOptions,
ClientTransportProvider clientTransportProvider,
ScheduledExecutorService deadlineCancellationExecutor) {
this.method = method;
// 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.nl.topicus.jdbc.shaded.com.grpc/grpc-java/issues/368
this.callExecutor = executor == directExecutor()
? new SerializeReentrantCallsDirectExecutor()
: new SerializingExecutor(executor);
// 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.clientTransportProvider = clientTransportProvider;
this.deadlineCancellationExecutor = deadlineCancellationExecutor;
}
@Override
public void cancelled(Context context) {
stream.cancel(statusFromCancelled(context));
}
/**
* Provider of {@link ClientTransport}s.
*/
interface ClientTransportProvider {
/**
* Returns a transport for a new call.
*
* @param args object containing call arguments.
*/
ClientTransport get(PickSubchannelArgs args);
}
ClientCallImpl setDecompressorRegistry(DecompressorRegistry decompressorRegistry) {
this.decompressorRegistry = decompressorRegistry;
return this;
}
ClientCallImpl setCompressorRegistry(CompressorRegistry nl.topicus.jdbc.shaded.com.ressorRegistry) {
this.nl.topicus.jdbc.shaded.com.ressorRegistry = nl.topicus.jdbc.shaded.com.ressorRegistry;
return this;
}
@VisibleForTesting
static void prepareHeaders(
Metadata headers, DecompressorRegistry decompressorRegistry, Compressor nl.topicus.jdbc.shaded.com.ressor) {
headers.discardAll(MESSAGE_ENCODING_KEY);
if (nl.topicus.jdbc.shaded.com.ressor != Codec.Identity.NONE) {
headers.put(MESSAGE_ENCODING_KEY, nl.topicus.jdbc.shaded.com.ressor.getMessageEncoding());
}
headers.discardAll(MESSAGE_ACCEPT_ENCODING_KEY);
byte[] advertisedEncodings =
InternalDecompressorRegistry.getRawAdvertisedMessageEncodings(decompressorRegistry);
if (advertisedEncodings.length != 0) {
headers.put(MESSAGE_ACCEPT_ENCODING_KEY, advertisedEncodings);
}
}
@Override
public void start(final Listener observer, Metadata headers) {
checkState(stream == null, "Already started");
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;
class ClosedByContext extends ContextRunnable {
ClosedByContext() {
super(context);
}
@Override
public void runInContext() {
closeObserver(observer, statusFromCancelled(context), new Metadata());
}
}
callExecutor.execute(new ClosedByContext());
return;
}
final String nl.topicus.jdbc.shaded.com.ressorName = callOptions.getCompressor();
Compressor nl.topicus.jdbc.shaded.com.ressor = null;
if (nl.topicus.jdbc.shaded.com.ressorName != null) {
nl.topicus.jdbc.shaded.com.ressor = nl.topicus.jdbc.shaded.com.ressorRegistry.lookupCompressor(nl.topicus.jdbc.shaded.com.ressorName);
if (nl.topicus.jdbc.shaded.com.ressor == null) {
stream = NoopClientStream.INSTANCE;
class ClosedByNotFoundCompressor extends ContextRunnable {
ClosedByNotFoundCompressor() {
super(context);
}
@Override
public void runInContext() {
closeObserver(
observer,
Status.INTERNAL.withDescription(
String.format("Unable to find nl.topicus.jdbc.shaded.com.ressor by name %s", nl.topicus.jdbc.shaded.com.ressorName)),
new Metadata());
}
}
callExecutor.execute(new ClosedByNotFoundCompressor());
return;
}
} else {
nl.topicus.jdbc.shaded.com.ressor = Codec.Identity.NONE;
}
prepareHeaders(headers, decompressorRegistry, nl.topicus.jdbc.shaded.com.ressor);
Deadline effectiveDeadline = effectiveDeadline();
boolean deadlineExceeded = effectiveDeadline != null && effectiveDeadline.isExpired();
if (!deadlineExceeded) {
updateTimeoutHeaders(effectiveDeadline, callOptions.getDeadline(),
context.getDeadline(), headers);
ClientTransport transport = clientTransportProvider.get(
new PickSubchannelArgsImpl(method, headers, callOptions));
Context origContext = context.attach();
try {
stream = transport.newStream(method, headers, callOptions);
} finally {
context.detach(origContext);
}
} else {
stream = new FailingClientStream(DEADLINE_EXCEEDED);
}
if (callOptions.getAuthority() != null) {
stream.setAuthority(callOptions.getAuthority());
}
if (callOptions.getMaxInboundMessageSize() != null) {
stream.setMaxInboundMessageSize(callOptions.getMaxInboundMessageSize());
}
if (callOptions.getMaxOutboundMessageSize() != null) {
stream.setMaxOutboundMessageSize(callOptions.getMaxOutboundMessageSize());
}
stream.setCompressor(nl.topicus.jdbc.shaded.com.ressor);
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(this, directExecutor());
if (effectiveDeadline != null
// If the context has the effective deadline, we don't need to schedule an extra task.
&& context.getDeadline() != effectiveDeadline
// 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();
}
}
/**
* Based on the deadline, calculate and set the timeout to the given headers.
*/
private static void updateTimeoutHeaders(@Nullable Deadline effectiveDeadline,
@Nullable Deadline callDeadline, @Nullable Deadline outerCallDeadline, Metadata headers) {
headers.discardAll(TIMEOUT_KEY);
if (effectiveDeadline == null) {
return;
}
long effectiveTimeout = max(0, effectiveDeadline.timeRemaining(TimeUnit.NANOSECONDS));
headers.put(TIMEOUT_KEY, effectiveTimeout);
logIfContextNarrowedTimeout(effectiveTimeout, effectiveDeadline, outerCallDeadline,
callDeadline);
}
private static void logIfContextNarrowedTimeout(long effectiveTimeout,
Deadline effectiveDeadline, @Nullable Deadline outerCallDeadline,
@Nullable Deadline callDeadline) {
if (!log.isLoggable(Level.INFO) || outerCallDeadline != effectiveDeadline) {
return;
}
StringBuilder builder = new StringBuilder();
builder.append(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.info(builder.toString());
}
private void removeContextListenerAndCancelDeadlineFuture() {
context.removeListener(this);
ScheduledFuture> f = deadlineCancellationFuture;
if (f != null) {
f.cancel(false);
}
}
private class DeadlineTimer implements Runnable {
@Override
public void run() {
// DelayedStream.cancel() is safe to call from a thread that is different from where the
// stream is created.
stream.cancel(DEADLINE_EXCEEDED);
}
}
private ScheduledFuture> startDeadlineTimer(Deadline deadline) {
return deadlineCancellationExecutor.schedule(
new LogExceptionRunnable(new DeadlineTimer()), deadline.timeRemaining(TimeUnit.NANOSECONDS),
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) {
Preconditions.checkState(stream != null, "Not started");
checkArgument(numMessages >= 0, "Number requested must be non-negative");
stream.request(numMessages);
}
@Override
public void cancel(@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);
}
if (cause != null) {
status = status.withCause(cause);
}
stream.cancel(status);
}
} finally {
removeContextListenerAndCancelDeadlineFuture();
}
}
@Override
public void halfClose() {
Preconditions.checkState(stream != null, "Not started");
Preconditions.checkState(!cancelCalled, "call was cancelled");
Preconditions.checkState(!halfCloseCalled, "call already half-closed");
halfCloseCalled = true;
stream.halfClose();
}
@Override
public void sendMessage(ReqT message) {
Preconditions.checkState(stream != null, "Not started");
Preconditions.checkState(!cancelCalled, "call was cancelled");
Preconditions.checkState(!halfCloseCalled, "call was half-closed");
try {
// TODO(notcarl): Find out if messageIs needs to be closed.
InputStream messageIs = method.streamRequest(message);
stream.writeMessage(messageIs);
} catch (Throwable e) {
stream.cancel(Status.CANCELLED.withCause(e).withDescription("Failed to stream message"));
return;
}
// For unary requests, we don't flush since we know that halfClose should be nl.topicus.jdbc.shaded.com.ng 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 observer, Status status, Metadata trailers) {
observer.onClose(status, trailers);
}
private class ClientStreamListenerImpl implements ClientStreamListener {
private final Listener observer;
private boolean closed;
public ClientStreamListenerImpl(Listener observer) {
this.observer = Preconditions.checkNotNull(observer, "observer");
}
@Override
public void headersRead(final Metadata headers) {
Decompressor decompressor = Codec.Identity.NONE;
if (headers.containsKey(MESSAGE_ENCODING_KEY)) {
String encoding = headers.get(MESSAGE_ENCODING_KEY);
decompressor = decompressorRegistry.lookupDecompressor(encoding);
if (decompressor == null) {
stream.cancel(Status.INTERNAL.withDescription(
String.format("Can't find decompressor for %s", encoding)));
return;
}
}
stream.setDecompressor(decompressor);
class HeadersRead extends ContextRunnable {
HeadersRead() {
super(context);
}
@Override
public final void runInContext() {
try {
if (closed) {
return;
}
observer.onHeaders(headers);
} catch (Throwable t) {
Status status =
Status.CANCELLED.withCause(t).withDescription("Failed to read headers");
stream.cancel(status);
close(status, new Metadata());
}
}
}
callExecutor.execute(new HeadersRead());
}
@Override
public void messageRead(final InputStream message) {
class MessageRead extends ContextRunnable {
MessageRead() {
super(context);
}
@Override
public final void runInContext() {
try {
if (closed) {
return;
}
try {
observer.onMessage(method.parseResponse(message));
} finally {
message.close();
}
} catch (Throwable t) {
Status status =
Status.CANCELLED.withCause(t).withDescription("Failed to read message.");
stream.cancel(status);
close(status, new Metadata());
}
}
}
callExecutor.execute(new MessageRead());
}
/**
* Must be called from application thread.
*/
private void close(Status status, Metadata trailers) {
closed = true;
cancelListenersShouldBeRemoved = true;
try {
closeObserver(observer, status, trailers);
} finally {
removeContextListenerAndCancelDeadlineFuture();
}
}
@Override
public void closed(Status status, 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()) {
status = DEADLINE_EXCEEDED;
// Replace trailers to prevent mixing sources of status and trailers.
trailers = new Metadata();
}
}
final Status savedStatus = status;
final Metadata savedTrailers = trailers;
class StreamClosed extends ContextRunnable {
StreamClosed() {
super(context);
}
@Override
public final void runInContext() {
if (closed) {
// We intentionally don't keep the status or metadata from the server.
return;
}
close(savedStatus, savedTrailers);
}
}
callExecutor.execute(new StreamClosed());
}
@Override
public void onReady() {
class StreamOnReady extends ContextRunnable {
StreamOnReady() {
super(context);
}
@Override
public final void runInContext() {
try {
observer.onReady();
} catch (Throwable t) {
Status status =
Status.CANCELLED.withCause(t).withDescription("Failed to call onReady.");
stream.cancel(status);
close(status, new Metadata());
}
}
}
callExecutor.execute(new StreamOnReady());
}
}
}