
org.glowroot.agent.shaded.grpc.internal.ManagedChannelImpl Maven / Gradle / Ivy
/*
* 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.grpc.internal.GrpcUtil.TIMER_SERVICE;
import org.glowroot.agent.shaded.grpc.CallOptions;
import org.glowroot.agent.shaded.grpc.Channel;
import org.glowroot.agent.shaded.grpc.ClientCall;
import org.glowroot.agent.shaded.grpc.ClientInterceptor;
import org.glowroot.agent.shaded.grpc.ClientInterceptors;
import org.glowroot.agent.shaded.grpc.Codec;
import org.glowroot.agent.shaded.grpc.Compressor;
import org.glowroot.agent.shaded.grpc.ExperimentalApi;
import org.glowroot.agent.shaded.grpc.ManagedChannel;
import org.glowroot.agent.shaded.grpc.Metadata;
import org.glowroot.agent.shaded.grpc.MethodDescriptor;
import org.glowroot.agent.shaded.grpc.Status;
import org.glowroot.agent.shaded.grpc.internal.ClientCallImpl.ClientTransportProvider;
import org.glowroot.agent.shaded.grpc.internal.ClientTransport.PingCallback;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.glowroot.agent.jul.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
/** A communication channel for making outgoing RPCs. */
@ThreadSafe
public final class ManagedChannelImpl extends ManagedChannel {
private static final Logger log = Logger.getLogger(ManagedChannelImpl.class.getName());
private final ClientTransportFactory transportFactory;
private final Executor executor;
private final boolean usingSharedExecutor;
private final String userAgent;
private final Object lock = new Object();
/**
* Executor that runs deadline timers for requests.
*/
private ScheduledExecutorService scheduledExecutor;
// TODO(carl-mastrangelo): Allow clients to pass this in
private final BackoffPolicy.Provider backoffPolicyProvider =
new ExponentialBackoffPolicy.Provider();
/**
* We delegate to this channel, so that we can have interceptors as necessary. If there aren't
* any interceptors this will just be {@link RealChannel}.
*/
private final Channel interceptorChannel;
/**
* All transports that are not stopped. At the very least {@link #activeTransport} will be
* present, but previously used transports that still have streams or are stopping may also be
* present.
*/
@GuardedBy("lock")
private Collection transports = new ArrayList();
/**
* The transport for new outgoing requests. 'this' lock must be held when assigning to
* activeTransport.
*/
private volatile ClientTransport activeTransport;
@GuardedBy("lock")
private boolean shutdown;
@GuardedBy("lock")
private boolean terminated;
private long reconnectTimeMillis;
private BackoffPolicy reconnectPolicy;
private volatile Compressor defaultCompressor;
private final ClientTransportProvider transportProvider = new ClientTransportProvider() {
@Override
public ClientTransport get() {
return obtainActiveTransport();
}
};
ManagedChannelImpl(ClientTransportFactory transportFactory, @Nullable Executor executor,
@Nullable String userAgent, List interceptors) {
this.transportFactory = transportFactory;
this.userAgent = userAgent;
this.interceptorChannel = ClientInterceptors.intercept(new RealChannel(), interceptors);
scheduledExecutor = SharedResourceHolder.get(TIMER_SERVICE);
if (executor == null) {
usingSharedExecutor = true;
this.executor = SharedResourceHolder.get(GrpcUtil.SHARED_CHANNEL_EXECUTOR);
} else {
usingSharedExecutor = false;
this.executor = executor;
}
}
/**
* Sets the default compression method for this Channel. By default, new calls will use the
* provided compressor. Each individual Call can override this by specifying it in CallOptions.
* If the remote host does not support the message encoding, the call will likely break. There
* is currently no provided way to discover what message encodings the remote host supports.
* @param c The compressor to use. If {@code null} no compression will by performed. This is
* equivalent to using {@code Codec.Identity.NONE}. If not null, the Compressor must be
* threadsafe.
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/492")
public void setDefaultCompressor(@Nullable Compressor c) {
defaultCompressor = (c != null) ? c : Codec.Identity.NONE;
}
/**
* Initiates an orderly shutdown in which preexisting calls continue but new calls are immediately
* cancelled.
*/
@Override
public ManagedChannelImpl shutdown() {
ClientTransport savedActiveTransport;
synchronized (lock) {
if (shutdown) {
return this;
}
shutdown = true;
// After shutdown there are no new calls, so no new cancellation tasks are needed
scheduledExecutor = SharedResourceHolder.release(TIMER_SERVICE, scheduledExecutor);
savedActiveTransport = activeTransport;
if (savedActiveTransport != null) {
activeTransport = null;
} else if (transports.isEmpty()) {
terminated = true;
lock.notifyAll();
onChannelTerminated();
}
}
if (savedActiveTransport != null) {
savedActiveTransport.shutdown();
}
return this;
}
/**
* Initiates a forceful shutdown in which preexisting and new calls are cancelled. Although
* forceful, the shutdown process is still not instantaneous; {@link #isTerminated()} will likely
* return {@code false} immediately after this method returns.
*
* NOT YET IMPLEMENTED. This method currently behaves identically to shutdown().
*/
// TODO(ejona86): cancel preexisting calls.
@Override
public ManagedChannelImpl shutdownNow() {
shutdown();
return this;
}
@Override
public boolean isShutdown() {
synchronized (lock) {
return shutdown;
}
}
@Override
public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
synchronized (lock) {
long timeoutNanos = unit.toNanos(timeout);
long endTimeNanos = System.nanoTime() + timeoutNanos;
while (!terminated && (timeoutNanos = endTimeNanos - System.nanoTime()) > 0) {
TimeUnit.NANOSECONDS.timedWait(lock, timeoutNanos);
}
return terminated;
}
}
@Override
public boolean isTerminated() {
synchronized (lock) {
return terminated;
}
}
/**
* Pings the remote endpoint to verify that the transport is still active. When an acknowledgement
* is received, the given callback will be invoked using the given executor.
*
*
If the underlying transport has no mechanism by when to send a ping, this method may throw
* an {@link UnsupportedOperationException}. The operation may {@linkplain
* org.glowroot.agent.shaded.grpc.internal.ClientTransport.PingCallback#pingFailed(Throwable) fail} due to transient
* transport errors. In that case, trying again may succeed.
*
* @see ClientTransport#ping(ClientTransport.PingCallback, Executor)
*/
public void ping(final PingCallback callback, final Executor executor) {
try {
obtainActiveTransport().ping(callback, executor);
} catch (final RuntimeException ex) {
executor.execute(new Runnable() {
@Override
public void run() {
callback.pingFailed(ex);
}
});
}
}
/*
* Creates a new outgoing call on the channel.
*/
@Override
public ClientCall newCall(MethodDescriptor method,
CallOptions callOptions) {
boolean hasCodecOverride = callOptions.getCompressor() != null;
if (!hasCodecOverride && defaultCompressor != Codec.Identity.NONE) {
callOptions = callOptions.withCompressor(defaultCompressor);
}
return interceptorChannel.newCall(method, callOptions);
}
@Override
public String authority() {
return interceptorChannel.authority();
}
private ClientTransport obtainActiveTransport() {
ClientTransport savedActiveTransport = activeTransport;
// If we know there is an active transport and we are not in backoff mode, return quickly.
if (savedActiveTransport != null && !(savedActiveTransport instanceof InactiveTransport)) {
return savedActiveTransport;
}
synchronized (lock) {
if (shutdown) {
return null;
}
savedActiveTransport = activeTransport;
if (savedActiveTransport instanceof InactiveTransport) {
if (System.nanoTime() > TimeUnit.MILLISECONDS.toNanos(reconnectTimeMillis)) {
// The timeout expired, clear the inactive transport and update the shutdown status to
// something that is retryable.
activeTransport = null;
savedActiveTransport = activeTransport;
} else {
// We are still in backoff mode, just return the inactive transport.
return savedActiveTransport;
}
}
if (savedActiveTransport != null) {
return savedActiveTransport;
}
// There is no active transport, or we just finished backoff. Create a new transport.
ClientTransport newActiveTransport = transportFactory.newClientTransport();
transports.add(newActiveTransport);
boolean failed = true;
try {
newActiveTransport.start(new TransportListener(newActiveTransport));
failed = false;
} finally {
if (failed) {
transports.remove(newActiveTransport);
}
}
// It's possible that start() called transportShutdown() and transportTerminated(). If so, we
// wouldn't want to make it the active transport.
if (transports.contains(newActiveTransport)) {
// start() must return before we set activeTransport, since activeTransport is accessed
// without a lock.
activeTransport = newActiveTransport;
}
return newActiveTransport;
}
}
private class RealChannel extends Channel {
@Override
public ClientCall newCall(MethodDescriptor method,
CallOptions callOptions) {
return new ClientCallImpl(
method,
new SerializingExecutor(executor),
callOptions,
transportProvider,
scheduledExecutor)
.setUserAgent(userAgent);
}
@Override
public String authority() {
return transportFactory.authority();
}
}
private class TransportListener implements ClientTransport.Listener {
private final ClientTransport transport;
public TransportListener(ClientTransport transport) {
this.transport = transport;
}
@Override
public void transportReady() {
synchronized (lock) {
if (activeTransport == transport) {
reconnectPolicy = null;
}
}
}
@Override
public void transportShutdown(Status s) {
synchronized (lock) {
if (activeTransport == transport) {
activeTransport = null;
// This transport listener was attached to the active transport.
if (s.isOk()) {
return;
}
// Alright, something bad has happened.
if (reconnectPolicy == null) {
// This happens the first time something bad has happened.
reconnectPolicy = backoffPolicyProvider.get();
reconnectTimeMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
}
activeTransport = new InactiveTransport(s);
reconnectTimeMillis += reconnectPolicy.nextBackoffMillis();
}
}
}
@Override
public void transportTerminated() {
synchronized (lock) {
if (activeTransport == transport) {
log.warning("transportTerminated called without previous transportShutdown");
activeTransport = null;
}
// TODO(notcarl): replace this with something more meaningful
transportShutdown(Status.UNKNOWN.withDescription("transport shutdown for unknown reason"));
transports.remove(transport);
if (shutdown && transports.isEmpty()) {
if (terminated) {
log.warning("transportTerminated called after already terminated");
}
terminated = true;
lock.notifyAll();
onChannelTerminated();
}
}
}
}
/**
* If we're using the shared executor, returns its reference.
*/
private void onChannelTerminated() {
if (usingSharedExecutor) {
SharedResourceHolder.release(GrpcUtil.SHARED_CHANNEL_EXECUTOR, (ExecutorService) executor);
}
// Release the transport factory so that it can deallocate any resources.
transportFactory.release();
}
private static final class InactiveTransport implements ClientTransport {
private final Status shutdownStatus;
private InactiveTransport(Status s) {
shutdownStatus = s;
}
@Override
public ClientStream newStream(
MethodDescriptor, ?> method, Metadata headers, ClientStreamListener listener) {
listener.closed(shutdownStatus, new Metadata());
return new ClientCallImpl.NoopClientStream();
}
@Override
public void start(Listener listener) {
throw new IllegalStateException();
}
@Override
public void ping(final PingCallback callback, Executor executor) {
executor.execute(new Runnable() {
@Override
public void run() {
callback.pingFailed(shutdownStatus.asException());
}
});
}
@Override
public void shutdown() {
// no-op
}
}
}