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

org.glowroot.agent.shaded.grpc.internal.ClientCallImpl Maven / Gradle / Ivy

There is a newer version: 0.9.24
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 org.glowroot.agent.shaded.grpc.internal;

import static org.glowroot.agent.shaded.google.common.base.Preconditions.checkNotNull;
import static org.glowroot.agent.shaded.google.common.base.Preconditions.checkState;
import static org.glowroot.agent.shaded.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.glowroot.agent.shaded.grpc.internal.GrpcUtil.ACCEPT_ENCODING_JOINER;
import static org.glowroot.agent.shaded.grpc.internal.GrpcUtil.MESSAGE_ACCEPT_ENCODING_KEY;
import static org.glowroot.agent.shaded.grpc.internal.GrpcUtil.MESSAGE_ENCODING_KEY;
import static org.glowroot.agent.shaded.grpc.internal.GrpcUtil.TIMEOUT_KEY;
import static org.glowroot.agent.shaded.grpc.internal.GrpcUtil.USER_AGENT_KEY;

import org.glowroot.agent.shaded.google.common.annotations.VisibleForTesting;
import org.glowroot.agent.shaded.google.common.base.Preconditions;
import org.glowroot.agent.shaded.google.common.base.Throwables;
import org.glowroot.agent.shaded.google.common.util.concurrent.FutureCallback;
import org.glowroot.agent.shaded.google.common.util.concurrent.Futures;
import org.glowroot.agent.shaded.google.common.util.concurrent.ListenableFuture;

import org.glowroot.agent.shaded.grpc.CallOptions;
import org.glowroot.agent.shaded.grpc.ClientCall;
import org.glowroot.agent.shaded.grpc.Codec;
import org.glowroot.agent.shaded.grpc.Compressor;
import org.glowroot.agent.shaded.grpc.CompressorRegistry;
import org.glowroot.agent.shaded.grpc.Context;
import org.glowroot.agent.shaded.grpc.Decompressor;
import org.glowroot.agent.shaded.grpc.DecompressorRegistry;
import org.glowroot.agent.shaded.grpc.Metadata;
import org.glowroot.agent.shaded.grpc.MethodDescriptor;
import org.glowroot.agent.shaded.grpc.MethodDescriptor.MethodType;
import org.glowroot.agent.shaded.grpc.Status;

import java.io.InputStream;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import javax.annotation.Nullable;

/**
 * Implementation of {@link ClientCall}.
 */
final class ClientCallImpl extends ClientCall
    implements Context.CancellationListener {
  private final MethodDescriptor method;
  private final Executor callExecutor;
  private final Context context;
  private final boolean unaryRequest;
  private final CallOptions callOptions;
  private ClientStream stream;
  private volatile ScheduledFuture deadlineCancellationFuture;
  private volatile boolean deadlineCancellationFutureShouldBeCancelled;
  private boolean cancelCalled;
  private boolean halfCloseCalled;
  private final ClientTransportProvider clientTransportProvider;
  private String userAgent;
  private ScheduledExecutorService deadlineCancellationExecutor;
  private Compressor compressor;
  private DecompressorRegistry decompressorRegistry = DecompressorRegistry.getDefaultInstance();
  private CompressorRegistry compressorRegistry = 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.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) {
    cancel();
  }

  /**
   * Provider of {@link ClientTransport}s.
   */
  interface ClientTransportProvider {
    /**
     * @return a future for client transport. If no more transports can be created, e.g., channel is
     *         shut down, the future's value will be {@code null}. If the call is cancelled, it will
     *         also cancel the future.
     */
    ListenableFuture get(CallOptions callOptions);
  }

  ClientCallImpl setUserAgent(String userAgent) {
    this.userAgent = userAgent;
    return this;
  }

  ClientCallImpl setDecompressorRegistry(DecompressorRegistry decompressorRegistry) {
    this.decompressorRegistry = decompressorRegistry;
    return this;
  }

  ClientCallImpl setCompressorRegistry(CompressorRegistry compressorRegistry) {
    this.compressorRegistry = compressorRegistry;
    return this;
  }

  @VisibleForTesting
  static void prepareHeaders(Metadata headers, CallOptions callOptions, String userAgent,
      DecompressorRegistry decompressorRegistry, Compressor compressor) {
    // Fill out the User-Agent header.
    headers.removeAll(USER_AGENT_KEY);
    if (userAgent != null) {
      headers.put(USER_AGENT_KEY, userAgent);
    }

    headers.removeAll(MESSAGE_ENCODING_KEY);
    if (compressor != Codec.Identity.NONE) {
      headers.put(MESSAGE_ENCODING_KEY, compressor.getMessageEncoding());
    }

    headers.removeAll(MESSAGE_ACCEPT_ENCODING_KEY);
    if (!decompressorRegistry.getAdvertisedMessageEncodings().isEmpty()) {
      String acceptEncoding =
          ACCEPT_ENCODING_JOINER.join(decompressorRegistry.getAdvertisedMessageEncodings());
      headers.put(MESSAGE_ACCEPT_ENCODING_KEY, acceptEncoding);
    }
  }

  @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;
      callExecutor.execute(new ContextRunnable(context) {
        @Override
        public void runInContext() {
          observer.onClose(Status.CANCELLED.withCause(context.cause()), new Metadata());
        }
      });
      return;
    }
    final String compressorName = callOptions.getCompressor();
    if (compressorName != null) {
      compressor = compressorRegistry.lookupCompressor(compressorName);
      if (compressor == null) {
        stream = NoopClientStream.INSTANCE;
        callExecutor.execute(new ContextRunnable(context) {
          @Override
          public void runInContext() {
            observer.onClose(
                Status.INTERNAL.withDescription(
                    String.format("Unable to find compressor by name %s", compressorName)),
                new Metadata());
          }
        });
        return;
      }
    } else {
      compressor = Codec.Identity.NONE;
    }

    prepareHeaders(headers, callOptions, userAgent, decompressorRegistry, compressor);

    ListenableFuture transportFuture = clientTransportProvider.get(callOptions);

    if (transportFuture.isDone()) {
      // Try to skip DelayedStream when possible to avoid the overhead of a volatile read in the
      // fast path. If that fails, stream will stay null and DelayedStream will be created.
      ClientTransport transport;
      try {
        transport = transportFuture.get();
        if (transport != null && updateTimeoutHeader(callOptions.getDeadlineNanoTime(), headers)) {
          stream = transport.newStream(method, headers);
        }
      } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
      } catch (Exception e) {
        // Fall through to DelayedStream
      }
    }

    if (stream == null) {
      DelayedStream delayed;
      stream = delayed = new DelayedStream();
      Futures.addCallback(transportFuture,
          new StreamCreationTask(delayed, headers, method, callOptions),
          transportFuture.isDone() ? directExecutor() : callExecutor);
    }

    if (callOptions.getAuthority() != null) {
      stream.setAuthority(callOptions.getAuthority());
    }
    stream.setCompressor(compressor);
    if (compressor != Codec.Identity.NONE) {
      stream.setMessageCompression(true);
    }

    stream.start(new ClientStreamListenerImpl(observer, transportFuture));
    // 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

    // Start the deadline timer after stream creation because it will close the stream
    Long timeoutNanos = getRemainingTimeoutNanos(callOptions.getDeadlineNanoTime());
    if (timeoutNanos != null) {
      deadlineCancellationFuture = startDeadlineTimer(timeoutNanos);
      if (deadlineCancellationFutureShouldBeCancelled) {
        // Race detected! ClientStreamListener.closed may have been called before
        // deadlineCancellationFuture was set, thereby preventing the future from being cancelled.
        // Go ahead and cancel again, just to be sure it was cancelled.
        deadlineCancellationFuture.cancel(false);
      }
    }
    // Propagate later Context cancellation to the remote side.
    this.context.addListener(this, directExecutor());
  }

  /**
   * Based on the deadline, calculate and set the timeout to the given headers.
   *
   * @return {@code false} if deadline already exceeded
   */
  static boolean updateTimeoutHeader(@Nullable Long deadlineNanoTime, Metadata headers) {
    // Fill out timeout on the headers
    // TODO(carl-mastrangelo): Find out if this should always remove the timeout,
    // even when returning false.
    headers.removeAll(TIMEOUT_KEY);
    // Convert the deadline to timeout. Timeout is more favorable than deadline on the wire
    // because timeout tolerates the clock difference between machines.
    Long timeoutNanos = getRemainingTimeoutNanos(deadlineNanoTime);
    if (timeoutNanos != null) {
      if (timeoutNanos <= 0) {
        return false;
      }
      headers.put(TIMEOUT_KEY, timeoutNanos);
    }
    return true;
  }

  /**
   * Return the remaining amount of nanoseconds before the deadline is reached.
   *
   * 

{@code null} if deadline is not set. Negative value if already expired. */ @Nullable private static Long getRemainingTimeoutNanos(@Nullable Long deadlineNanoTime) { if (deadlineNanoTime == null) { return null; } return deadlineNanoTime - System.nanoTime(); } @Override public void request(int numMessages) { Preconditions.checkState(stream != null, "Not started"); stream.request(numMessages); } @Override public void cancel() { 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. if (stream != null) { stream.cancel(Status.CANCELLED); } } finally { context.removeListener(ClientCallImpl.this); } } @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"); boolean failed = true; try { InputStream messageIs = method.streamRequest(message); stream.writeMessage(messageIs); failed = false; } finally { // TODO(notcarl): Find out if messageIs needs to be closed. if (failed) { cancel(); } } // 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(); } private ScheduledFuture startDeadlineTimer(long timeoutNanos) { return deadlineCancellationExecutor.schedule(new Runnable() { @Override public void run() { stream.cancel(Status.DEADLINE_EXCEEDED); } }, timeoutNanos, TimeUnit.NANOSECONDS); } private class ClientStreamListenerImpl implements ClientStreamListener { private final Listener observer; private final ListenableFuture transportFuture; private boolean closed; public ClientStreamListenerImpl(Listener observer, ListenableFuture transportFuture) { this.observer = Preconditions.checkNotNull(observer, "observer"); this.transportFuture = Preconditions.checkNotNull(transportFuture, "transportFuture"); } @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); callExecutor.execute(new ContextRunnable(context) { @Override public final void runInContext() { try { if (closed) { return; } observer.onHeaders(headers); } catch (Throwable t) { cancel(); throw Throwables.propagate(t); } } }); } @Override public void messageRead(final InputStream message) { callExecutor.execute(new ContextRunnable(context) { @Override public final void runInContext() { try { if (closed) { return; } try { observer.onMessage(method.parseResponse(message)); } finally { message.close(); } } catch (Throwable t) { cancel(); throw Throwables.propagate(t); } } }); } @Override public void closed(Status status, Metadata trailers) { Long timeoutNanos = getRemainingTimeoutNanos(callOptions.getDeadlineNanoTime()); transportFuture.cancel(false); if (status.getCode() == Status.Code.CANCELLED && timeoutNanos != 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 (timeoutNanos <= 0) { status = Status.DEADLINE_EXCEEDED; // Replace trailers to prevent mixing sources of status and trailers. trailers = new Metadata(); } } final Status savedStatus = status; final Metadata savedTrailers = trailers; callExecutor.execute(new ContextRunnable(context) { @Override public final void runInContext() { try { closed = true; deadlineCancellationFutureShouldBeCancelled = true; // manually optimize the volatile read ScheduledFuture future = deadlineCancellationFuture; if (future != null) { future.cancel(false); } observer.onClose(savedStatus, savedTrailers); } finally { context.removeListener(ClientCallImpl.this); } } }); } @Override public void onReady() { callExecutor.execute(new ContextRunnable(context) { @Override public final void runInContext() { observer.onReady(); } }); } } /** * Wakes up delayed stream when the transport is ready or failed. */ @VisibleForTesting static final class StreamCreationTask implements FutureCallback { private final DelayedStream stream; private final MethodDescriptor method; private final Metadata headers; private final CallOptions callOptions; StreamCreationTask(DelayedStream stream, Metadata headers, MethodDescriptor method, CallOptions callOptions) { this.stream = stream; this.headers = headers; this.method = method; this.callOptions = callOptions; } @Override public void onSuccess(ClientTransport transport) { if (transport == null) { stream.setError(Status.UNAVAILABLE.withDescription("Channel is shutdown")); return; } if (!updateTimeoutHeader(callOptions.getDeadlineNanoTime(), headers)) { stream.setError(Status.DEADLINE_EXCEEDED); return; } // setStream correctly handles being cancelled. stream.setStream(transport.newStream(method, headers)); } @Override public void onFailure(Throwable t) { stream.setError(Status.fromThrowable(t)); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy