All Downloads are FREE. Search and download functionalities are using the official Maven repository.

nl.topicus.jdbc.shaded.io.grpc.internal.ClientCallImpl Maven / Gradle / Ivy

There is a newer version: 1.1.6
Show newest version
/*
 * 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());
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy