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

io.grpc.internal.InternalSubchannel Maven / Gradle / Ivy

There is a newer version: 1.66.0
Show newest version
/*
 * 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.internal;

import static io.grpc.ConnectivityState.CONNECTING;
import static io.grpc.ConnectivityState.IDLE;
import static io.grpc.ConnectivityState.READY;
import static io.grpc.ConnectivityState.SHUTDOWN;
import static io.grpc.ConnectivityState.TRANSIENT_FAILURE;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.common.base.Stopwatch;
import com.google.common.base.Supplier;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import com.google.errorprone.annotations.ForOverride;
import io.grpc.Attributes;
import io.grpc.CallOptions;
import io.grpc.ChannelLogger;
import io.grpc.ChannelLogger.ChannelLogLevel;
import io.grpc.ConnectivityState;
import io.grpc.ConnectivityStateInfo;
import io.grpc.EquivalentAddressGroup;
import io.grpc.HttpConnectProxiedSocketAddress;
import io.grpc.InternalChannelz;
import io.grpc.InternalChannelz.ChannelStats;
import io.grpc.InternalInstrumented;
import io.grpc.InternalLogId;
import io.grpc.InternalWithLogId;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import io.grpc.Status;
import io.grpc.SynchronizationContext;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
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;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;

/**
 * Transports for a single {@link SocketAddress}.
 */
@ThreadSafe
final class InternalSubchannel implements InternalInstrumented {
  private static final Logger log = Logger.getLogger(InternalSubchannel.class.getName());

  private final InternalLogId logId;
  private final String authority;
  private final String userAgent;
  private final BackoffPolicy.Provider backoffPolicyProvider;
  private final Callback callback;
  private final ClientTransportFactory transportFactory;
  private final ScheduledExecutorService scheduledExecutor;
  private final InternalChannelz channelz;
  private final CallTracer callsTracer;
  private final ChannelTracer channelTracer;
  private final ChannelLogger channelLogger;

  // File-specific convention: methods without GuardedBy("lock") MUST NOT be called under the lock.
  private final Object lock = new Object();

  // File-specific convention:
  //
  // 1. In a method without GuardedBy("lock"), executeLater() MUST be followed by a drain() later in
  // the same method.
  //
  // 2. drain() MUST NOT be called under "lock".
  //
  // 3. Every synchronized("lock") must be inside a try-finally which calls drain() in "finally".
  private final SynchronizationContext syncContext;

  /**
   * The index of the address corresponding to pendingTransport/activeTransport, or at beginning if
   * both are null.
   */
  @GuardedBy("lock")
  private Index addressIndex;

  /**
   * The policy to control back off between reconnects. Non-{@code null} when a reconnect task is
   * scheduled.
   */
  @GuardedBy("lock")
  private BackoffPolicy reconnectPolicy;

  /**
   * Timer monitoring duration since entering CONNECTING state.
   */
  @GuardedBy("lock")
  private final Stopwatch connectingTimer;

  @GuardedBy("lock")
  @Nullable
  private ScheduledFuture reconnectTask;

  @GuardedBy("lock")
  private boolean reconnectCanceled;

  /**
   * All transports that are not terminated. At the very least the value of {@link #activeTransport}
   * will be present, but previously used transports that still have streams or are stopping may
   * also be present.
   */
  @GuardedBy("lock")
  private final Collection transports = new ArrayList<>();

  // Must only be used from syncContext
  private final InUseStateAggregator inUseStateAggregator =
      new InUseStateAggregator() {
        @Override
        protected void handleInUse() {
          callback.onInUse(InternalSubchannel.this);
        }

        @Override
        protected void handleNotInUse() {
          callback.onNotInUse(InternalSubchannel.this);
        }
      };

  /**
   * The to-be active transport, which is not ready yet.
   */
  @GuardedBy("lock")
  @Nullable
  private ConnectionClientTransport pendingTransport;

  /**
   * The transport for new outgoing requests. 'lock' must be held when assigning to it. Non-null
   * only in READY state.
   */
  @Nullable
  private volatile ManagedClientTransport activeTransport;

  @GuardedBy("lock")
  private ConnectivityStateInfo state = ConnectivityStateInfo.forNonError(IDLE);

  @GuardedBy("lock")
  private Status shutdownReason;

  InternalSubchannel(List addressGroups, String authority, String userAgent,
      BackoffPolicy.Provider backoffPolicyProvider,
      ClientTransportFactory transportFactory, ScheduledExecutorService scheduledExecutor,
      Supplier stopwatchSupplier, SynchronizationContext syncContext, Callback callback,
      InternalChannelz channelz, CallTracer callsTracer, ChannelTracer channelTracer,
      InternalLogId logId, TimeProvider timeProvider) {
    Preconditions.checkNotNull(addressGroups, "addressGroups");
    Preconditions.checkArgument(!addressGroups.isEmpty(), "addressGroups is empty");
    checkListHasNoNulls(addressGroups, "addressGroups contains null entry");
    this.addressIndex = new Index(
        Collections.unmodifiableList(new ArrayList<>(addressGroups)));
    this.authority = authority;
    this.userAgent = userAgent;
    this.backoffPolicyProvider = backoffPolicyProvider;
    this.transportFactory = transportFactory;
    this.scheduledExecutor = scheduledExecutor;
    this.connectingTimer = stopwatchSupplier.get();
    this.syncContext = syncContext;
    this.callback = callback;
    this.channelz = channelz;
    this.callsTracer = callsTracer;
    this.channelTracer = Preconditions.checkNotNull(channelTracer, "channelTracer");
    this.logId = InternalLogId.allocate("Subchannel", authority);
    this.channelLogger = new ChannelLoggerImpl(channelTracer, timeProvider);
  }

  ChannelLogger getChannelLogger() {
    return channelLogger;
  }

  /**
   * Returns a READY transport that will be used to create new streams.
   *
   * 

Returns {@code null} if the state is not READY. Will try to connect if state is IDLE. */ @Nullable ClientTransport obtainActiveTransport() { ClientTransport savedTransport = activeTransport; if (savedTransport != null) { return savedTransport; } try { synchronized (lock) { savedTransport = activeTransport; // Check again, since it could have changed before acquiring the lock if (savedTransport != null) { return savedTransport; } if (state.getState() == IDLE) { channelLogger.log(ChannelLogLevel.INFO, "CONNECTING as requested"); gotoNonErrorState(CONNECTING); startNewTransport(); } } } finally { syncContext.drain(); } return null; } /** * Returns a READY transport if there is any, without trying to connect. */ @Nullable ClientTransport getTransport() { return activeTransport; } /** * Returns the authority string associated with this Subchannel. */ String getAuthority() { return authority; } @GuardedBy("lock") private void startNewTransport() { Preconditions.checkState(reconnectTask == null, "Should have no reconnectTask scheduled"); if (addressIndex.isAtBeginning()) { connectingTimer.reset().start(); } SocketAddress address = addressIndex.getCurrentAddress(); HttpConnectProxiedSocketAddress proxiedAddr = null; if (address instanceof HttpConnectProxiedSocketAddress) { proxiedAddr = (HttpConnectProxiedSocketAddress) address; address = proxiedAddr.getTargetAddress(); } ClientTransportFactory.ClientTransportOptions options = new ClientTransportFactory.ClientTransportOptions() .setAuthority(authority) .setEagAttributes(addressIndex.getCurrentEagAttributes()) .setUserAgent(userAgent) .setHttpConnectProxiedSocketAddress(proxiedAddr); ConnectionClientTransport transport = new CallTracingTransport( transportFactory.newClientTransport(address, options), callsTracer); channelz.addClientSocket(transport); pendingTransport = transport; transports.add(transport); Runnable runnable = transport.start(new TransportListener(transport, address)); if (runnable != null) { syncContext.executeLater(runnable); } } /** * Only called after all addresses attempted and failed (TRANSIENT_FAILURE). * @param status the causal status when the channel begins transition to * TRANSIENT_FAILURE. */ @GuardedBy("lock") private void scheduleBackoff(final Status status) { class EndOfCurrentBackoff implements Runnable { @Override public void run() { try { synchronized (lock) { reconnectTask = null; if (reconnectCanceled) { // Even though cancelReconnectTask() will cancel this task, the task may have already // started when it's being canceled. return; } channelLogger.log(ChannelLogLevel.INFO, "CONNECTING after backoff"); gotoNonErrorState(CONNECTING); startNewTransport(); } } catch (Throwable t) { // TODO(zhangkun): we may consider using SynchronizationContext to schedule the reconnect // timer, so that we don't need this catch, since SynchronizationContext would catch it. log.log(Level.WARNING, "Exception handling end of backoff", t); } finally { syncContext.drain(); } } } gotoState(ConnectivityStateInfo.forTransientFailure(status)); if (reconnectPolicy == null) { reconnectPolicy = backoffPolicyProvider.get(); } long delayNanos = reconnectPolicy.nextBackoffNanos() - connectingTimer.elapsed(TimeUnit.NANOSECONDS); channelLogger.log( ChannelLogLevel.INFO, "TRANSIENT_FAILURE ({0}). Will reconnect after {1} ns", printShortStatus(status), delayNanos); Preconditions.checkState(reconnectTask == null, "previous reconnectTask is not done"); reconnectCanceled = false; reconnectTask = scheduledExecutor.schedule( new LogExceptionRunnable(new EndOfCurrentBackoff()), delayNanos, TimeUnit.NANOSECONDS); } /** * Immediately attempt to reconnect if the current state is TRANSIENT_FAILURE. Otherwise this * method has no effect. */ void resetConnectBackoff() { try { synchronized (lock) { if (state.getState() != TRANSIENT_FAILURE) { return; } cancelReconnectTask(); channelLogger.log(ChannelLogLevel.INFO, "CONNECTING; backoff interrupted"); gotoNonErrorState(CONNECTING); startNewTransport(); } } finally { syncContext.drain(); } } @GuardedBy("lock") private void gotoNonErrorState(ConnectivityState newState) { gotoState(ConnectivityStateInfo.forNonError(newState)); } @GuardedBy("lock") private void gotoState(final ConnectivityStateInfo newState) { if (state.getState() != newState.getState()) { Preconditions.checkState(state.getState() != SHUTDOWN, "Cannot transition out of SHUTDOWN to " + newState); state = newState; syncContext.executeLater(new Runnable() { @Override public void run() { callback.onStateChange(InternalSubchannel.this, newState); } }); } } /** Replaces the existing addresses, avoiding unnecessary reconnects. */ public void updateAddresses(List newAddressGroups) { Preconditions.checkNotNull(newAddressGroups, "newAddressGroups"); checkListHasNoNulls(newAddressGroups, "newAddressGroups contains null entry"); Preconditions.checkArgument(!newAddressGroups.isEmpty(), "newAddressGroups is empty"); newAddressGroups = Collections.unmodifiableList(new ArrayList<>(newAddressGroups)); ManagedClientTransport savedTransport = null; try { synchronized (lock) { SocketAddress previousAddress = addressIndex.getCurrentAddress(); addressIndex.updateGroups(newAddressGroups); if (state.getState() == READY || state.getState() == CONNECTING) { if (!addressIndex.seekTo(previousAddress)) { // Forced to drop the connection if (state.getState() == READY) { savedTransport = activeTransport; activeTransport = null; addressIndex.reset(); gotoNonErrorState(IDLE); } else { savedTransport = pendingTransport; pendingTransport = null; addressIndex.reset(); startNewTransport(); } } } } } finally { syncContext.drain(); } if (savedTransport != null) { savedTransport.shutdown( Status.UNAVAILABLE.withDescription( "InternalSubchannel closed transport due to address change")); } } public void shutdown(Status reason) { ManagedClientTransport savedActiveTransport; ConnectionClientTransport savedPendingTransport; try { synchronized (lock) { if (state.getState() == SHUTDOWN) { return; } shutdownReason = reason; gotoNonErrorState(SHUTDOWN); savedActiveTransport = activeTransport; savedPendingTransport = pendingTransport; activeTransport = null; pendingTransport = null; addressIndex.reset(); if (transports.isEmpty()) { handleTermination(); } // else: the callback will be run once all transports have been terminated cancelReconnectTask(); } } finally { syncContext.drain(); } if (savedActiveTransport != null) { savedActiveTransport.shutdown(reason); } if (savedPendingTransport != null) { savedPendingTransport.shutdown(reason); } } @Override public String toString() { // addressGroupsCopy being a little stale is fine, just avoid calling toString with the lock // since there may be many addresses. Object addressGroupsCopy; synchronized (lock) { addressGroupsCopy = addressIndex.getGroups(); } return MoreObjects.toStringHelper(this) .add("logId", logId.getId()) .add("addressGroups", addressGroupsCopy) .toString(); } @GuardedBy("lock") private void handleTermination() { channelLogger.log(ChannelLogLevel.INFO, "Terminated"); syncContext.executeLater(new Runnable() { @Override public void run() { callback.onTerminated(InternalSubchannel.this); } }); } private void handleTransportInUseState( final ConnectionClientTransport transport, final boolean inUse) { syncContext.execute(new Runnable() { @Override public void run() { inUseStateAggregator.updateObjectInUse(transport, inUse); } }); } void shutdownNow(Status reason) { shutdown(reason); Collection transportsCopy; try { synchronized (lock) { transportsCopy = new ArrayList(transports); } } finally { syncContext.drain(); } for (ManagedClientTransport transport : transportsCopy) { transport.shutdownNow(reason); } } List getAddressGroups() { try { synchronized (lock) { return addressIndex.getGroups(); } } finally { syncContext.drain(); } } @GuardedBy("lock") private void cancelReconnectTask() { if (reconnectTask != null) { reconnectTask.cancel(false); reconnectCanceled = true; reconnectTask = null; reconnectPolicy = null; } } @Override public InternalLogId getLogId() { return logId; } @Override public ListenableFuture getStats() { SettableFuture ret = SettableFuture.create(); ChannelStats.Builder builder = new ChannelStats.Builder(); List addressGroupsSnapshot; List transportsSnapshot; synchronized (lock) { addressGroupsSnapshot = addressIndex.getGroups(); transportsSnapshot = new ArrayList(transports); } builder.setTarget(addressGroupsSnapshot.toString()).setState(getState()); builder.setSockets(transportsSnapshot); callsTracer.updateBuilder(builder); channelTracer.updateBuilder(builder); ret.set(builder.build()); return ret; } @VisibleForTesting ConnectivityState getState() { try { synchronized (lock) { return state.getState(); } } finally { syncContext.drain(); } } private static void checkListHasNoNulls(List list, String msg) { for (Object item : list) { Preconditions.checkNotNull(item, msg); } } /** Listener for real transports. */ private class TransportListener implements ManagedClientTransport.Listener { final ConnectionClientTransport transport; final SocketAddress address; TransportListener(ConnectionClientTransport transport, SocketAddress address) { this.transport = transport; this.address = address; } @Override public void transportReady() { channelLogger.log(ChannelLogLevel.INFO, "READY"); Status savedShutdownReason; try { synchronized (lock) { savedShutdownReason = shutdownReason; reconnectPolicy = null; if (savedShutdownReason != null) { // activeTransport should have already been set to null by shutdown(). We keep it null. Preconditions.checkState(activeTransport == null, "Unexpected non-null activeTransport"); } else if (pendingTransport == transport) { gotoNonErrorState(READY); activeTransport = transport; pendingTransport = null; } } } finally { syncContext.drain(); } if (savedShutdownReason != null) { transport.shutdown(savedShutdownReason); } } @Override public void transportInUse(boolean inUse) { handleTransportInUseState(transport, inUse); } @Override public void transportShutdown(Status s) { channelLogger.log( ChannelLogLevel.INFO, "{0} SHUTDOWN with {1}", transport.getLogId(), printShortStatus(s)); try { synchronized (lock) { if (state.getState() == SHUTDOWN) { return; } if (activeTransport == transport) { gotoNonErrorState(IDLE); activeTransport = null; addressIndex.reset(); } else if (pendingTransport == transport) { Preconditions.checkState(state.getState() == CONNECTING, "Expected state is CONNECTING, actual state is %s", state.getState()); addressIndex.increment(); // Continue reconnect if there are still addresses to try. if (!addressIndex.isValid()) { pendingTransport = null; addressIndex.reset(); // Initiate backoff // Transition to TRANSIENT_FAILURE scheduleBackoff(s); } else { startNewTransport(); } } } } finally { syncContext.drain(); } } @Override public void transportTerminated() { channelLogger.log(ChannelLogLevel.INFO, "{0} Terminated", transport.getLogId()); channelz.removeClientSocket(transport); handleTransportInUseState(transport, false); try { synchronized (lock) { transports.remove(transport); if (state.getState() == SHUTDOWN && transports.isEmpty()) { handleTermination(); } } } finally { syncContext.drain(); } Preconditions.checkState(activeTransport != transport, "activeTransport still points to this transport. " + "Seems transportShutdown() was not called."); } } // All methods are called in syncContext abstract static class Callback { /** * Called when the subchannel is terminated, which means it's shut down and all transports * have been terminated. */ @ForOverride void onTerminated(InternalSubchannel is) { } /** * Called when the subchannel's connectivity state has changed. */ @ForOverride void onStateChange(InternalSubchannel is, ConnectivityStateInfo newState) { } /** * Called when the subchannel's in-use state has changed to true, which means at least one * transport is in use. */ @ForOverride void onInUse(InternalSubchannel is) { } /** * Called when the subchannel's in-use state has changed to false, which means no transport is * in use. */ @ForOverride void onNotInUse(InternalSubchannel is) { } } @VisibleForTesting static final class CallTracingTransport extends ForwardingConnectionClientTransport { private final ConnectionClientTransport delegate; private final CallTracer callTracer; private CallTracingTransport(ConnectionClientTransport delegate, CallTracer callTracer) { this.delegate = delegate; this.callTracer = callTracer; } @Override protected ConnectionClientTransport delegate() { return delegate; } @Override public ClientStream newStream( MethodDescriptor method, Metadata headers, CallOptions callOptions) { final ClientStream streamDelegate = super.newStream(method, headers, callOptions); return new ForwardingClientStream() { @Override protected ClientStream delegate() { return streamDelegate; } @Override public void start(final ClientStreamListener listener) { callTracer.reportCallStarted(); super.start(new ForwardingClientStreamListener() { @Override protected ClientStreamListener delegate() { return listener; } @Override public void closed(Status status, Metadata trailers) { callTracer.reportCallEnded(status.isOk()); super.closed(status, trailers); } @Override public void closed( Status status, RpcProgress rpcProgress, Metadata trailers) { callTracer.reportCallEnded(status.isOk()); super.closed(status, rpcProgress, trailers); } }); } }; } } /** Index as in 'i', the pointer to an entry. Not a "search index." */ @VisibleForTesting static final class Index { private List addressGroups; private int groupIndex; private int addressIndex; public Index(List groups) { this.addressGroups = groups; } public boolean isValid() { // addressIndex will never be invalid return groupIndex < addressGroups.size(); } public boolean isAtBeginning() { return groupIndex == 0 && addressIndex == 0; } public void increment() { EquivalentAddressGroup group = addressGroups.get(groupIndex); addressIndex++; if (addressIndex >= group.getAddresses().size()) { groupIndex++; addressIndex = 0; } } public void reset() { groupIndex = 0; addressIndex = 0; } public SocketAddress getCurrentAddress() { return addressGroups.get(groupIndex).getAddresses().get(addressIndex); } public Attributes getCurrentEagAttributes() { return addressGroups.get(groupIndex).getAttributes(); } public List getGroups() { return addressGroups; } /** Update to new groups, resetting the current index. */ public void updateGroups(List newGroups) { addressGroups = newGroups; reset(); } /** Returns false if the needle was not found and the current index was left unchanged. */ public boolean seekTo(SocketAddress needle) { for (int i = 0; i < addressGroups.size(); i++) { EquivalentAddressGroup group = addressGroups.get(i); int j = group.getAddresses().indexOf(needle); if (j == -1) { continue; } this.groupIndex = i; this.addressIndex = j; return true; } return false; } } private String printShortStatus(Status status) { StringBuilder buffer = new StringBuilder(); buffer.append(status.getCode()); if (status.getDescription() != null) { buffer.append("(").append(status.getDescription()).append(")"); } return buffer.toString(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy